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