| 1 | import { |
| 2 | AuthRelayReadScope, |
| 3 | AuthRelayWriteScope, |
| 4 | EnvironmentId, |
| 5 | type EnvironmentCloudLinkStateResult, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import type { RelayManagedEndpointProviderKind } from "@t3tools/contracts/relay"; |
| 8 | import { AsyncResult } from "effect/reactivity"; |
| 9 | import { act, useLayoutEffect } from "react"; |
| 10 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 11 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 12 | |
| 13 | const 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 | |
| 27 | vi.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 | })); |
| 41 | vi.mock("@clerk/react", () => ({ |
| 42 | useAuth: () => ({ getToken: testState.getToken, isSignedIn: true }), |
| 43 | })); |
| 44 | vi.mock("../components/ui/toast", () => ({ toastManager: { add: testState.toast } })); |
| 45 | vi.mock("../state/relay", () => ({ |
| 46 | relayEnvironmentDiscovery: { refresh: testState.refreshDiscovery, stateValueAtom: {} }, |
| 47 | })); |
| 48 | vi.mock("../state/session", () => ({ |
| 49 | readEnvironmentScope: (_environmentId: string, scope: string) => testState.scopes.has(scope), |
| 50 | })); |
| 51 | vi.mock("../state/use-atom-command", () => ({ useAtomCommand: (command: unknown) => command })); |
| 52 | vi.mock("./linkEnvironmentAtoms", () => ({ |
| 53 | linkPrimaryEnvironment: testState.link, |
| 54 | unlinkPrimaryEnvironment: testState.unlink, |
| 55 | updatePrimaryEnvironmentPreferences: testState.preferences, |
| 56 | })); |
| 57 | vi.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 | })); |
| 67 | vi.mock("./publicConfig", () => ({ resolveRelayClerkTokenOptions: () => ({}) })); |
| 68 | |
| 69 | import { useCloudLinkController, type CloudLinkDesiredState } from "./useCloudLinkController"; |
| 70 | |
| 71 | const target = { |
| 72 | environmentId: EnvironmentId.make("primary"), |
| 73 | label: "Primary", |
| 74 | httpBaseUrl: "http://localhost:3773", |
| 75 | wsBaseUrl: "ws://localhost:3773/ws", |
| 76 | }; |
| 77 | const 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 | |
| 86 | let renderer: ReactTestRenderer | null = null; |
| 87 | let controller: ReturnType<typeof useCloudLinkController> | null = null; |
| 88 | |
| 89 | function ControllerProbe() { |
| 90 | const value = useCloudLinkController(); |
| 91 | useLayoutEffect(() => { |
| 92 | controller = value; |
| 93 | }); |
| 94 | return null; |
| 95 | } |
| 96 | |
| 97 | async function mountController() { |
| 98 | await act(() => { |
| 99 | renderer = create(<ControllerProbe />); |
| 100 | }); |
| 101 | } |
| 102 | |
| 103 | async 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 | |
| 112 | function expectNoMutations() { |
| 113 | expect(testState.link).not.toHaveBeenCalled(); |
| 114 | expect(testState.unlink).not.toHaveBeenCalled(); |
| 115 | expect(testState.preferences).not.toHaveBeenCalled(); |
| 116 | } |
| 117 | |
| 118 | beforeEach(() => { |
| 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 | |
| 138 | afterEach(async () => { |
| 139 | await act(() => renderer?.unmount()); |
| 140 | renderer = null; |
| 141 | controller = null; |
| 142 | vi.restoreAllMocks(); |
| 143 | vi.unstubAllGlobals(); |
| 144 | }); |
| 145 | |
| 146 | describe("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 | }); |