Files

115 lines
5.0 KiB
TypeScript

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> = {}): 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<DistributionStore, string> = {
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);
});
});