1import {
2 AuthOrchestrationOperateScope,
3 EnvironmentId,
4 type AuthSessionState,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import { AsyncResult, Atom } from "effect/reactivity";
8import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
9
10const harness = vi.hoisted(() => ({ lastAtom: null as Atom.Atom<unknown> | null }));
11vi.mock("react", () => ({ useMemo: <A>(factory: () => A) => factory() }));
12vi.mock("@effect/atom-react", async () => {
13 const { appAtomRegistry } = await import("./atom-registry");
14 return {
15 useAtomValue: <A>(atom: Atom.Atom<A>) => {
16 harness.lastAtom = atom;
17 return appAtomRegistry.get(atom);
18 },
19 };
20});
21vi.mock("../connection/hub", () => ({
22 hubConnection: {
23 organizationPermissionAtom: Atom.family((_id: EnvironmentId | null) =>
24 Atom.make(false).pipe(Atom.keepAlive),
25 ),
26 },
27}));
28vi.mock("../connection/runtime", () => ({ connectionAtomRuntime: {} }));
29vi.mock("@t3tools/client-runtime/state/session", () => ({
30 createEnvironmentSessionAtoms: () => ({
31 sessionStateAtom: Atom.family((_id: EnvironmentId) =>
32 Atom.make(AsyncResult.initial<AuthSessionState, Error>()).pipe(Atom.keepAlive),
33 ),
34 }),
35}));
36
37import { appAtomRegistry } from "./atom-registry";
38import {
39 environmentSession,
40 readEnvironmentScope,
41 useEnvironmentScope,
42 useEnvironmentsWithScope,
43} from "./session";
44
45const primary = EnvironmentId.make("primary");
46const secondary = EnvironmentId.make("secondary");
47const session = (canOperate: boolean): AuthSessionState => ({
48 authenticated: true,
49 scopes: canOperate ? [AuthOrchestrationOperateScope] : [],
50 auth: {
51 policy: "remote-reachable",
52 bootstrapMethods: [],
53 sessionMethods: [],
54 sessionCookieName: "session",
55 },
56});
57// The production atom is read-only; the fake session source is writable.
58const source = (id: EnvironmentId) =>
59 environmentSession.sessionStateAtom(id) as unknown as Atom.Writable<
60 AsyncResult.AsyncResult<AuthSessionState, Error>
61 >;
62const releases: Array<() => void> = [];
63beforeEach(() => {
64 harness.lastAtom = null;
65 appAtomRegistry.set(source(primary), AsyncResult.success(session(true)));
66 appAtomRegistry.set(source(secondary), AsyncResult.initial());
67});
68afterEach(() => {
69 releases.splice(0).forEach((release) => release());
70});
71
72it("keeps pending and denied environments out of worker grants while retaining valid cached access", () => {
73 expect(useEnvironmentScope(secondary, AuthOrchestrationOperateScope)).toBe(false);
74 expect(readEnvironmentScope(secondary, AuthOrchestrationOperateScope)).toBe(false);
75 const environments = [{ environmentId: primary }, { environmentId: secondary }];
76 expect(useEnvironmentsWithScope(environments, AuthOrchestrationOperateScope)).toEqual(
77 new Set([primary]),
78 );
79 appAtomRegistry.set(source(secondary), AsyncResult.waiting(AsyncResult.success(session(true))));
80 expect(readEnvironmentScope(secondary, AuthOrchestrationOperateScope)).toBe(true);
81 expect(useEnvironmentsWithScope(environments, AuthOrchestrationOperateScope)).toEqual(
82 new Set([primary, secondary]),
83 );
84 appAtomRegistry.set(source(secondary), AsyncResult.success(session(false)));
85 expect(useEnvironmentScope(secondary, AuthOrchestrationOperateScope)).toBe(false);
86});
87
88it("reacts to grant revocation, failure, and regrant without a connection list change", () => {
89 const environments = [{ environmentId: primary }, { environmentId: secondary }];
90 useEnvironmentsWithScope(environments, AuthOrchestrationOperateScope);
91 const observed = harness.lastAtom;
92 if (!observed) throw new Error("Missing worker grant atom");
93 const changes: unknown[] = [];
94 releases.push(appAtomRegistry.subscribe(observed, (value) => changes.push(value)));
95 appAtomRegistry.set(source(secondary), AsyncResult.success(session(true)));
96 expect(appAtomRegistry.get(observed)).toEqual(new Set([primary, secondary]));
97 appAtomRegistry.set(source(secondary), AsyncResult.success(session(false)));
98 expect(appAtomRegistry.get(observed)).toEqual(new Set([primary]));
99 appAtomRegistry.set(
100 source(primary),
101 AsyncResult.failure(Cause.fail(new Error("Session lookup failed"))),
102 );
103 expect(appAtomRegistry.get(observed)).toEqual(new Set());
104 appAtomRegistry.set(source(secondary), AsyncResult.success(session(true)));
105 expect(appAtomRegistry.get(observed)).toEqual(new Set([secondary]));
106 expect(changes).not.toHaveLength(0);
107});
108
109it("uses exact new-server permissions and keeps old-server grants usable", () => {
110 // A legacy representation must never override an explicitly narrowed grant.
111 appAtomRegistry.set(source(primary), AsyncResult.success({ ...session(true), permissions: [] }));
112 expect(useEnvironmentScope(primary, AuthOrchestrationOperateScope)).toBe(false);
113 expect(readEnvironmentScope(primary, AuthOrchestrationOperateScope)).toBe(false);
114 appAtomRegistry.set(
115 source(primary),
116 AsyncResult.success({ ...session(false), permissions: [AuthOrchestrationOperateScope] }),
117 );
118 expect(useEnvironmentScope(primary, AuthOrchestrationOperateScope)).toBe(true);
119 expect(readEnvironmentScope(primary, AuthOrchestrationOperateScope)).toBe(true);
120 appAtomRegistry.set(source(primary), AsyncResult.success(session(true)));
121 expect(useEnvironmentScope(primary, AuthOrchestrationOperateScope)).toBe(true);
122});