Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163import type { Did } from "@atcute/lexicons/syntax";import type { Session } from "@atcute/oauth-browser-client";import { LoginError, OAuthResponseError, OAuthUserAgent, TokenRefreshError, deleteStoredSession, finalizeAuthorization, getSession} from "@atcute/oauth-browser-client";import { type Mock, beforeEach, describe, expect, it, vi } from "vitest";import { createAuth } from "$lib/auth.svelte";
interface MockAgent { sub: string; getSession: Mock;}
vi.mock("@atcute/oauth-browser-client", async (importOriginal) => { const mod = await importOriginal<Record<string, unknown>>(); class MockOAuthUserAgent { static instances: 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; MockOAuthUserAgent.instances.push(this); } } return { ...mod, OAuthUserAgent: MockOAuthUserAgent, configureOAuth: vi.fn(), createAuthorizationUrl: vi.fn(), finalizeAuthorization: vi.fn(), getSession: vi.fn(), listStoredSessions: vi.fn(() => []), deleteStoredSession: vi.fn() };});
// the vi.mock above swaps in a class that records its instancesconst MockedUserAgent = OAuthUserAgent as unknown as { instances: MockAgent[] };
const mockedGetSession = vi.mocked(getSession);const mockedDeleteStoredSession = vi.mocked(deleteStoredSession);const mockedFinalize = vi.mocked(finalizeAuthorization);
const alice = "did:plc:alice" as Did;const liveSession = (did: Did): Session => ({ info: { sub: did } }) as unknown as Session;
beforeEach(() => { vi.clearAllMocks(); MockedUserAgent.instances.length = 0;});
describe("agentFor", () => { it("returns a live agent and reuses it", async () => { mockedGetSession.mockResolvedValue(liveSession(alice)); const auth = createAuth("http://127.0.0.1:1", null);
const agent = await auth.agentFor(alice); expect(agent.sub).toBe(alice); expect(await auth.agentFor(alice)).toBe(agent); expect(mockedGetSession).toHaveBeenCalledTimes(1); expect(mockedGetSession.mock.calls[0]).toEqual([alice]); expect(MockedUserAgent.instances[0].getSession).toHaveBeenCalledTimes(1); });
it("prunes a revoked session and throws a friendly error", async () => { mockedGetSession.mockRejectedValue(new TokenRefreshError(alice, "session was revoked")); const auth = createAuth("http://127.0.0.1:1", null);
await expect(auth.agentFor(alice)).rejects.toThrow(/session expired/); expect(mockedDeleteStoredSession).toHaveBeenCalledWith(alice); });
it("treats an invalid_token response from the token endpoint as dead", async () => { mockedGetSession.mockRejectedValue( new OAuthResponseError(new Response(null, { status: 400 }), { error: "invalid_token", error_description: '"exp" claim timestamp check failed' }) ); const auth = createAuth("http://127.0.0.1:1", null);
await expect(auth.agentFor(alice)).rejects.toThrow(/session expired/); expect(mockedDeleteStoredSession).toHaveBeenCalledWith(alice); });
it("keeps the session on transient failures", async () => { const cause = new TypeError("fetch failed"); mockedGetSession.mockRejectedValue(cause); const auth = createAuth("http://127.0.0.1:1", null);
await expect(auth.agentFor(alice)).rejects.toBe(cause); expect(mockedDeleteStoredSession).not.toHaveBeenCalled(); });
it("prunes a cached agent whose session died since it was minted", async () => { mockedGetSession.mockResolvedValue(liveSession(alice)); const auth = createAuth("http://127.0.0.1:1", null); await auth.agentFor(alice);
MockedUserAgent.instances[0].getSession.mockRejectedValue( new TokenRefreshError(alice, "session was revoked") ); await expect(auth.agentFor(alice)).rejects.toThrow(/session expired/); expect(mockedDeleteStoredSession).toHaveBeenCalledWith(alice); });});
describe("completePendingSignIn", () => { const seeded = { did: alice, handle: "alice.example" };
it("adopts the exchanged session without disturbing the seeded profile", async () => { mockedFinalize.mockResolvedValue({ session: liveSession(alice), state: null }); const auth = createAuth("http://127.0.0.1:1", seeded);
const completion = auth.completePendingSignIn("#state=sid&code=c&iss=https://pds.example"); // the topbar must not drop to the skeleton mid-exchange expect(auth.resolving).toBe(false); expect(auth.currentUser).toEqual({ did: alice, handle: "alice.example" }); await completion;
expect(auth.currentUser).toEqual({ did: alice, handle: "alice.example" }); expect(mockedFinalize).toHaveBeenCalledTimes(1); expect(mockedFinalize.mock.calls[0][0].get("code")).toBe("c"); });
it("drops the seeded account and rethrows when the exchange fails", async () => { mockedFinalize.mockRejectedValue(new LoginError("unknown state provided")); const auth = createAuth("http://127.0.0.1:1", seeded);
await expect(auth.completePendingSignIn("#state=sid&code=c")).rejects.toThrow( /unknown state/ ); expect(auth.state.kind).toBe("logged-out"); expect(auth.currentUser).toBeNull(); });
it("blocks agentFor until the pending exchange has stored the session", async () => { const gate = Promise.withResolvers<{ session: Session; state: null }>(); mockedFinalize.mockReturnValue(gate.promise); mockedGetSession.mockResolvedValue(liveSession(alice)); const auth = createAuth("http://127.0.0.1:1", seeded);
const completion = auth.completePendingSignIn("#state=sid&code=c"); const agentPromise = auth.agentFor(alice); // flush microtasks so agentFor reaches its park point on the exchange for (let i = 0; i < 5; i++) await Promise.resolve(); expect(mockedGetSession).not.toHaveBeenCalled();
gate.resolve({ session: liveSession(alice), state: null }); await completion; // the adopted session satisfies agentFor without a storage reload expect((await agentPromise).sub).toBe(alice); expect(mockedGetSession).not.toHaveBeenCalled(); });});