Skip to content
Source
Sign inSign up

protectedData.test.ts

121 lines4.3 KB

1import * as Effect from "effect/Effect";
2import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
3​
4const mocks = vi.hoisted(() => ({
5 appState: "background" as string | null,
6 nativeCheckSupported: true,
7 platform: "ios",
8 subscribed: Promise.withResolvers<void>(),
9 listeners: new Set<(state: string) => void>(),
10 isProtectedDataAvailable: vi.fn<() => Promise<boolean>>(),
11 readStorage: vi.fn<() => Promise<void>>(),
12}));
13​
14vi.mock("expo", () => ({
15 requireOptionalNativeModule: () => ({
16 ...(mocks.nativeCheckSupported && {
17 isProtectedDataAvailable: mocks.isProtectedDataAvailable,
18 }),
19 }),
20}));
21vi.mock("react-native", () => ({
22 Platform: {
23 get OS() {
24 return mocks.platform;
25 },
26 },
27 AppState: {
28 get currentState() {
29 return mocks.appState;
30 },
31 addEventListener: (_event: string, listener: (state: string) => void) => {
32 mocks.listeners.add(listener);
33 mocks.subscribed.resolve();
34 return { remove: () => mocks.listeners.delete(listener) };
35 },
36 },
37}));
38​
39vi.mock("../persistence/layer", async () => {
40 const Effect = await import("effect/Effect");
41 const Layer = await import("effect/Layer");
42 return { layer: Layer.effectDiscard(Effect.promise(mocks.readStorage)) };
43});
44vi.mock("../features/cloud/dpop", async () => ({
45 layer: (await import("effect/Layer")).empty,
46}));
47vi.mock("../features/cloud/managedRelayLayer", async () => {
48 const Layer = await import("effect/Layer");
49 return { layer: () => Layer.empty };
50});
51vi.mock("../features/cloud/publicConfig", () => ({
52 resolveCloudPublicConfig: () => ({ relay: { url: null } }),
53}));
54vi.mock("../features/observability/tracing", async () => ({
55 layer: (await import("effect/Layer")).empty,
56}));
57vi.mock("@t3tools/client-runtime/rpc", async () => {
58 const Layer = await import("effect/Layer");
59 return { layerRemoteHttpClient: () => Layer.empty };
60});
61​
62let runtime: typeof import("./runtime").runtime | undefined;
63​
64beforeEach(() => {
65 vi.resetModules();
66 mocks.appState = "background";
67 mocks.nativeCheckSupported = true;
68 mocks.platform = "ios";
69 mocks.subscribed = Promise.withResolvers<void>();
70 mocks.listeners.clear();
71 mocks.isProtectedDataAvailable.mockReset();
72 mocks.readStorage.mockReset().mockResolvedValue(undefined);
73});
74​
75afterEach(async () => {
76 await runtime?.dispose();
77 runtime = undefined;
78});
79​
80describe("protected storage startup", () => {
81 it.each([
82 { launch: "background", state: "background", checkRejects: false },
83 { launch: "background with a failed native check", state: "background", checkRejects: true },
84 { launch: "unset without a native check", state: null, nativeCheckSupported: false },
85 ])(
86 "keeps a locked $launch launch usable after activation without rebuilding its runtime",
87 async (scenario) => {
88 mocks.appState = scenario.state;
89 mocks.nativeCheckSupported = scenario.nativeCheckSupported ?? true;
90 mocks.isProtectedDataAvailable.mockImplementation(async () => {
91 if (scenario.checkRejects) throw new Error("Protected data check unavailable.");
92 return false;
93 });
94 mocks.readStorage.mockImplementation(async () => {
95 if (mocks.appState !== "active") throw new Error("User interaction is not allowed.");
96 });
97 ({ runtime } = await import("./runtime"));
98 const startup = runtime.runPromiseExit(Effect.succeed("ready"));
99​
100 // A missing gate finishes startup with a cached storage failure instead.
101 await Promise.race([mocks.subscribed.promise, startup]);
102 expect(mocks.readStorage).not.toHaveBeenCalled();
103 mocks.appState = "inactive";
104 for (const listener of mocks.listeners) listener("inactive");
105 await Promise.resolve();
106 expect(mocks.readStorage).not.toHaveBeenCalled();
107​
108 mocks.appState = "active";
109 for (const listener of mocks.listeners) listener("active");
110 expect(await startup).toMatchObject({ _tag: "Success", value: "ready" });
111 expect(await runtime.runPromise(Effect.succeed("still ready"))).toBe("still ready");
112 expect(mocks.readStorage).toHaveBeenCalledOnce();
113 expect(mocks.listeners.size).toBe(0);
114 mocks.appState = "background";
115 await (await import("./protectedData")).whenProtectedDataAvailable();
116 expect(mocks.isProtectedDataAvailable).toHaveBeenCalledTimes(
117 mocks.nativeCheckSupported ? 1 : 0,
118 );
119 },
120 );
121});