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<Record<string, unknown>>();
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<unknown> = 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();
});
});