import { describe, it, expect } from "bun:test"; import { createHmac } from "crypto"; import { verifySignature } from "../server/lib/providers/github/verify"; import { parseEvent } from "../server/lib/providers/github/parse"; import { matchRoute } from "../server/lib/events/match"; import type { Route, WebhookEvent } from "../server/lib/types"; function sign(body: string, secret: string): string { return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`; } describe("verifySignature", () => { const secret = "test-secret"; it("returns true for valid signature", async () => { const body = '{"hello":"world"}'; expect(await verifySignature(body, sign(body, secret), secret)).toBe(true); }); it("returns false for invalid signature", async () => { expect(await verifySignature("body", "sha256=invalid", secret)).toBe(false); }); it("returns false for missing signature", async () => { expect(await verifySignature("body", undefined, secret)).toBe(false); }); }); describe("parseEvent", () => { it("parses valid push event", () => { const headers = { "x-github-event": "push", "x-hub-signature-256": "sha256=abc" }; const body = JSON.stringify({ ref: "refs/heads/main", commits: [] }); const event = parseEvent(headers, body); expect(event).not.toBeNull(); expect(event!.event).toBe("push"); expect(event!.payload.ref).toBe("refs/heads/main"); }); it("returns null for missing event header", () => { expect(parseEvent({}, "{}")).toBeNull(); }); it("returns null for invalid JSON", () => { expect(parseEvent({ "x-github-event": "push" }, "not json")).toBeNull(); }); }); describe("matchRoute", () => { const baseRoute: Route = { id: "test", name: "Test", enabled: true, filters: [], targets: [{ channelId: "123" }], }; it("matches all events when no filters", () => { const event: WebhookEvent = { event: "push", payload: {} }; expect(matchRoute(baseRoute, event)).toBe(true); }); it("rejects disabled routes", () => { const route = { ...baseRoute, enabled: false }; const event: WebhookEvent = { event: "push", payload: {} }; expect(matchRoute(route, event)).toBe(false); }); it("matches event filter", () => { const route = { ...baseRoute, filters: [{ type: "event" as const, match: "push" }] }; expect(matchRoute(route, { event: "push", payload: {} })).toBe(true); expect(matchRoute(route, { event: "issues", payload: {} })).toBe(false); }); it("matches event exclude filter", () => { const route = { ...baseRoute, filters: [{ type: "event" as const, match: "push", exclude: true }], }; expect(matchRoute(route, { event: "push", payload: {} })).toBe(false); expect(matchRoute(route, { event: "issues", payload: {} })).toBe(true); }); it("matches repo filter", () => { const route = { ...baseRoute, filters: [{ type: "repo" as const, match: "owner/repo" }] }; const event: WebhookEvent = { event: "push", payload: { repository: { full_name: "owner/repo" } }, }; expect(matchRoute(route, event)).toBe(true); }); it("matches actor filter", () => { const route = { ...baseRoute, filters: [{ type: "actor" as const, match: "octocat" }] }; const event: WebhookEvent = { event: "push", payload: { sender: { login: "octocat" } } }; expect(matchRoute(route, event)).toBe(true); }); it("matches action filter", () => { const route = { ...baseRoute, filters: [{ type: "action" as const, match: "opened" }] }; const event: WebhookEvent = { event: "issues", payload: { action: "opened" } }; expect(matchRoute(route, event)).toBe(true); }); it("matches branch filter for push events", () => { const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "main" }] }; const event: WebhookEvent = { event: "push", payload: { ref: "refs/heads/main" } }; expect(matchRoute(route, event)).toBe(true); expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/dev" } })).toBe(false); }); it("matches branch filter for pull_request events", () => { const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-x" }] }; const event: WebhookEvent = { event: "pull_request", payload: { pull_request: { head: { ref: "feature-x" } } }, }; expect(matchRoute(route, event)).toBe(true); expect( matchRoute(route, { event: "pull_request", payload: { pull_request: { head: { ref: "other" } } }, }), ).toBe(false); }); it("matches keyword filter with regex (//-wrapped)", () => { const route = { ...baseRoute, filters: [{ type: "keyword" as const, match: "/fix(es|ed)\\s+bug/" }], }; const event: WebhookEvent = { event: "push", payload: { commits: [{ message: "fixes bug #123" }] }, }; expect(matchRoute(route, event)).toBe(true); expect( matchRoute(route, { event: "push", payload: { commits: [{ message: "adds feature" }] } }), ).toBe(false); }); it("matches keyword filter with plain text fallback", () => { const route = { ...baseRoute, filters: [{ type: "keyword" as const, match: "deploy" }] }; const event: WebhookEvent = { event: "push", payload: { commits: [{ message: "deploy to prod" }] }, }; expect(matchRoute(route, event)).toBe(true); }); it("matches keyword filter with glob search", () => { const route = { ...baseRoute, filters: [{ type: "keyword" as const, match: "*deploy*" }] }; const event: WebhookEvent = { event: "push", payload: { commits: [{ message: "auto deploy pipeline" }] }, }; expect(matchRoute(route, event)).toBe(true); expect( matchRoute(route, { event: "push", payload: { commits: [{ message: "build only" }] } }), ).toBe(false); }); it("matches event filter with glob", () => { const route = { ...baseRoute, filters: [{ type: "event" as const, match: "pull_*" }] }; expect(matchRoute(route, { event: "pull_request", payload: {} })).toBe(true); expect(matchRoute(route, { event: "pull_request_review", payload: {} })).toBe(true); expect(matchRoute(route, { event: "push", payload: {} })).toBe(false); }); it("matches repo filter with glob across slashes", () => { const route = { ...baseRoute, filters: [{ type: "repo" as const, match: "myorg/*" }] }; expect( matchRoute(route, { event: "push", payload: { repository: { full_name: "myorg/backend" } }, }), ).toBe(true); expect( matchRoute(route, { event: "push", payload: { repository: { full_name: "other/backend" } }, }), ).toBe(false); }); it("matches branch filter with single-char ? wildcard", () => { const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-?" }] }; expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe( true, ); expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-xy" } })).toBe( false, ); }); it("matches branch filter with //-wrapped regex", () => { const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "/^feat/" }] }; expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe( true, ); expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/main" } })).toBe(false); }); it("treats glob special chars literally when not wrapped", () => { const route = { ...baseRoute, filters: [{ type: "event" as const, match: "release-" }], }; expect(matchRoute(route, { event: "release-x", payload: {} })).toBe(false); expect(matchRoute(route, { event: "release-", payload: {} })).toBe(true); }); it("matches multiple filters (AND logic)", () => { const route = { ...baseRoute, filters: [ { type: "event" as const, match: "push" }, { type: "actor" as const, match: "octocat" }, ], }; const event: WebhookEvent = { event: "push", payload: { sender: { login: "octocat" } } }; expect(matchRoute(route, event)).toBe(true); expect(matchRoute(route, { event: "push", payload: { sender: { login: "other" } } })).toBe( false, ); }); it("matches array of patterns", () => { const route = { ...baseRoute, filters: [{ type: "event" as const, match: ["push", "pull_request"] }], }; expect(matchRoute(route, { event: "push", payload: {} })).toBe(true); expect(matchRoute(route, { event: "pull_request", payload: {} })).toBe(true); expect(matchRoute(route, { event: "issues", payload: {} })).toBe(false); }); });