import type { Did } from "@atcute/lexicons/syntax"; import type { Session } from "@atcute/oauth-browser-client"; import { finalizeAuthorization, getSession } from "@atcute/oauth-browser-client"; import { flushSync } from "svelte"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createAuth, type Auth } from "#lib/auth.svelte.ts"; vi.mock("@atcute/oauth-browser-client", async (importOriginal) => { const mod = await importOriginal>(); class MockOAuthUserAgent { readonly sub: string; getSession = vi.fn(async () => this.session); signOut = vi.fn(async () => {}); constructor(readonly session: { info: { sub: string } }) { this.sub = session.info.sub; } } return { ...mod, OAuthUserAgent: MockOAuthUserAgent, configureOAuth: vi.fn(), finalizeAuthorization: vi.fn(), getSession: vi.fn(), listStoredSessions: vi.fn(() => []), deleteStoredSession: vi.fn() }; }); const mockedFinalize = vi.mocked(finalizeAuthorization); const mockedGetSession = vi.mocked(getSession); const alice = "did:plc:alice" as Did; const bob = "did:plc:bob" as Did; const liveSession = (did: Did): Session => ({ info: { sub: did } }) as unknown as Session; const response = { code: "c", state: "sid", iss: "https://pds.example" }; const watch = (read: () => unknown) => { let runs = 0; const stop = $effect.root(() => { $effect(() => { read(); runs++; }); }); flushSync(); return { get runs() { flushSync(); return runs; }, stop }; }; const settle = (auth: Auth) => vi.waitFor(() => expect(auth.state.kind).toBe("authenticated")); beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); vi.stubGlobal( "fetch", vi.fn(async (input: URL) => { const did = new URL(input).searchParams.get("identifier"); return Response.json({ did, handle: `${did?.slice(8)}.example` }); }) ); }); afterEach(() => { vi.unstubAllGlobals(); }); describe("auth readers", () => { it("don't rerun while the same account finishes signing in", async () => { mockedFinalize.mockResolvedValue({ session: liveSession(alice), state: null }); const auth = createAuth("http://bobbin.example", { did: alice, handle: "alice.example" }); const did = watch(() => auth.currentDid); const agent = watch(() => auth.agent); await auth.completePendingSignIn("#state=sid&code=c"); await settle(auth); expect(did.runs).toBe(1); // once with no agent yet, once when it arrives expect(agent.runs).toBe(2); did.stop(); agent.stop(); }); it("rerun when the account changes or signs out", async () => { mockedFinalize.mockResolvedValueOnce({ session: liveSession(alice), state: null }); const auth = createAuth("http://bobbin.example", null); await auth.completeAuthorization(alice, response); await settle(auth); const did = watch(() => auth.currentDid); mockedFinalize.mockResolvedValueOnce({ session: liveSession(bob), state: null }); await auth.completeAuthorization(bob, response); await settle(auth); expect(auth.currentDid).toBe(bob); expect(did.runs).toBe(2); await auth.signOut(); expect(auth.currentDid).toBeNull(); expect(did.runs).toBe(3); did.stop(); }); it("don't rerun an effect that asked agentFor for an agent", async () => { mockedGetSession.mockResolvedValue(liveSession(alice)); const auth = createAuth("http://bobbin.example", null); let pending: Promise = Promise.resolve(); const effect = watch(() => { pending = auth.agentFor(alice, { relogin: false }); }); await pending; mockedFinalize.mockResolvedValue({ session: liveSession(bob), state: null }); await auth.completeAuthorization(bob, response); await settle(auth); expect(effect.runs).toBe(1); effect.stop(); }); });