kanban-dev-webapp/src/lib/git/signature.test.ts

66 lines
No EOL
2.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { verifyWebhookSignature, signatureHeader } from "@/lib/git/signature";
// Computed independently with:
// crypto.createHmac('sha256', SECRET).update(BODY, 'utf8').digest('hex')
const SECRET = "whsec_test_2a6f9c";
const BODY = '{"ref":"refs/heads/main","commits":[{"id":"abc123"}]}';
const VALID = "9a27e804fac4e850127587433949087f44f3d30cc9b92a0bb956dc5a794e073a";
describe("verifyWebhookSignature", () => {
it("accepts a correct signature", () => {
expect(verifyWebhookSignature(BODY, SECRET, VALID)).toBe(true);
});
it("rejects a signature for a different body", () => {
expect(verifyWebhookSignature(`${BODY} `, SECRET, VALID)).toBe(false);
});
it("rejects a signature made with a different secret", () => {
expect(verifyWebhookSignature(BODY, "wrong-secret", VALID)).toBe(false);
});
it("rejects a missing or empty signature", () => {
expect(verifyWebhookSignature(BODY, SECRET, null)).toBe(false);
expect(verifyWebhookSignature(BODY, SECRET, "")).toBe(false);
});
it("rejects a signature of the wrong length without throwing", () => {
// Node's timingSafeEqual throws on length mismatch. An uncaught throw here
// becomes a 500, which tells an attacker their guess had the wrong shape.
expect(() => verifyWebhookSignature(BODY, SECRET, "abc")).not.toThrow();
expect(verifyWebhookSignature(BODY, SECRET, "abc")).toBe(false);
});
it("rejects non-hex characters without throwing", () => {
const sameLength = "z".repeat(VALID.length);
expect(() => verifyWebhookSignature(BODY, SECRET, sameLength)).not.toThrow();
expect(verifyWebhookSignature(BODY, SECRET, sameLength)).toBe(false);
});
it("refuses to verify when no secret is configured", () => {
// A project without a secret must fail closed. Treating "no secret" as
// "no check required" turns every unconfigured project into an open endpoint.
expect(verifyWebhookSignature(BODY, "", VALID)).toBe(false);
});
it("accepts an upper-case hex signature", () => {
expect(verifyWebhookSignature(BODY, SECRET, VALID.toUpperCase())).toBe(true);
});
});
describe("signatureHeader", () => {
it("reads the Forgejo header", () => {
const headers = new Headers({ "x-forgejo-signature": VALID });
expect(signatureHeader(headers)).toBe(VALID);
});
it("falls back to the Gitea header", () => {
const headers = new Headers({ "x-gitea-signature": VALID });
expect(signatureHeader(headers)).toBe(VALID);
});
it("returns null when neither is present", () => {
expect(signatureHeader(new Headers())).toBeNull();
});
});