1import { expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Logger from "effect/Logger";
4import * as SecureStore from "expo-secure-store";
5import { vi } from "vite-plus/test";
6
7const secureStore = vi.hoisted(() => new Map<string, string>());
8
9vi.mock("expo-secure-store", () => ({
10 getItemAsync: vi.fn((key: string) => Promise.resolve(secureStore.get(key) ?? null)),
11 setItemAsync: vi.fn((key: string, value: string) => {
12 secureStore.set(key, value);
13 return Promise.resolve();
14 }),
15 deleteItemAsync: vi.fn((key: string) => {
16 secureStore.delete(key);
17 return Promise.resolve();
18 }),
19}));
20
21import {
22 ManagedRelayTokenStoreError,
23 managedRelayAccessTokenStore,
24} from "./managedRelayTokenStore";
25
26it.effect("round-trips and clears persisted managed relay access tokens", () =>
27 Effect.gen(function* () {
28 secureStore.clear();
29 const entries = [
30 {
31 accountId: "user-1",
32 clientId: "t3-mobile",
33 relayUrl: "https://relay.example.test",
34 thumbprint: "thumbprint",
35 scopes: ["environment:connect"],
36 accessToken: "access-token",
37 expiresAtMillis: 1_800_000,
38 },
39 ] as const;
40
41 yield* managedRelayAccessTokenStore.save(entries);
42 expect(yield* managedRelayAccessTokenStore.load).toEqual(entries);
43
44 yield* managedRelayAccessTokenStore.clear;
45 expect(yield* managedRelayAccessTokenStore.load).toEqual([]);
46 }),
47);
48
49it.effect("falls back to an empty cache when persisted data is invalid", () =>
50 Effect.gen(function* () {
51 secureStore.clear();
52 secureStore.set("t3code.cloud.relay-access-tokens", "not-json");
53
54 expect(yield* managedRelayAccessTokenStore.load).toEqual([]);
55 }),
56);
57
58it.effect("logs structured storage failures before falling back to an empty cache", () => {
59 const messages: Array<unknown> = [];
60 const logger = Logger.make(({ message }) => {
61 messages.push(message);
62 });
63 const cause = new Error("secure store unavailable");
64 vi.mocked(SecureStore.getItemAsync).mockRejectedValueOnce(cause);
65
66 return Effect.gen(function* () {
67 expect(yield* managedRelayAccessTokenStore.load).toEqual([]);
68
69 const message = messages.find(
70 (candidate) =>
71 Array.isArray(candidate) && candidate[0] === "Managed relay token store operation failed.",
72 );
73 expect(message).toBeDefined();
74 const context = (message as ReadonlyArray<unknown>)[1] as {
75 readonly cause: ManagedRelayTokenStoreError;
76 };
77 expect(context.cause).toBeInstanceOf(ManagedRelayTokenStoreError);
78 expect(context.cause).toMatchObject({
79 operation: "read",
80 storageKey: "t3code.cloud.relay-access-tokens",
81 cause,
82 });
83 expect(context.cause.message).not.toContain(cause.message);
84 }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false })));
85});