Реализована проверка версионности и требование soft\force обновлений приложения
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
const secureValues = vi.hoisted(() => new Map<string, string>());
|
||||
|
||||
vi.mock("expo-constants", () => ({
|
||||
default: { expoConfig: { version: "1.0.0" } },
|
||||
default: { expoConfig: { version: "1.0.1" } },
|
||||
}));
|
||||
vi.mock("expo-crypto", () => ({
|
||||
CryptoDigestAlgorithm: { SHA256: "SHA-256" },
|
||||
@@ -217,7 +217,7 @@ describe("OIDC device metadata", () => {
|
||||
expect(first).toMatchObject({
|
||||
han_fingerprint: "stable-fingerprint",
|
||||
han_platform: "android",
|
||||
han_app_version: "1.0.0",
|
||||
han_app_version: "1.0.1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user