import { describe, expect, it } from "vitest"; import { classifyUpdate, isSoftUpdateDismissed, isTrustedStoreUrl, parseNativeBuild, persistSoftUpdateDismiss, selectAppUpdate, softDismissKey, } from "../../src/app-update"; import type { DistributionStore } from "../../src/config"; import type { MobileUpdatePolicy } from "../../src/types"; const policy = (overrides: Partial = {}): MobileUpdatePolicy => ({ enabled: true, minimum_build: 2, latest_build: 4, latest_version: "1.0.4", store_url: "https://play.google.com/store/apps/details?id=ru.han.chat", ...overrides, }); describe("app update classification", () => { it("классифицирует целочисленные границы как force, soft и none", () => { expect(classifyUpdate(1, policy())).toBe("force"); expect(classifyUpdate(2, policy())).toBe("soft"); expect(classifyUpdate(3, policy())).toBe("soft"); expect(classifyUpdate(4, policy())).toBe("none"); expect(classifyUpdate(5, policy())).toBe("none"); }); it("работает fail-open для выключенной, отсутствующей и некорректной политики", () => { expect(classifyUpdate(null, policy())).toBe("none"); expect(classifyUpdate(1, policy({ enabled: false }))).toBe("none"); expect(classifyUpdate(1.5, policy())).toBe("none"); expect(classifyUpdate(1, policy({ minimum_build: 5, latest_build: 4 }))).toBe("none"); }); it("принимает только положительный integer native build", () => { expect(parseNativeBuild("2")).toBe(2); expect(parseNativeBuild("002")).toBeNull(); expect(parseNativeBuild("2.0")).toBeNull(); expect(parseNativeBuild("0")).toBeNull(); expect(parseNativeBuild(undefined)).toBeNull(); }); }); describe("store channels and URLs", () => { const trusted: Record = { google_play: "https://play.google.com/store/apps/details?id=ru.han.chat", rustore: "https://www.rustore.ru/catalog/app/ru.han.chat", app_store: "https://apps.apple.com/ru/app/han-chat/id123456789", }; it("разрешает только HTTPS URL доверенного магазина для каждого канала", () => { for (const [channel, url] of Object.entries(trusted) as Array<[DistributionStore, string]>) { expect(isTrustedStoreUrl(channel, url)).toBe(true); } expect(isTrustedStoreUrl("google_play", trusted.rustore)).toBe(false); expect(isTrustedStoreUrl("rustore", "http://www.rustore.ru/catalog/app/ru.han.chat")).toBe(false); expect(isTrustedStoreUrl("app_store", "https://apps.apple.com.evil.test/app/id1")).toBe(false); expect(isTrustedStoreUrl("google_play", `${trusted.google_play}#redirect`)).toBe(false); expect(isTrustedStoreUrl( "google_play", "https://play.google.com/store/apps/details?id=ru.attacker.app", )).toBe(false); expect(isTrustedStoreUrl( "rustore", "https://www.rustore.ru/catalog/app/ru.attacker.app", )).toBe(false); expect(isTrustedStoreUrl("rustore", `${trusted.rustore}?ref=other`)).toBe(false); expect(isTrustedStoreUrl("app_store", "https://apps.apple.com/ru/app/han-chat")).toBe(false); }); it("выбирает только политику канала текущей store-сборки", () => { const policies = { google_play: policy({ store_url: trusted.google_play }), rustore: policy({ store_url: trusted.rustore }), app_store: policy({ store_url: trusted.app_store }), }; const update = selectAppUpdate("rustore", { version: "1.0.1", build: 1 }, policies); expect(update).toMatchObject({ kind: "force", channel: "rustore", storeUrl: trusted.rustore }); expect(selectAppUpdate(null, { version: "1.0.1", build: 1 }, policies)).toBeNull(); }); it("формирует отдельный dismiss key по каналу и latest build", () => { expect(softDismissKey("google_play", 4)).toBe("han.app-update.dismissed.google_play.4"); expect(softDismissKey("rustore", 4)).not.toBe(softDismissKey("google_play", 4)); expect(softDismissKey("google_play", 5)).not.toBe(softDismissKey("google_play", 4)); }); it("не скрывает soft update при ошибке чтения SecureStore", async () => { const update = selectAppUpdate( "google_play", { version: "1.0.1", build: 2 }, { google_play: policy() }, ); expect(update?.kind).toBe("soft"); await expect(isSoftUpdateDismissed(update!, { getItem: async () => { throw new Error("secure store unavailable"); }, })).resolves.toBe(false); }); it("позволяет закрыть soft update при ошибке записи SecureStore", async () => { const update = selectAppUpdate( "google_play", { version: "1.0.1", build: 2 }, { google_play: policy() }, ); await expect(persistSoftUpdateDismiss(update!, { setItem: async () => { throw new Error("secure store unavailable"); }, })).resolves.toBe(false); }); });