115 lines
5.0 KiB
TypeScript
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);
|
|
});
|
|
});
|