A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352import { describe, expect, mock, test } from "bun:test";import { HappyViewOAuthClient } from "../client";import { ApiError } from "../errors";import { MemoryStorage } from "../storage";
function createMockFetch(responses: Array<{ status: number; body: unknown }>) { let callIndex = 0; const calls: Array<{ url: string; init: RequestInit }> = [];
const fetchFn = mock(async (input: RequestInfo | URL, init?: RequestInit) => { const url = input instanceof URL ? input.toString() : String(input); calls.push({ url, init: init ?? {} }); const resp = responses[callIndex] ?? { status: 500, body: { error: "no more mocked responses" }, }; callIndex++; return new Response(JSON.stringify(resp.body), { status: resp.status }); });
return { fetchFn, calls };}
async function generateTestJwk(): Promise<JsonWebKey> { const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"], ); const jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey); // Remove key_ops so importJwk can re-import with its own usage constraints delete jwk.key_ops; return jwk;}
function createClient(overrides?: { fetchFn?: typeof globalThis.fetch; clientSecret?: string; storage?: MemoryStorage;}) { return new HappyViewOAuthClient({ instanceUrl: "https://happyview.example.com", clientKey: "hvc_testkey", clientSecret: overrides?.clientSecret, storage: overrides?.storage ?? new MemoryStorage(), fetch: overrides?.fetchFn, });}
describe("HappyViewOAuthClient", () => { describe("provisionDpopKey", () => { test("calls POST /oauth/dpop-keys with client credentials in headers", async () => { const testJwk = await generateTestJwk(); const { fetchFn, calls } = createMockFetch([ { status: 201, body: { provision_id: "hvp_abc123", dpop_key: testJwk, }, }, ]);
const client = createClient({ fetchFn, clientSecret: "hvs_secret" }); const result = await client.provisionDpopKey();
expect(calls[0].url).toBe( "https://happyview.example.com/oauth/dpop-keys", ); const headers = new Headers(calls[0].init.headers); expect(headers.get("x-client-key")).toBe("hvc_testkey"); expect(headers.get("x-client-secret")).toBe("hvs_secret"); expect(result.provisionId).toBe("hvp_abc123"); expect(result.dpopKey).toBeDefined(); expect(result.rawJwk).toBeDefined(); });
test("includes PKCE challenge for public clients and returns verifier", async () => { const testJwk = await generateTestJwk(); const { fetchFn, calls } = createMockFetch([ { status: 201, body: { provision_id: "hvp_public", dpop_key: testJwk, }, }, ]);
const client = createClient({ fetchFn }); const result = await client.provisionDpopKey();
const body = JSON.parse(calls[0].init.body as string); expect(body.pkce_challenge).toBeDefined(); expect(typeof body.pkce_challenge).toBe("string"); expect(result.pkceVerifier).toBeDefined(); expect(typeof result.pkceVerifier).toBe("string"); });
test("does not include PKCE for confidential clients", async () => { const testJwk = await generateTestJwk(); const { fetchFn, calls } = createMockFetch([ { status: 201, body: { provision_id: "hvp_conf", dpop_key: testJwk, }, }, ]);
const client = createClient({ fetchFn, clientSecret: "hvs_sec" }); const result = await client.provisionDpopKey();
const body = JSON.parse(calls[0].init.body as string); expect(body.pkce_challenge).toBeUndefined(); expect(result.pkceVerifier).toBeUndefined(); });
test("throws ApiError on non-201 response", async () => { const { fetchFn } = createMockFetch([ { status: 400, body: { message: "bad request" } }, ]);
const client = createClient({ fetchFn }); try { await client.provisionDpopKey(); expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(ApiError); expect((err as ApiError).status).toBe(400); expect((err as ApiError).body).toEqual({ message: "bad request" }); } }); });
describe("registerSession", () => { test("calls POST /oauth/sessions and returns a HappyViewSession", async () => { const testJwk = await generateTestJwk(); const { fetchFn, calls } = createMockFetch([ { status: 201, body: { session_id: "sess_123", did: "did:plc:testuser" }, }, ]);
const storage = new MemoryStorage(); const client = createClient({ fetchFn, clientSecret: "hvs_sec", storage, }); const session = await client.registerSession({ provisionId: "hvp_abc", did: "did:plc:testuser", accessToken: "at_token", scopes: "atproto", dpopKey: testJwk, });
expect(calls[0].url).toBe( "https://happyview.example.com/oauth/sessions", ); expect(session.did).toBe("did:plc:testuser"); });
test("persists session and last active DID to storage", async () => { const testJwk = await generateTestJwk(); const { fetchFn } = createMockFetch([ { status: 201, body: { session_id: "sess_123", did: "did:plc:testuser" }, }, ]);
const storage = new MemoryStorage(); const client = createClient({ fetchFn, clientSecret: "hvs_sec", storage, }); await client.registerSession({ provisionId: "hvp_abc", did: "did:plc:testuser", accessToken: "at_token", scopes: "atproto", dpopKey: testJwk, });
const stored = await storage.get("happyview:session:did:plc:testuser"); expect(stored).not.toBeNull(); const parsed = JSON.parse(stored!); expect(parsed.did).toBe("did:plc:testuser"); expect(parsed.accessToken).toBe("at_token");
const lastActive = await storage.get("happyview:last-active-did"); expect(lastActive).toBe("did:plc:testuser"); }); });
describe("deleteSession", () => { test("calls DELETE /oauth/sessions/:did", async () => { const testJwk = await generateTestJwk(); const { fetchFn, calls } = createMockFetch([ { status: 204, body: null }, ]);
const storage = new MemoryStorage(); await storage.set( "happyview:session:did:plc:testuser", JSON.stringify({ did: "did:plc:testuser", dpopKey: testJwk, accessToken: "at_token", clientKey: "hvc_testkey", instanceUrl: "https://happyview.example.com", }), ); await storage.set("happyview:last-active-did", "did:plc:testuser");
const client = createClient({ fetchFn, clientSecret: "hvs_sec", storage, }); await client.deleteSession("did:plc:testuser");
expect(calls[0].url).toBe( "https://happyview.example.com/oauth/sessions/did:plc:testuser", ); expect(calls[0].init.method).toBe("DELETE"); });
test("clears session and last active DID from storage", async () => { const testJwk = await generateTestJwk(); const { fetchFn } = createMockFetch([{ status: 204, body: null }]);
const storage = new MemoryStorage(); await storage.set( "happyview:session:did:plc:testuser", JSON.stringify({ did: "did:plc:testuser", dpopKey: testJwk, accessToken: "at_token", clientKey: "hvc_testkey", instanceUrl: "https://happyview.example.com", }), ); await storage.set("happyview:last-active-did", "did:plc:testuser");
const client = createClient({ fetchFn, clientSecret: "hvs_sec", storage, }); await client.deleteSession("did:plc:testuser");
expect( await storage.get("happyview:session:did:plc:testuser"), ).toBeNull(); expect(await storage.get("happyview:last-active-did")).toBeNull(); });
test("preserves last active DID when deleting a different session", async () => { const testJwk = await generateTestJwk(); const { fetchFn } = createMockFetch([{ status: 204, body: null }]);
const storage = new MemoryStorage(); await storage.set( "happyview:session:did:plc:other", JSON.stringify({ did: "did:plc:other", dpopKey: testJwk, accessToken: "at_token", clientKey: "hvc_testkey", instanceUrl: "https://happyview.example.com", }), ); await storage.set("happyview:last-active-did", "did:plc:testuser");
const client = createClient({ fetchFn, clientSecret: "hvs_sec", storage, }); await client.deleteSession("did:plc:other");
expect(await storage.get("happyview:session:did:plc:other")).toBeNull(); expect(await storage.get("happyview:last-active-did")).toBe( "did:plc:testuser", ); }); });
describe("restoreSession", () => { test("returns null when no session in storage", async () => { const client = createClient(); const session = await client.restoreSession("did:plc:nobody"); expect(session).toBeNull(); });
test("restores session from storage", async () => { const testJwk = await generateTestJwk(); const storage = new MemoryStorage(); await storage.set( "happyview:session:did:plc:testuser", JSON.stringify({ did: "did:plc:testuser", dpopKey: testJwk, accessToken: "at_stored", clientKey: "hvc_testkey", instanceUrl: "https://happyview.example.com", }), );
const client = createClient({ storage }); const session = await client.restoreSession("did:plc:testuser"); expect(session).not.toBeNull(); expect(session!.did).toBe("did:plc:testuser"); }); });
describe("restore", () => { test("returns null when no last active DID", async () => { const client = createClient(); const session = await client.restore(); expect(session).toBeNull(); });
test("restores last active session", async () => { const testJwk = await generateTestJwk(); const storage = new MemoryStorage(); await storage.set("happyview:last-active-did", "did:plc:testuser"); await storage.set( "happyview:session:did:plc:testuser", JSON.stringify({ did: "did:plc:testuser", dpopKey: testJwk, accessToken: "at_stored", clientKey: "hvc_testkey", instanceUrl: "https://happyview.example.com", }), );
const client = createClient({ storage }); const session = await client.restore(); expect(session).not.toBeNull(); expect(session!.did).toBe("did:plc:testuser"); }); });});