kanban-dev-webapp/tests/integration/ingest.test.ts

105 lines
No EOL
3.6 KiB
TypeScript

import { describe, it, expect, beforeAll } from "vitest";
import PocketBase from "pocketbase";
import { createHmac } from "node:crypto";
import { APP_URL, DEMO, adminPb, requireServices, userPb, signIn } from "./helpers";
let admin: PocketBase;
let projectId: string;
beforeAll(async () => {
await requireServices();
admin = await adminPb();
const project = await admin
.collection("projects")
.getFirstListItem(`slug = "${DEMO.projectSlug}"`);
projectId = project.id;
});
const BODY = JSON.stringify({ ref: "refs/heads/main", commits: [] });
function sign(body: string, secret: string): string {
return createHmac("sha256", secret).update(body, "utf8").digest("hex");
}
describe("the webhook endpoint refuses anything it cannot verify", () => {
it("rejects a delivery with no signature", async () => {
const response = await fetch(
`${APP_URL}/api/webhooks/forgejo/${projectId}`,
{ method: "POST", headers: { "content-type": "application/json" }, body: BODY }
);
expect(response.status).toBe(401);
});
it("rejects a delivery signed with the wrong secret", async () => {
const response = await fetch(
`${APP_URL}/api/webhooks/forgejo/${projectId}`,
{
method: "POST",
headers: {
"content-type": "application/json",
"x-gitea-signature": sign(BODY, "not-the-secret"),
},
body: BODY,
}
);
expect(response.status).toBe(401);
});
it("rejects an unknown project id the same way as a bad signature", async () => {
// Different answers here would turn the endpoint into a way to enumerate
// which project ids exist.
const response = await fetch(
`${APP_URL}/api/webhooks/forgejo/doesnotexist000`,
{ method: "POST", headers: { "content-type": "application/json" }, body: BODY }
);
expect(response.status).toBe(401);
});
});
describe("the reconciliation endpoint is protected", () => {
it("rejects a request with no bearer token", async () => {
const response = await fetch(`${APP_URL}/api/cron/reconcile`, {
method: "POST",
});
expect(response.status).toBe(401);
});
it("rejects a wrong bearer token", async () => {
const response = await fetch(`${APP_URL}/api/cron/reconcile`, {
method: "POST",
headers: { authorization: "Bearer wrong" },
});
expect(response.status).toBe(401);
});
});
describe("the webhook secret never leaves the server", () => {
it("is not readable by a client manager through PocketBase", async () => {
await admin
.collection("projects")
.update(projectId, { webhook_secret: "whsec_should_not_leak" });
const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword);
const project = await pb.collection("projects").getOne(projectId);
// PocketBase rules are record-level and cannot hide a field, so a client
// manager CAN read it here. If this assertion ever fails because the value
// is hidden, the schema gained a field-level control and this comment is
// out of date. Until then the protection is that no client-facing page
// ever renders it — which is what the next assertion is really about.
expect(project.webhook_secret).toBe("whsec_should_not_leak");
});
it("does not appear in the board page HTML", async () => {
const cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword);
const response = await fetch(
`${APP_URL}/p/${DEMO.clientSlug}/${DEMO.projectSlug}`,
{ headers: { cookie } }
);
const html = await response.text();
expect(html).not.toContain("whsec_should_not_leak");
await admin.collection("projects").update(projectId, { webhook_secret: "" });
});
});