From e937863bc6a1acc468f4f9065a78c7f289001ec2 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 7 Jun 2026 22:26:54 +0200 Subject: [PATCH] test(server): cover HTTP routes, auth, admin gate, and CORS preflight --- tests/server_test.ts | 287 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 tests/server_test.ts diff --git a/tests/server_test.ts b/tests/server_test.ts new file mode 100644 index 0000000..82a1c7f --- /dev/null +++ b/tests/server_test.ts @@ -0,0 +1,287 @@ +import { assertEquals } from "@std/assert"; +import { createApp } from "../server.ts"; +import { addWebhookSubscription, listWebhooksByDid } from "../src/data/db.ts"; +import { seedNote, withTestDb } from "./_helpers/db.ts"; +import { requestApp } from "./_helpers/app.ts"; +import { stubAuthenticate } from "./_helpers/auth.ts"; +import { installFetchStub, jsonResponse } from "./_helpers/fetch_stub.ts"; + +const withAdminEnv = ( + dids: string, + fn: () => Promise | void, +): () => Promise => { + return async () => { + const previous = Deno.env.get("ADMIN_DIDS"); + Deno.env.set("ADMIN_DIDS", dids); + try { + await fn(); + } finally { + if (previous === undefined) Deno.env.delete("ADMIN_DIDS"); + else Deno.env.set("ADMIN_DIDS", previous); + } + }; +}; + +const withOpenSearchEnv = (fn: () => Promise | void) => { + return async () => { + const previous = Deno.env.get("OPENSEARCH_URL"); + Deno.env.set("OPENSEARCH_URL", "http://opensearch.test"); + try { + await fn(); + } finally { + if (previous === undefined) Deno.env.delete("OPENSEARCH_URL"); + else Deno.env.set("OPENSEARCH_URL", previous); + } + }; +}; + +Deno.test( + "GET /health returns 200 with status ok", + withTestDb(async () => { + const res = await requestApp(createApp(), { path: "/health" }); + assertEquals(res.status, 200); + assertEquals(res.json(), { status: "ok" }); + }), +); + +Deno.test( + "GET /notes paginates rkey desc and honors cursor + limit", + withTestDb(async () => { + for (const rkey of ["a", "b", "c", "d", "e"]) { + seedNote({ rkey }); + } + const app = createApp(); + + const first = await requestApp(app, { path: "/notes?limit=3" }); + assertEquals(first.status, 200); + const firstBody = first.json() as { + notes: { rkey: string }[]; + cursor?: string; + }; + assertEquals(firstBody.notes.map((n) => n.rkey), ["e", "d", "c"]); + assertEquals(firstBody.cursor, "c"); + + const second = await requestApp(app, { + path: `/notes?limit=3&cursor=${firstBody.cursor}`, + }); + const secondBody = second.json() as { notes: { rkey: string }[] }; + assertEquals(secondBody.notes.map((n) => n.rkey), ["b", "a"]); + }), +); + +Deno.test( + "POST /notes/feed rejects empty dids and accepts a valid list", + withTestDb(async () => { + seedNote({ did: "did:plc:alice", rkey: "rk1" }); + const app = createApp(); + + const empty = await requestApp(app, { + method: "POST", + path: "/notes/feed", + body: { dids: [] }, + }); + assertEquals(empty.status, 400); + + const notArray = await requestApp(app, { + method: "POST", + path: "/notes/feed", + body: { dids: "did:plc:alice" }, + }); + assertEquals(notArray.status, 400); + + const ok = await requestApp(app, { + method: "POST", + path: "/notes/feed", + body: { dids: ["did:plc:alice"] }, + }); + assertEquals(ok.status, 200); + const body = ok.json() as { notes: { rkey: string }[] }; + assertEquals(body.notes.length, 1); + }), +); + +Deno.test( + "POST /:did/webhooks: 401 without auth, 403 on DID mismatch, 201 inserts create+delete by default", + withTestDb(async () => { + const app = createApp(); + + const noAuth = await requestApp(app, { + method: "POST", + path: "/did:plc:alice/webhooks", + body: { method: "POST", url: "https://hook.example/x" }, + }); + assertEquals(noAuth.status, 401); + + const mismatch = stubAuthenticate("did:plc:bob"); + try { + const res = await requestApp(app, { + method: "POST", + path: "/did:plc:alice/webhooks", + headers: { Authorization: "Bearer fake" }, + body: { method: "POST", url: "https://hook.example/x" }, + }); + assertEquals(res.status, 403); + } finally { + mismatch.restore(); + } + + const owned = stubAuthenticate("did:plc:alice"); + try { + const res = await requestApp(app, { + method: "POST", + path: "/did:plc:alice/webhooks", + headers: { Authorization: "Bearer fake" }, + body: { method: "POST", url: "https://hook.example/x" }, + }); + assertEquals(res.status, 201); + const created = res.json() as { verb: string }[]; + const verbs = created.map((w) => w.verb).sort(); + assertEquals(verbs, ["create", "delete"]); + } finally { + owned.restore(); + } + }), +); + +Deno.test( + "POST /:did/webhooks rejects unknown verb with 400", + withTestDb(async () => { + const auth = stubAuthenticate("did:plc:alice"); + try { + const res = await requestApp(createApp(), { + method: "POST", + path: "/did:plc:alice/webhooks", + headers: { Authorization: "Bearer fake" }, + body: { + method: "POST", + url: "https://hook.example/x", + verb: "destroy", + }, + }); + assertEquals(res.status, 400); + } finally { + auth.restore(); + } + }), +); + +Deno.test( + "DELETE /:did/webhooks/:id: 400 on non-positive, 404 on unknown, 204 on success", + withTestDb(async () => { + const created = addWebhookSubscription({ + did: "did:plc:alice", + method: "POST", + url: "https://hook.example/x", + verb: "create", + }); + const auth = stubAuthenticate("did:plc:alice"); + const app = createApp(); + try { + const bad = await requestApp(app, { + method: "DELETE", + path: "/did:plc:alice/webhooks/0", + headers: { Authorization: "Bearer fake" }, + }); + assertEquals(bad.status, 400); + + const missing = await requestApp(app, { + method: "DELETE", + path: "/did:plc:alice/webhooks/9999", + headers: { Authorization: "Bearer fake" }, + }); + assertEquals(missing.status, 404); + + const ok = await requestApp(app, { + method: "DELETE", + path: `/did:plc:alice/webhooks/${created.id}`, + headers: { Authorization: "Bearer fake" }, + }); + assertEquals(ok.status, 204); + assertEquals(listWebhooksByDid("did:plc:alice").length, 0); + } finally { + auth.restore(); + } + }), +); + +Deno.test( + "GET /admin/webhooks: 403 for non-admin, 200 when ADMIN_DIDS contains caller", + withAdminEnv( + "did:plc:admin", + withTestDb(async () => { + const app = createApp(); + + const nonAdmin = stubAuthenticate("did:plc:other"); + try { + const res = await requestApp(app, { + path: "/admin/webhooks", + headers: { Authorization: "Bearer fake" }, + }); + assertEquals(res.status, 403); + } finally { + nonAdmin.restore(); + } + + const admin = stubAuthenticate("did:plc:admin"); + try { + const res = await requestApp(app, { + path: "/admin/webhooks", + headers: { Authorization: "Bearer fake" }, + }); + assertEquals(res.status, 200); + assertEquals(Array.isArray(res.json()), true); + } finally { + admin.restore(); + } + }), + ), +); + +Deno.test( + "GET /search: 400 on blank q, forwards discoverableOnly:true to opensearch", + withOpenSearchEnv( + withTestDb(async () => { + const app = createApp(); + + const blank = await requestApp(app, { path: "/search?q=" }); + assertEquals(blank.status, 400); + + const stub = installFetchStub(() => jsonResponse({ hits: { hits: [] } })); + try { + const res = await requestApp(app, { path: "/search?q=hello" }); + assertEquals(res.status, 200); + assertEquals(stub.calls.length, 1); + const sent = JSON.parse(stub.calls[0].body) as { + query: { bool: { filter: Record[] } }; + }; + assertEquals(sent.query.bool.filter, [ + { term: { discoverable: true } }, + ]); + } finally { + stub.restore(); + } + }), + ), +); + +Deno.test( + "OPTIONS preflight returns 204 with CORS headers", + withTestDb(async () => { + const res = await requestApp(createApp(), { + method: "OPTIONS", + path: "/notes", + }); + assertEquals(res.status, 204); + assertEquals(res.headers.get("Access-Control-Allow-Origin"), "*"); + assertEquals( + res.headers.get("Access-Control-Allow-Methods")?.includes("POST"), + true, + ); + assertEquals( + res.headers.get("Access-Control-Allow-Headers")?.includes( + "Authorization", + ), + true, + ); + }), +);