1// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no createHmac.
2import * as NodeCrypto from "node:crypto";
3
4import { assert, describe, it } from "@effect/vitest";
5
6import { verifyWebhookSignature } from "./webhookVerification.ts";
7
8const body = new TextEncoder().encode('{"action":"opened"}');
9const secret = "shared-secret";
10const hmac = () => NodeCrypto.createHmac("sha256", secret).update(body);
11
12describe("verifyWebhookSignature", () => {
13 const github = { header: "x-hub-signature-256", encoding: "hex", prefix: "sha256=" } as const;
14
15 it("accepts a GitHub-style hex signature with prefix", () => {
16 const headers = { "x-hub-signature-256": `sha256=${hmac().digest("hex")}` };
17 assert.isTrue(verifyWebhookSignature({ signature: github, secret, headers, body }));
18 });
19
20 it("rejects a tampered body, a wrong secret, a missing header and a missing prefix", () => {
21 const valid = `sha256=${hmac().digest("hex")}`;
22 const tampered = new TextEncoder().encode('{"action":"closed"}');
23 assert.isFalse(
24 verifyWebhookSignature({
25 signature: github,
26 secret,
27 headers: { "x-hub-signature-256": valid },
28 body: tampered,
29 }),
30 );
31 assert.isFalse(
32 verifyWebhookSignature({
33 signature: github,
34 secret: "other",
35 headers: { "x-hub-signature-256": valid },
36 body,
37 }),
38 );
39 assert.isFalse(verifyWebhookSignature({ signature: github, secret, headers: {}, body }));
40 assert.isFalse(
41 verifyWebhookSignature({
42 signature: github,
43 secret,
44 headers: { "x-hub-signature-256": hmac().digest("hex") },
45 body,
46 }),
47 );
48 });
49
50 it("accepts a base64 signature without prefix", () => {
51 assert.isTrue(
52 verifyWebhookSignature({
53 signature: { header: "X-Signature", encoding: "base64", prefix: "" },
54 secret,
55 headers: { "x-signature": hmac().digest("base64") },
56 body,
57 }),
58 );
59 });
60});