1import {
2 AuthRelayReadScope,
3 AuthRelayWriteScope,
4 EnvironmentId,
5 type EnvironmentCloudLinkStateResult,
6} from "@t3tools/contracts";
7import type { RelayManagedEndpointProviderKind } from "@t3tools/contracts/relay";
8import { AsyncResult } from "effect/reactivity";
9import { act, useLayoutEffect } from "react";
10import { create, type ReactTestRenderer } from "react-test-renderer";
11import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
12
13const testState = vi.hoisted(() => ({
14 scopes: new Set<string>(),
15 snapshot: null as EnvironmentCloudLinkStateResult | null,
16 renderedSnapshot: null as EnvironmentCloudLinkStateResult | null,
17 relayProviderKind: null as RelayManagedEndpointProviderKind | null,
18 getToken: vi.fn<() => Promise<string | null>>(),
19 link: vi.fn(),
20 unlink: vi.fn(),
21 preferences: vi.fn(),
22 refreshDiscovery: vi.fn(),
23 refreshLink: vi.fn(),
24 toast: vi.fn(),
25}));
26
27vi.mock("@effect/atom-react", () => ({
28 useAtomValue: () => ({
29 environments: new Map(
30 testState.relayProviderKind === null
31 ? []
32 : [
33 [
34 "primary",
35 { environment: { endpoint: { providerKind: testState.relayProviderKind } } },
36 ],
37 ],
38 ),
39 }),
40}));
41vi.mock("@clerk/react", () => ({
42 useAuth: () => ({ getToken: testState.getToken, isSignedIn: true }),
43}));
44vi.mock("../components/ui/toast", () => ({ toastManager: { add: testState.toast } }));
45vi.mock("../state/relay", () => ({
46 relayEnvironmentDiscovery: { refresh: testState.refreshDiscovery, stateValueAtom: {} },
47}));
48vi.mock("../state/session", () => ({
49 readEnvironmentScope: (_environmentId: string, scope: string) => testState.scopes.has(scope),
50}));
51vi.mock("../state/use-atom-command", () => ({ useAtomCommand: (command: unknown) => command }));
52vi.mock("./linkEnvironmentAtoms", () => ({
53 linkPrimaryEnvironment: testState.link,
54 unlinkPrimaryEnvironment: testState.unlink,
55 updatePrimaryEnvironmentPreferences: testState.preferences,
56}));
57vi.mock("./primaryCloudLinkState", () => ({
58 readCachedPrimaryCloudLinkState: () => testState.snapshot,
59 usePrimaryCloudLinkState: () => ({
60 target,
61 data: testState.renderedSnapshot,
62 error: null,
63 isPending: false,
64 refresh: testState.refreshLink,
65 }),
66}));
67vi.mock("./publicConfig", () => ({ resolveRelayClerkTokenOptions: () => ({}) }));
68
69import { useCloudLinkController, type CloudLinkDesiredState } from "./useCloudLinkController";
70
71const target = {
72 environmentId: EnvironmentId.make("primary"),
73 label: "Primary",
74 httpBaseUrl: "http://localhost:3773",
75 wsBaseUrl: "ws://localhost:3773/ws",
76};
77const linkedState: EnvironmentCloudLinkStateResult = {
78 linked: true,
79 cloudUserId: "account-1",
80 relayUrl: "https://relay.example.com",
81 relayIssuer: "https://relay.example.com",
82 managedTunnelActive: true,
83 publishAgentActivity: false,
84};
85
86let renderer: ReactTestRenderer | null = null;
87let controller: ReturnType<typeof useCloudLinkController> | null = null;
88
89function ControllerProbe() {
90 const value = useCloudLinkController();
91 useLayoutEffect(() => {
92 controller = value;
93 });
94 return null;
95}
96
97async function mountController() {
98 await act(() => {
99 renderer = create(<ControllerProbe />);
100 });
101}
102
103async function reconcile(desired: CloudLinkDesiredState) {
104 let succeeded = false;
105 await act(async () => {
106 if (controller === null) throw new Error("Controller is not mounted.");
107 succeeded = await controller.reconcileCloudState(desired);
108 });
109 return succeeded;
110}
111
112function expectNoMutations() {
113 expect(testState.link).not.toHaveBeenCalled();
114 expect(testState.unlink).not.toHaveBeenCalled();
115 expect(testState.preferences).not.toHaveBeenCalled();
116}
117
118beforeEach(() => {
119 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
120 vi.spyOn(console, "error").mockImplementation(() => undefined);
121 testState.scopes = new Set([AuthRelayReadScope, AuthRelayWriteScope]);
122 testState.snapshot = linkedState;
123 testState.renderedSnapshot = linkedState;
124 testState.relayProviderKind = null;
125 testState.getToken.mockReset().mockResolvedValue("clerk-token");
126 for (const command of [
127 testState.link,
128 testState.unlink,
129 testState.preferences,
130 testState.refreshDiscovery,
131 ]) {
132 command.mockReset().mockResolvedValue(AsyncResult.success({}));
133 }
134 testState.refreshLink.mockReset();
135 testState.toast.mockReset();
136});
137
138afterEach(async () => {
139 await act(() => renderer?.unmount());
140 renderer = null;
141 controller = null;
142 vi.restoreAllMocks();
143 vi.unstubAllGlobals();
144});
145
146describe("useCloudLinkController", () => {
147 it.each([AuthRelayReadScope, AuthRelayWriteScope])(
148 "does not mutate without %s, even with a cached link snapshot",
149 async (scope) => {
150 await mountController();
151 testState.scopes.delete(scope);
152
153 expect(await reconcile({ managedTunnel: true, publish: true })).toBe(false);
154 expect(testState.getToken).not.toHaveBeenCalled();
155 expectNoMutations();
156 },
157 );
158
159 it("does not interpret unavailable link state as an unlinked environment", async () => {
160 testState.snapshot = null;
161 await mountController();
162
163 expect(await reconcile({ managedTunnel: false, publish: true })).toBe(false);
164 expect(testState.getToken).not.toHaveBeenCalled();
165 expectNoMutations();
166 });
167
168 it.each([AuthRelayReadScope, AuthRelayWriteScope])(
169 "rechecks %s after requesting the cloud token",
170 async (scope) => {
171 await mountController();
172 testState.getToken.mockImplementationOnce(async () => {
173 testState.scopes.delete(scope);
174 return "clerk-token";
175 });
176
177 expect(await reconcile({ managedTunnel: false, publish: false })).toBe(false);
178 expectNoMutations();
179 },
180 );
181
182 it("stops if the link-state read fails while requesting the cloud token", async () => {
183 await mountController();
184 testState.getToken.mockImplementationOnce(async () => {
185 testState.snapshot = null;
186 return "clerk-token";
187 });
188
189 expect(await reconcile({ managedTunnel: true, publish: true })).toBe(false);
190 expectNoMutations();
191 });
192
193 it("uses the latest linked mode to change publishing without replacing the tunnel", async () => {
194 testState.snapshot = { ...linkedState, linked: false, managedTunnelActive: false };
195 testState.renderedSnapshot = testState.snapshot;
196 await mountController();
197 testState.getToken.mockImplementationOnce(async () => {
198 testState.snapshot = linkedState;
199 return "clerk-token";
200 });
201
202 expect(await reconcile({ managedTunnel: true, publish: true })).toBe(true);
203 expect(testState.link).not.toHaveBeenCalled();
204 expect(testState.unlink).not.toHaveBeenCalled();
205 expect(testState.preferences).toHaveBeenCalledExactlyOnceWith({
206 target,
207 publishAgentActivity: true,
208 });
209 });
210
211 it("shows a link the relay holds as publish-only as off and relinks it as managed", async () => {
212 testState.relayProviderKind = "manual";
213 await mountController();
214
215 expect(controller?.managedTunnelActive).toBe(false);
216 expect(controller?.managedTunnelOutOfSync).toBe(true);
217 expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
218 expect(testState.link).toHaveBeenCalledExactlyOnceWith({
219 target,
220 clerkToken: "clerk-token",
221 mode: "managed",
222 });
223 });
224
225 it("shows a revoked link as off and links it again when turned on", async () => {
226 testState.snapshot = { ...linkedState, linkRevoked: true };
227 testState.renderedSnapshot = testState.snapshot;
228 await mountController();
229 expect(controller).toMatchObject({
230 linked: false,
231 linkRevoked: true,
232 managedTunnelActive: false,
233 managedTunnelOutOfSync: false,
234 });
235
236 expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
237 expect(testState.link).toHaveBeenCalledExactlyOnceWith({
238 target,
239 clerkToken: "clerk-token",
240 mode: "managed",
241 });
242 });
243
244 it("keeps a managed link that the relay also holds as managed", async () => {
245 testState.relayProviderKind = "cloudflare_tunnel";
246 await mountController();
247
248 expect(controller?.managedTunnelActive).toBe(true);
249 expect(controller?.managedTunnelOutOfSync).toBe(false);
250 expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
251 expect(testState.link).not.toHaveBeenCalled();
252 });
253
254 it("stops the preference write if management permission is lost after linking", async () => {
255 await mountController();
256 testState.link.mockImplementationOnce(async () => {
257 testState.scopes.delete(AuthRelayWriteScope);
258 return AsyncResult.success({});
259 });
260
261 expect(await reconcile({ managedTunnel: false, publish: true })).toBe(false);
262 expect(testState.link).toHaveBeenCalledOnce();
263 expect(testState.preferences).not.toHaveBeenCalled();
264 expect(testState.refreshLink).toHaveBeenCalledOnce();
265 });
266
267 it("still lets an authorized user unlink when the cloud token is unavailable", async () => {
268 await mountController();
269 testState.getToken.mockRejectedValueOnce(new Error("Cloud sign-in unavailable"));
270
271 expect(await reconcile({ managedTunnel: false, publish: false })).toBe(true);
272 expect(testState.unlink).toHaveBeenCalledExactlyOnceWith({ target, clerkToken: null });
273 expect(testState.link).not.toHaveBeenCalled();
274 expect(testState.preferences).not.toHaveBeenCalled();
275 });
276
277 it("unlinks if the link-state read fails while requesting the cloud token", async () => {
278 await mountController();
279 testState.getToken.mockImplementationOnce(async () => {
280 testState.snapshot = null;
281 throw new Error("Cloud sign-in unavailable");
282 });
283
284 expect(await reconcile({ managedTunnel: false, publish: false })).toBe(true);
285 expect(testState.unlink).toHaveBeenCalledExactlyOnceWith({ target, clerkToken: null });
286 expect(testState.link).not.toHaveBeenCalled();
287 expect(testState.preferences).not.toHaveBeenCalled();
288 expect(testState.refreshLink).toHaveBeenCalledOnce();
289 });
290});