import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { WebhookHandler } from "../src/webhooks/WebhookHandler"; import prisma from "@shieldai/db"; const TEST_SECRET = "test-webhook-secret-2026"; let runId = Date.now(); describe("WebhookHandler", () => { let handler: WebhookHandler; beforeEach(() => { handler = new WebhookHandler(TEST_SECRET); }); describe("signature verification", () => { it("verifies valid signature", () => { const payload = JSON.stringify({ userId: "test-123" }); const sig = handler["computeSignature"](payload); expect(handler.verifySignature(payload, sig)).toBe(true); }); it("rejects invalid signature", () => { const payload = JSON.stringify({ userId: "test-123" }); expect(handler.verifySignature(payload, "invalid-sig")).toBe(false); }); it("rejects missing signature", () => { expect(handler.verifySignature("payload", "")).toBe(false); }); it("accepts signature from array", () => { const payload = JSON.stringify({ userId: "test-123" }); const sig = handler["computeSignature"](payload); expect(handler.verifySignature(payload, ["other", sig, "another"])).toBe(true); }); }); describe("processEvent", () => { let userId: string; beforeEach(async () => { const user = await prisma.user.create({ data: { email: `webhook-test-${runId}@shieldai.local`, subscriptionTier: "PREMIUM", }, }); userId = user.id; runId = Date.now(); }); afterEach(async () => { await prisma.webhookEvent.deleteMany(); await prisma.scanJob.deleteMany({ where: { userId } }); await prisma.user.delete({ where: { id: userId } }); }); it("processes SCAN_TRIGGER event", async () => { const result = await handler.processEvent("SCAN_TRIGGER", { userId, dataSource: "HIBP", }); expect(result.eventId).toBeDefined(); expect(result.scanTriggered).toBe(true); const job = await prisma.scanJob.findFirst({ where: { userId, scheduledBy: "webhook" }, }); expect(job).not.toBeNull(); }); it("processes BREACH_DETECTED event", async () => { const result = await handler.processEvent("BREACH_DETECTED", { userId, breachName: "TestBreach", }); expect(result.eventId).toBeDefined(); expect(result.scanTriggered).toBe(false); }); it("normalizes event type", async () => { const result = await handler.processEvent("scan_trigger", { userId, }); expect(result.eventId).toBeDefined(); const event = await prisma.webhookEvent.findUnique({ where: { id: result.eventId }, }); expect(event?.eventType).toBe("SCAN_TRIGGER"); }); it("returns false for non-existent user", async () => { const result = await handler.processEvent("SCAN_TRIGGER", { userId: "non-existent-user-id", }); expect(result.scanTriggered).toBe(false); }); it("links scan job to webhook event", async () => { const result = await handler.processEvent("SCAN_TRIGGER", { userId, }); expect(result.scanTriggered).toBe(true); const event = await prisma.webhookEvent.findUnique({ where: { id: result.eventId }, }); expect(event?.scanJobId).toBeDefined(); expect(event?.processed).toBe(true); }); }); describe("signature validation in processEvent", () => { it("accepts event with valid signature", async () => { const payload = { userId: "test" }; const payloadStr = JSON.stringify(payload); const sig = handler["computeSignature"](payloadStr); const result = await handler.processEvent("SCAN_TRIGGER", payload, undefined, sig); expect(result.eventId).toBeDefined(); }); it("rejects event with invalid signature", async () => { const payload = { userId: "test" }; try { await handler.processEvent("SCAN_TRIGGER", payload, undefined, "bad-signature"); expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("signature"); } }); it("accepts event without signature when no signature provided", async () => { const result = await handler.processEvent("SCAN_TRIGGER", { userId: "test" }); expect(result.eventId).toBeDefined(); }); }); describe("processPendingEvents", () => { it("retries unprocessed events", async () => { const user = await prisma.user.create({ data: { email: `retry-test-${runId}@shieldai.local`, subscriptionTier: "BASIC", }, }); runId = Date.now(); await prisma.webhookEvent.create({ data: { eventType: "SCAN_TRIGGER", payload: JSON.stringify({ userId: user.id }), processed: false, }, }); const processed = await handler.processPendingEvents(); expect(processed).toBeGreaterThanOrEqual(1); const job = await prisma.scanJob.findFirst({ where: { userId: user.id, scheduledBy: "webhook" }, }); expect(job).not.toBeNull(); await prisma.scanJob.deleteMany({ where: { userId: user.id } }); await prisma.user.delete({ where: { id: user.id } }); }); }); describe("getEventHistory", () => { afterEach(async () => { await prisma.webhookEvent.deleteMany(); }); it("returns events ordered by creation time", async () => { await handler.processEvent("SCAN_TRIGGER", { userId: "user-1" }); await handler.processEvent("BREACH_DETECTED", { userId: "user-2" }); const events = await handler.getEventHistory(); expect(events.length).toBeGreaterThanOrEqual(2); expect(events[0].createdAt.getTime()).toBeGreaterThanOrEqual(events[1].createdAt.getTime()); }); it("respects limit and offset", async () => { for (let i = 0; i < 5; i++) { await handler.processEvent("SCAN_TRIGGER", { userId: `user-${i}` }); } const events = await handler.getEventHistory(3, 0); expect(events).toHaveLength(3); }); }); });