test(server): cover HTTP routes, auth, admin gate, and CORS preflight
Some checks failed
CI / check (push) Failing after 12s
Some checks failed
CI / check (push) Failing after 12s
This commit is contained in:
287
tests/server_test.ts
Normal file
287
tests/server_test.ts
Normal file
@@ -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> | void,
|
||||||
|
): () => Promise<void> => {
|
||||||
|
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> | 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<string, unknown>[] } };
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user