Skip to content
Source
Sign inSign up

linkEnvironment.test.ts

268 lines8.3 KB

1import { beforeEach, vi } from "vite-plus/test";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { EnvironmentId } from "@t3tools/contracts";
6import { RelayMobileClientId } from "@t3tools/contracts/relay";
7import { ManagedRelay } from "@t3tools/client-runtime/relay";
8import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
9import { HttpClient } from "effect/http";
10​
11import * as MobileStorage from "../../persistence/mobile-storage";
12​
13import { linkEnvironmentToCloudWithPreference } from "./linkEnvironment";
14​
15vi.mock("expo-constants", () => ({
16 default: {
17 expoConfig: {
18 extra: {
19 relay: {
20 url: "https://relay.example.test",
21 },
22 },
23 },
24 },
25}));
26​
27vi.mock("expo-device", () => ({
28 deviceType: 1,
29 DeviceType: {
30 UNKNOWN: 0,
31 PHONE: 1,
32 TABLET: 2,
33 DESKTOP: 3,
34 TV: 4,
35 },
36 osVersion: "18.4.1",
37 modelName: "iPhone 15 Pro",
38}));
39​
40vi.mock("react-native", () => ({
41 Platform: {
42 OS: "ios",
43 },
44}));
45​
46vi.mock("expo-secure-store", () => ({
47 deleteItemAsync: vi.fn(),
48 getItemAsync: vi.fn(),
49 setItemAsync: vi.fn(),
50}));
51​
52const savedConnection = {
53 environmentId: EnvironmentId.make("env-1"),
54 environmentLabel: "Desktop",
55 pairingUrl: "https://desktop.example.test/",
56 displayUrl: "https://desktop.example.test/",
57 httpBaseUrl: "https://desktop.example.test/",
58 wsBaseUrl: "wss://desktop.example.test/ws",
59 bearerToken: "local-bearer",
60};
61​
62const createProofMock = vi.fn(
63 (input: { readonly method: string; readonly url: string; readonly accessToken?: string }) =>
64 Effect.succeed(`dpop:${input.method}:${input.url}`),
65);
66const layerTestDpopSigner = Layer.succeed(
67 ManagedRelay.ManagedRelayDpopSigner,
68 ManagedRelay.ManagedRelayDpopSigner.of({
69 thumbprint: Effect.succeed("client-proof-key-thumbprint"),
70 createProof: (input) => createProofMock(input),
71 }),
72);
73​
74function layerCloudClient() {
75 const layerHttpClient = layerRemoteHttpClient((input, init) => globalThis.fetch(input, init));
76 return Layer.mergeAll(
77 layerHttpClient,
78 Layer.succeed(
79 MobileStorage.MobileStorage,
80 MobileStorage.MobileStorage.of({
81 loadOrCreateAgentAwarenessDeviceId: Effect.succeed("device-1"),
82 loadAgentAwarenessDeviceId: Effect.succeed("device-1"),
83 loadAgentAwarenessRegistrationRecord: Effect.succeed(null),
84 saveAgentAwarenessRegistrationRecord: () => Effect.void,
85 clearAgentAwarenessRegistrationRecord: Effect.void,
86 loadRecentThreadShortcuts: Effect.succeed([]),
87 saveRecentThreadShortcuts: () => Effect.void,
88 }),
89 ),
90 ManagedRelay.layer({
91 relayUrl: "https://relay.example.test",
92 clientId: RelayMobileClientId,
93 }).pipe(Layer.provideMerge(layerTestDpopSigner), Layer.provide(layerHttpClient)),
94 );
96​
97const withCloudServices = <A, E>(
98 effect: Effect.Effect<
99 A,
100 E,
101 | HttpClient.HttpClient
102 | ManagedRelay.ManagedRelayClient
103 | ManagedRelay.ManagedRelayDpopSigner
104 | MobileStorage.MobileStorage
105 >,
106) => effect.pipe(Effect.provide(layerCloudClient()));
107​
108function validLinkProof() {
109 return "signed-environment-link-jwt";
111​
112function validLinkResponse(environmentId = "env-1") {
113 return {
114 ok: true,
115 environmentId,
116 endpoint: {
117 httpBaseUrl: "https://managed.example.test/",
118 wsBaseUrl: "wss://managed.example.test/ws",
119 providerKind: "cloudflare_tunnel",
120 },
121 endpointRuntime: {
122 providerKind: "cloudflare_tunnel",
123 connectorToken: "connector-token",
124 },
125 relayIssuer: "https://relay.example.test",
126 cloudUserId: "user_123",
127 environmentCredential: "environment-credential",
128 cloudMintPublicKey: "cloud-mint-public-key",
129 };
131​
132function validLinkChallengeResponse() {
133 return {
134 challenge: "link-challenge",
135 expiresAt: "2026-05-25T00:05:00.000Z",
136 };
138​
139function requestBodyText(body: BodyInit | null | undefined): string {
140 return body instanceof Uint8Array ? new TextDecoder().decode(body) : String(body ?? "");
142​
143describe("mobile cloud link environment client", () => {
144 beforeEach(() => {
145 vi.restoreAllMocks();
146 createProofMock.mockClear();
147 });
148​
149 it.effect(
150 "rejects relay link credentials for a different environment before persisting relay config",
151 () =>
152 Effect.gen(function* () {
153 const fetchMock = vi.fn((url: string | URL) => {
154 if (String(url).endsWith("/v1/client/environment-link-challenges")) {
155 return Promise.resolve(Response.json(validLinkChallengeResponse()));
156 }
157 if (String(url).endsWith("/api/connect/link-proof")) {
158 return Promise.resolve(Response.json(validLinkProof()));
159 }
160 return Promise.resolve(Response.json(validLinkResponse("env-other")));
161 });
162 vi.stubGlobal("fetch", fetchMock);
163​
164 const error = yield* withCloudServices(
165 linkEnvironmentToCloudWithPreference({
166 clerkToken: "clerk-token",
167 connection: savedConnection,
168 liveActivitiesEnabled: true,
169 }),
170 ).pipe(Effect.flip);
171 expect(error).toMatchObject({
172 _tag: "CloudEnvironmentLinkError",
173 message: "Relay returned credentials for a different environment.",
174 });
175 expect(fetchMock).toHaveBeenCalledTimes(3);
176 }),
177 );
178​
179 it.effect("rejects relay link credentials for a different managed endpoint provider", () =>
180 Effect.gen(function* () {
181 const fetchMock = vi.fn((url: string | URL) => {
182 if (String(url).endsWith("/v1/client/environment-link-challenges")) {
183 return Promise.resolve(Response.json(validLinkChallengeResponse()));
184 }
185 if (String(url).endsWith("/api/connect/link-proof")) {
186 return Promise.resolve(Response.json(validLinkProof()));
187 }
188 return Promise.resolve(
189 Response.json({
190 ...validLinkResponse(),
191 endpoint: {
192 ...validLinkResponse().endpoint,
193 providerKind: "manual",
194 },
195 }),
196 );
197 });
198 vi.stubGlobal("fetch", fetchMock);
199​
200 const error = yield* withCloudServices(
201 linkEnvironmentToCloudWithPreference({
202 clerkToken: "clerk-token",
203 connection: savedConnection,
204 liveActivitiesEnabled: true,
205 }),
206 ).pipe(Effect.flip);
207 expect(error).toMatchObject({
208 _tag: "CloudEnvironmentLinkError",
209 message: "Relay returned credentials for a different endpoint provider.",
210 });
211 expect(fetchMock).toHaveBeenCalledTimes(3);
212 }),
213 );
214​
215 it.effect("preserves disabled Live Activity preferences when linking an environment", () =>
216 Effect.gen(function* () {
217 const bodies: Array<unknown> = [];
218 const fetchMock = vi.fn((url: string | URL, init?: RequestInit) => {
219 if (init?.body) {
220 bodies.push(JSON.parse(requestBodyText(init.body)));
221 }
222 if (String(url).endsWith("/v1/client/environment-link-challenges")) {
223 return Promise.resolve(Response.json(validLinkChallengeResponse()));
224 }
225 if (String(url).endsWith("/api/connect/link-proof")) {
226 return Promise.resolve(Response.json(validLinkProof()));
227 }
228 if (String(url).endsWith("/v1/client/environment-links")) {
229 return Promise.resolve(Response.json(validLinkResponse()));
230 }
231 return Promise.resolve(
232 Response.json({ ok: true, endpointRuntimeStatus: { status: "configured" } }),
233 );
234 });
235 vi.stubGlobal("fetch", fetchMock);
236​
237 yield* withCloudServices(
238 linkEnvironmentToCloudWithPreference({
239 clerkToken: "clerk-token",
240 connection: savedConnection,
241 liveActivitiesEnabled: false,
242 }),
243 );
244​
245 expect(bodies[1]).toMatchObject({
246 endpoint: {
247 httpBaseUrl: "https://desktop.example.test/",
248 wsBaseUrl: "wss://desktop.example.test/ws",
249 providerKind: "cloudflare_tunnel",
250 },
251 origin: {
252 localHttpHost: "127.0.0.1",
253 localHttpPort: 443,
254 },
255 });
256 expect(bodies[2]).toMatchObject({
257 deviceId: "device-1",
258 notificationsEnabled: true,
259 liveActivitiesEnabled: false,
260 managedTunnelsEnabled: true,
261 });
262 expect(bodies[3]).toMatchObject({
263 cloudUserId: "user_123",
264 environmentCredential: "environment-credential",
265 });
266 }),
267 );
268});