mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
226 lines
8.4 KiB
TypeScript
226 lines
8.4 KiB
TypeScript
import { describe, it, expect } from "bun:test";
|
|
import { createHmac } from "crypto";
|
|
import { detectProvider } from "../server/lib/providers";
|
|
import { customProvider } from "../server/lib/providers/custom";
|
|
import { verifyGiteaSignature } from "../server/lib/providers/gitea/verify";
|
|
import { parseGiteaEvent } from "../server/lib/providers/gitea/parse";
|
|
import { parseEvent } from "../server/lib/providers/github/parse";
|
|
import { formatEvent } from "../server/lib/formatters";
|
|
import type { Env, Route } from "../server/lib/types";
|
|
|
|
function giteaSign(body: string, secret: string): string {
|
|
return createHmac("sha256", secret).update(body).digest("hex");
|
|
}
|
|
|
|
describe("provider detection", () => {
|
|
it("detects github by X-GitHub-Event header", () => {
|
|
const p = detectProvider({ "x-github-event": "push" });
|
|
expect(p?.id).toBe("github");
|
|
});
|
|
|
|
it("detects gitea by X-Gitea-Event header", () => {
|
|
const p = detectProvider({ "x-gitea-event": "push" });
|
|
expect(p?.id).toBe("gitea");
|
|
});
|
|
|
|
it("prefers gitea when both gitea and github headers are present", () => {
|
|
// Gitea webhooks also send GitHub-compatible headers (X-GitHub-Event,
|
|
// X-Hub-Signature-256, X-Gogs-*), so detection must not misclassify them.
|
|
const p = detectProvider({
|
|
"x-gitea-event": "push",
|
|
"x-github-event": "push",
|
|
"x-gitea-signature": "abc",
|
|
"x-hub-signature-256": "sha256=abc",
|
|
});
|
|
expect(p?.id).toBe("gitea");
|
|
});
|
|
|
|
it("returns null for unknown providers", () => {
|
|
expect(detectProvider({})).toBeNull();
|
|
expect(detectProvider({ "x-gitlab-event": "Push Hook" })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("gitea signature", () => {
|
|
const secret = "gitea-secret";
|
|
|
|
it("accepts a valid hex HMAC-SHA256 signature (no sha256= prefix)", async () => {
|
|
const body = '{"ref":"refs/heads/main"}';
|
|
const sig = giteaSign(body, secret);
|
|
expect(await verifyGiteaSignature(body, sig, secret)).toBe(true);
|
|
});
|
|
|
|
it("rejects an invalid signature", async () => {
|
|
expect(await verifyGiteaSignature("body", "deadbeef", secret)).toBe(false);
|
|
});
|
|
|
|
it("rejects when signature or secret is missing", async () => {
|
|
expect(await verifyGiteaSignature("body", undefined, secret)).toBe(false);
|
|
expect(await verifyGiteaSignature("body", "abc", undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("gitea event parsing", () => {
|
|
it("parses a push event and sets the provider", () => {
|
|
const event = parseGiteaEvent(
|
|
{ "x-gitea-event": "push", "x-gitea-delivery": "d-1" },
|
|
JSON.stringify({
|
|
ref: "refs/heads/main",
|
|
compare_url: "https://git.example.com/org/repo/compare/abc...def",
|
|
pusher: { login: "octo", html_url: "https://git.example.com/octo" },
|
|
repository: { full_name: "org/repo", html_url: "https://git.example.com/org/repo" },
|
|
}),
|
|
);
|
|
expect(event).not.toBeNull();
|
|
expect(event!.provider).toBe("gitea");
|
|
expect(event!.event).toBe("push");
|
|
expect(event!.deliveryId).toBe("d-1");
|
|
expect(event!.payload.compare).toBe("https://git.example.com/org/repo/compare/abc...def");
|
|
expect((event!.payload.sender as { login?: string }).login).toBe("octo");
|
|
});
|
|
|
|
it("maps pull_request_comment to pull_request_review_comment and pulls the PR", () => {
|
|
const event = parseGiteaEvent(
|
|
{ "x-gitea-event": "pull_request_comment" },
|
|
JSON.stringify({
|
|
action: "created",
|
|
issue: {
|
|
number: 7,
|
|
title: "Add feature",
|
|
html_url: "https://git.example.com/org/repo/pulls/7",
|
|
},
|
|
comment: {
|
|
body: "looks good",
|
|
line: 12,
|
|
html_url: "https://git.example.com/org/repo/pulls/7#issuecomment-1",
|
|
},
|
|
repository: { full_name: "org/repo" },
|
|
sender: { login: "octo" },
|
|
}),
|
|
);
|
|
expect(event!.event).toBe("pull_request_review_comment");
|
|
const pr = event!.payload.pull_request as { number?: number; title?: string };
|
|
expect(pr.number).toBe(7);
|
|
const comment = event!.payload.comment as { position?: number };
|
|
expect(comment.position).toBe(12);
|
|
});
|
|
|
|
it("copies top-level commit_id onto the comment for commit_comment events", () => {
|
|
const event = parseGiteaEvent(
|
|
{ "x-gitea-event": "commit_comment" },
|
|
JSON.stringify({
|
|
action: "created",
|
|
commit_id: "abcd1234ef",
|
|
comment: {
|
|
body: "why?",
|
|
html_url: "https://git.example.com/org/repo/commit/abcd1234ef#commitcomment-1",
|
|
},
|
|
repository: { full_name: "org/repo" },
|
|
sender: { login: "octo" },
|
|
}),
|
|
);
|
|
expect(event!.event).toBe("commit_comment");
|
|
expect((event!.payload.comment as { commit_id?: string }).commit_id).toBe("abcd1234ef");
|
|
});
|
|
|
|
it("returns null for missing event header or invalid JSON", () => {
|
|
expect(parseGiteaEvent({}, "{}")).toBeNull();
|
|
expect(parseGiteaEvent({ "x-gitea-event": "push" }, "not json")).toBeNull();
|
|
});
|
|
|
|
it("formats a normalized gitea push with gitea commit links", () => {
|
|
const event = parseGiteaEvent(
|
|
{ "x-gitea-event": "push" },
|
|
JSON.stringify({
|
|
ref: "refs/heads/main",
|
|
compare_url: "https://git.example.com/org/repo/compare/abc...def",
|
|
pusher: { login: "octo", html_url: "https://git.example.com/octo" },
|
|
commits: [{ id: "abcd1234ef", message: "fix stuff", added: [], removed: [], modified: [] }],
|
|
repository: { full_name: "org/repo", html_url: "https://git.example.com/org/repo" },
|
|
}),
|
|
);
|
|
const route: Route = {
|
|
id: "test",
|
|
name: "Test",
|
|
enabled: true,
|
|
filters: [],
|
|
targets: [{ channelId: "111" }],
|
|
};
|
|
const msg = formatEvent(route, event!);
|
|
expect(msg.title).toContain("org/repo");
|
|
expect(msg.fields![0].value).toBe(
|
|
"[`abcd123`](https://git.example.com/org/repo/commit/abcd1234ef) fix stuff",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("custom provider", () => {
|
|
const secret = "tenant-secret";
|
|
const body = JSON.stringify({
|
|
title: "Deploy failed",
|
|
repo: "acme/widget",
|
|
color: "red",
|
|
description: "prod down",
|
|
});
|
|
|
|
function env(overrides: Record<string, unknown> = {}): Env {
|
|
return { GITHUB_WEBHOOK_SECRET: secret, ...overrides } as Env;
|
|
}
|
|
|
|
it("matches only requests with X-WebHooker-Signature and no forge headers", () => {
|
|
expect(customProvider.matches({ "x-webhooker-signature": "sha256=abc" })).toBe(true);
|
|
expect(customProvider.matches({})).toBe(false);
|
|
expect(customProvider.matches({ "x-github-event": "push" })).toBe(false);
|
|
expect(customProvider.matches({ "x-gitea-event": "push" })).toBe(false);
|
|
});
|
|
|
|
it("detectProvider finds the custom provider", () => {
|
|
expect(detectProvider({ "x-webhooker-signature": "sha256=abc" })?.id).toBe("custom");
|
|
});
|
|
|
|
it("accepts a valid sha256 HMAC signature", async () => {
|
|
const sig = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
expect(await customProvider.verify(body, { "x-webhooker-signature": sig }, env())).toBe(true);
|
|
});
|
|
|
|
it("rejects an invalid signature", async () => {
|
|
expect(
|
|
await customProvider.verify(body, { "x-webhooker-signature": "sha256=wrong" }, env()),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects when the secret is missing", async () => {
|
|
const sig = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
expect(
|
|
(await customProvider.verify(body, { "x-webhooker-signature": sig }, {})) as unknown as Env,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("parses the body into a custom event with optional deliveryId", () => {
|
|
const event = customProvider.parse(body, {});
|
|
expect(event).not.toBeNull();
|
|
expect(event!.event).toBe("custom");
|
|
expect(event!.provider).toBe("custom");
|
|
expect((event!.payload as { title: string }).title).toBe("Deploy failed");
|
|
|
|
const withId = customProvider.parse(JSON.stringify({ title: "x", deliveryId: "alert-1" }), {});
|
|
expect(withId!.deliveryId).toBe("alert-1");
|
|
});
|
|
|
|
it("returns null for invalid JSON", () => {
|
|
expect(customProvider.parse("not json", {})).toBeNull();
|
|
});
|
|
|
|
it("extracts the GitHub App installation id on github events", () => {
|
|
const event = parseEvent(
|
|
{ "x-github-event": "push", "x-github-delivery": "d1" },
|
|
JSON.stringify({ installation: { id: 42 }, repository: { full_name: "a/b" } }),
|
|
);
|
|
expect(event!.installationId).toBe(42);
|
|
const noInstall = parseEvent(
|
|
{ "x-github-event": "push" },
|
|
JSON.stringify({ repository: { full_name: "a/b" } }),
|
|
);
|
|
expect(noInstall!.installationId).toBeUndefined();
|
|
});
|
|
});
|