/// import * as NodeCrypto from "node:crypto"; import { beforeEach, vi } from "vite-plus/test"; import { describe, expect, it } from "@effect/vitest"; import Constants from "expo-constants"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import { FetchHttpClient } from "effect/unstable/http"; import { ManagedRelay } from "@t3tools/client-runtime/relay"; import type { EnvironmentId } from "@t3tools/contracts"; import { verifyDpopProof } from "@t3tools/shared/dpop"; import type { SavedRemoteConnection } from "../../lib/connection"; import { cryptoLayer } from "../cloud/dpop"; import { managedRelayClientLayer } from "../cloud/managedRelayLayer"; import { makeRelayDeviceRegistrationRequest } from "./registrationPayload"; import { AgentAwarenessOperationError, __resetAgentAwarenessRemoteRegistrationForTest, refreshActiveLiveActivityRemoteRegistration, refreshAgentAwarenessRegistration, normalizeAgentAwarenessRelayBaseUrl, registerAgentAwarenessConnection, registerLiveActivityPushToken, setAgentAwarenessRelayTokenProvider, shouldRegisterAgentAwarenessDeviceForProvider, unregisterAgentAwarenessConnection, } from "./remoteRegistration"; import * as Notifications from "expo-notifications"; const secureStore = vi.hoisted(() => new Map()); const widgetMocks = vi.hoisted(() => ({ getInstances: vi.fn(() => []), })); const backgroundRuntime = vi.hoisted(() => ({ pending: [] as Array<{ readonly operation: unknown; readonly resolve: (exit: Exit.Exit) => void; }>, })); vi.mock("expo-constants", () => ({ default: { expoConfig: { version: "1.0.0", extra: {}, }, }, })); vi.mock("expo-widgets", () => ({ addPushToStartTokenListener: vi.fn(() => ({ remove: vi.fn() })), })); vi.mock("../../widgets/AgentActivity", () => ({ default: { getInstances: widgetMocks.getInstances, }, })); vi.mock("expo-notifications", () => ({ addPushTokenListener: vi.fn(() => ({ remove: vi.fn() })), getDevicePushTokenAsync: vi.fn(() => Promise.resolve({ type: "ios", data: "apns-token" })), getPermissionsAsync: vi.fn(() => Promise.resolve({ granted: true })), })); vi.mock("expo-crypto", () => ({ CryptoDigestAlgorithm: { SHA1: "SHA-1", SHA256: "SHA-256", SHA384: "SHA-384", SHA512: "SHA-512", }, getRandomBytes: (byteCount: number) => new Uint8Array(NodeCrypto.randomBytes(byteCount)), getRandomBytesAsync: (byteCount: number) => Promise.resolve(new Uint8Array(NodeCrypto.randomBytes(byteCount))), digest: (algorithm: string, data: unknown) => { if (!(data instanceof Uint8Array)) { return Promise.reject(new TypeError("expo-crypto digest data must be a typed array.")); } return Promise.resolve( new Uint8Array(NodeCrypto.createHash(algorithm).update(data).digest()).buffer, ); }, })); vi.mock("expo-secure-store", () => ({ getItemAsync: (key: string) => Promise.resolve(secureStore.get(key) ?? null), setItemAsync: (key: string, value: string) => { secureStore.set(key, value); return Promise.resolve(); }, })); vi.mock("react-native", () => ({ Platform: { OS: "ios", Version: "18.0", }, })); vi.mock("../../lib/runtime", () => ({ runtime: { runPromiseExit: (operation: unknown) => new Promise((resolve) => { backgroundRuntime.pending.push({ operation, resolve }); }), }, })); vi.mock("../../lib/storage", () => ({ loadAgentAwarenessDeviceId: vi.fn(() => Promise.resolve("device-1")), loadOrCreateAgentAwarenessDeviceId: vi.fn(() => Promise.resolve("device-1")), loadPreferences: vi.fn(() => Promise.resolve({})), })); function proofIat(proof: string): number { const payload = proof.split(".")[1]; if (!payload) { throw new Error("Missing DPoP payload."); } const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { readonly iat: number; }; return decoded.iat; } function savedConnection(): SavedRemoteConnection { return { environmentId: "env-1" as EnvironmentId, environmentLabel: "Desktop", pairingUrl: "https://desktop.example/pair", displayUrl: "https://desktop.example", httpBaseUrl: "https://desktop.example", wsBaseUrl: "wss://desktop.example/ws", bearerToken: "bearer-token", }; } const relayTestLayer = managedRelayClientLayer("https://relay.example.test").pipe( Layer.provide(Layer.mergeAll(FetchHttpClient.layer, cryptoLayer)), ); const runBackgroundOperations = Effect.fn("TestRemoteRegistration.runBackgroundOperations")( function* () { let idlePasses = 0; for (;;) { yield* Effect.promise(() => Promise.resolve()); const pending = backgroundRuntime.pending.shift(); if (!pending) { idlePasses++; if (idlePasses >= 3) { return; } continue; } idlePasses = 0; const exit = yield* Effect.exit( pending.operation as Effect.Effect, ); yield* Effect.sync(() => { pending.resolve(exit); }); } }, ); describe("makeRelayDeviceRegistrationRequest", () => { beforeEach(() => { vi.unstubAllGlobals(); vi.stubGlobal("__DEV__", false); secureStore.clear(); backgroundRuntime.pending.length = 0; Constants.expoConfig!.extra = {}; __resetAgentAwarenessRemoteRegistrationForTest(); widgetMocks.getInstances.mockReset(); widgetMocks.getInstances.mockReturnValue([]); }); it("preserves disabled Live Activity preferences in relay registrations", () => { expect( makeRelayDeviceRegistrationRequest({ deviceId: "device-1", label: "Julius's iPhone", iosMajorVersion: 18, appVersion: "1.0.0", pushToken: "apns-token", pushToStartToken: "push-to-start-token", notificationsEnabled: true, preferences: { liveActivitiesEnabled: false, }, }), ).toEqual({ deviceId: "device-1", label: "Julius's iPhone", platform: "ios", iosMajorVersion: 18, appVersion: "1.0.0", pushToken: "apns-token", pushToStartToken: "push-to-start-token", preferences: { liveActivitiesEnabled: false, notificationsEnabled: true, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, notifyOnFailure: true, }, }); }); it("marks notification delivery disabled when APNs permission is unavailable", () => { expect( makeRelayDeviceRegistrationRequest({ deviceId: "device-1", label: "Julius's iPhone", iosMajorVersion: 18, appVersion: "1.0.0", pushToStartToken: "push-to-start-token", notificationsEnabled: false, preferences: { liveActivitiesEnabled: true, }, }), ).toEqual({ deviceId: "device-1", label: "Julius's iPhone", platform: "ios", iosMajorVersion: 18, appVersion: "1.0.0", pushToStartToken: "push-to-start-token", preferences: { liveActivitiesEnabled: true, notificationsEnabled: false, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, notifyOnFailure: true, }, }); }); it("normalizes relay base URLs for APNs registration requests", () => { expect(normalizeAgentAwarenessRelayBaseUrl(" https://relay.example.test/// ")).toBe( "https://relay.example.test", ); expect(normalizeAgentAwarenessRelayBaseUrl(" ")).toBeNull(); }); it.effect("registers at most one listener while a Live Activity push token is pending", () => { registerAgentAwarenessConnection(savedConnection()); const addPushTokenListener = vi.fn(); const activity = { getPushToken: vi.fn(() => Promise.resolve(null)), addPushTokenListener, }; return Effect.gen(function* () { expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false); expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false); expect(activity.getPushToken).toHaveBeenCalledTimes(2); expect(addPushTokenListener).toHaveBeenCalledTimes(1); }).pipe(Effect.provide(relayTestLayer)); }); it.effect("preserves Live Activity push-token lookup failures", () => { const cause = new Error("native token lookup failed"); const activity = { getPushToken: vi.fn(() => Promise.reject(cause)), addPushTokenListener: vi.fn(), }; return Effect.gen(function* () { const error = yield* Effect.flip( registerLiveActivityPushToken({ activity: activity as never }), ); expect(error).toBeInstanceOf(AgentAwarenessOperationError); expect(error).toMatchObject({ _tag: "AgentAwarenessOperationError", operation: "read-live-activity-push-token", cause, message: "Agent awareness operation read-live-activity-push-token failed.", }); }).pipe(Effect.provide(relayTestLayer)); }); it.effect( "reports Live Activity token registration as skipped when relay auth is unavailable", () => { registerAgentAwarenessConnection(savedConnection()); const activity = { getPushToken: vi.fn(() => Promise.resolve("activity-token")), addPushTokenListener: vi.fn(), }; return Effect.gen(function* () { expect(yield* registerLiveActivityPushToken({ activity: activity as never })).toBe(false); }).pipe(Effect.provide(relayTestLayer)); }, ); it.effect( "registers APNS-started Live Activities for relay updates without mutating them locally", () => { const activity = { getPushToken: vi.fn(() => Promise.resolve("activity-token")), addPushTokenListener: vi.fn(), start: vi.fn(), update: vi.fn(), end: vi.fn(), }; widgetMocks.getInstances.mockReturnValue([activity] as never); setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); return Effect.gen(function* () { yield* refreshActiveLiveActivityRemoteRegistration(); expect(activity.getPushToken).toHaveBeenCalled(); expect(activity.start).not.toHaveBeenCalled(); expect(activity.update).not.toHaveBeenCalled(); expect(activity.end).not.toHaveBeenCalled(); }).pipe(Effect.provide(relayTestLayer)); }, ); it.effect("refreshes APNs registration for connected environments after settings changes", () => { registerAgentAwarenessConnection(savedConnection()); return Effect.gen(function* () { yield* runBackgroundOperations(); vi.mocked(Notifications.getDevicePushTokenAsync).mockClear(); yield* refreshAgentAwarenessRegistration(); expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1); }).pipe(Effect.provide(relayTestLayer)); }); it.effect("registers the APNs device when cloud auth becomes available", () => { const fetchMock = vi.fn((request: RequestInfo | URL) => { const url = request instanceof Request ? request.url : String(request); return Promise.resolve( Response.json( url.endsWith("/v1/client/dpop-token") ? { access_token: "relay-dpop-token", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", token_type: "DPoP", expires_in: 300, scope: "mobile:registration", } : { ok: true }, ), ); }); vi.stubGlobal("fetch", fetchMock); Constants.expoConfig!.extra = { relay: { url: "https://relay.example.test/", }, }; setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); return Effect.gen(function* () { yield* runBackgroundOperations(); expect(fetchMock).toHaveBeenCalledTimes(2); const [request, init] = fetchMock.mock.calls[1] as unknown as [ unknown, RequestInit | undefined, ]; const url = request instanceof Request ? request.url : String(request); const method = request instanceof Request ? request.method : init?.method; const headers = request instanceof Request ? request.headers : new Headers(init?.headers); const dpop = headers.get("dpop"); expect(url).toBe("https://relay.example.test/v1/mobile/devices"); expect(method).toBe("POST"); expect(headers.get("authorization")).toBe("DPoP relay-dpop-token"); expect(dpop).toEqual(expect.any(String)); if (!dpop) { throw new Error("Missing DPoP header."); } expect( verifyDpopProof({ proof: dpop, method: "POST", url: "https://relay.example.test/v1/mobile/devices", expectedAccessToken: "relay-dpop-token", nowEpochSeconds: proofIat(dpop), }), ).toMatchObject({ ok: true }); }).pipe(Effect.provide(relayTestLayer)); }); it.effect("coalesces simultaneous sign-in and environment connection registrations", () => { const fetchMock = vi.fn((request: RequestInfo | URL) => { const url = request instanceof Request ? request.url : String(request); return Promise.resolve( Response.json( url.endsWith("/v1/client/dpop-token") ? { access_token: "relay-dpop-token", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", token_type: "DPoP", expires_in: 300, scope: "mobile:registration", } : { ok: true }, ), ); }); vi.stubGlobal("fetch", fetchMock); Constants.expoConfig!.extra = { relay: { url: "https://relay.example.test/", }, }; vi.mocked(Notifications.getPermissionsAsync).mockClear(); setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); registerAgentAwarenessConnection(savedConnection()); return Effect.gen(function* () { yield* runBackgroundOperations(); expect(Notifications.getPermissionsAsync).toHaveBeenCalledTimes(1); }).pipe(Effect.provide(relayTestLayer)); }); it.effect("continues queued device registration after a failed auth lookup", () => { Constants.expoConfig!.extra = { relay: { url: "https://relay.example.test/", }, }; const tokenProvider = vi .fn<() => Promise>() .mockRejectedValueOnce(new Error("auth unavailable")) .mockResolvedValue("clerk-token-user-a"); setAgentAwarenessRelayTokenProvider(tokenProvider); const tokenListener = vi.mocked(Notifications.addPushTokenListener).mock.calls.at(-1)?.[0]; expect(tokenListener).toBeDefined(); tokenListener?.({ type: "ios", data: "rotated-apns-token" } as never); return Effect.gen(function* () { yield* runBackgroundOperations(); expect(backgroundRuntime.pending).toHaveLength(0); expect(tokenProvider).toHaveBeenCalledTimes(2); }).pipe(Effect.provide(relayTestLayer)); }); it("only registers again when the authenticated identity changes", () => { expect(shouldRegisterAgentAwarenessDeviceForProvider(null, "user-a")).toBe(true); expect(shouldRegisterAgentAwarenessDeviceForProvider("user-a", "user-a")).toBe(false); expect(shouldRegisterAgentAwarenessDeviceForProvider("user-a", "user-b")).toBe(true); expect(shouldRegisterAgentAwarenessDeviceForProvider("user-a", undefined)).toBe(true); }); it.effect("registers rotated APNs tokens without rereading the native token", () => { const fetchMock = vi.fn((request: RequestInfo | URL) => { const url = request instanceof Request ? request.url : String(request); return Promise.resolve( Response.json( url.endsWith("/v1/client/dpop-token") ? { access_token: "relay-dpop-token", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", token_type: "DPoP", expires_in: 300, scope: "mobile:registration", } : { ok: true }, ), ); }); vi.stubGlobal("fetch", fetchMock); Constants.expoConfig!.extra = { relay: { url: "https://relay.example.test/", }, }; vi.mocked(Notifications.getDevicePushTokenAsync).mockClear(); setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); const tokenListener = vi.mocked(Notifications.addPushTokenListener).mock.calls.at(-1)?.[0]; expect(tokenListener).toBeDefined(); tokenListener?.({ type: "ios", data: "rotated-apns-token" } as never); return Effect.gen(function* () { yield* runBackgroundOperations(); expect(Notifications.getDevicePushTokenAsync).toHaveBeenCalledTimes(1); }).pipe(Effect.provide(relayTestLayer)); }); it.effect( "keeps the user-scoped relay APNs device when an environment connection is removed", () => { const fetchMock = vi.fn((request: RequestInfo | URL) => { const url = request instanceof Request ? request.url : String(request); return Promise.resolve( Response.json( url.endsWith("/v1/client/dpop-token") ? { access_token: "relay-dpop-token", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", token_type: "DPoP", expires_in: 300, scope: "mobile:registration", } : { ok: true }, ), ); }); vi.stubGlobal("fetch", fetchMock); Constants.expoConfig!.extra = { relay: { url: "https://relay.example.test/", }, }; registerAgentAwarenessConnection(savedConnection()); setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); return Effect.gen(function* () { yield* runBackgroundOperations(); fetchMock.mockClear(); unregisterAgentAwarenessConnection(savedConnection().environmentId); expect(fetchMock).not.toHaveBeenCalled(); }).pipe(Effect.provide(relayTestLayer)); }, ); });