import { expect, test } from "bun:test"; import { createHmac } from "node:crypto"; import { and, eq, inArray } from "drizzle-orm"; import { emailDeliveries, emailWebhookEvents, events, getDb, groups } from "@album/database"; import { emailDeliveryOutcome, recordEmailWebhook, verifyEmailWebhook } from "./webhooks"; const key = Buffer.from("local-test-signing-secret-not-production"); const secret = `whsec_${key.toString("base64")}`; function signed(type = "email.delivered", timestamp = Math.floor(Date.now() / 1000)) { const id = `test_${crypto.randomUUID()}`; const payload = JSON.stringify({ type, created_at: new Date().toISOString(), data: { email_id: crypto.randomUUID() } }); const signature = createHmac("sha256", key).update(`${id}.${timestamp}.${payload}`).digest("base64"); return { payload, headers: new Headers({ "svix-id": id, "svix-timestamp": `${timestamp}`, "svix-signature": `v1,${signature}` }) }; } test("webhooks require an authentic, fresh, unmodified signature", () => { const input = signed(); expect(verifyEmailWebhook(input.payload, input.headers, secret)?.outcome).toBe("delivered"); expect(() => verifyEmailWebhook(input.payload + " ", input.headers, secret)).toThrow(); expect(() => verifyEmailWebhook(input.payload, new Headers(), secret)).toThrow(); const stale = signed("email.delivered", 1); expect(() => verifyEmailWebhook(stale.payload, stale.headers, secret)).toThrow(); const ignored = signed("email.opened"); expect(verifyEmailWebhook(ignored.payload, ignored.headers, secret)).toBeNull(); }); test.skipIf(process.env.WEBHOOK_INTEGRATION !== "1")("early, duplicate and reordered callbacks preserve terminal outcome and event scope", async () => { if (!["localhost", "127.0.0.1"].includes(new URL(process.env.DATABASE_URL!).hostname)) throw new Error("Local database required"); const db = getDb(); const [group] = await db.select({ id: groups.id }).from(groups).limit(1); const input = signed("email.bounced"); const callback = verifyEmailWebhook(input.payload, input.headers, secret)!; const ids = [callback.id, `${callback.id}-delivered`]; const [event] = await db.insert(events).values({ groupId: group!.id, title: "Webhook test", slug: `webhook-${crypto.randomUUID()}` }).returning(); try { await Promise.all([recordEmailWebhook(callback), recordEmailWebhook(callback)]); expect(await db.select().from(emailWebhookEvents).where(eq(emailWebhookEvents.id, callback.id))).toHaveLength(1); const [delivery] = await db.insert(emailDeliveries).values({ eventId: event!.id, recipient: "webhook@manyangles.test", provider: "resend", payload: { to: "webhook@manyangles.test", from: "test@manyangles.test", subject: "Test", html: "", text: "", referenceId: "test" } }).returning(); const scope = and(eq(emailDeliveries.eventId, event!.id), eq(emailDeliveries.id, delivery!.id)); expect((await db.select({ outcome: emailDeliveryOutcome }).from(emailDeliveries).where(scope))[0]?.outcome).toBeNull(); await db.update(emailDeliveries).set({ providerId: callback.providerId }).where(scope); await recordEmailWebhook({ ...callback, id: ids[1]!, outcome: "delivered", occurredAt: new Date(Date.now() + 1000) }); expect((await db.select({ outcome: emailDeliveryOutcome }).from(emailDeliveries).where(scope))[0]?.outcome).toBe("bounced"); expect(await db.select({ outcome: emailDeliveryOutcome }).from(emailDeliveries).where(eq(emailDeliveries.eventId, crypto.randomUUID()))).toHaveLength(0); await db.update(emailDeliveries).set({ provider: "mailpit" }).where(scope); expect((await db.select({ outcome: emailDeliveryOutcome }).from(emailDeliveries).where(scope))[0]?.outcome).toBeNull(); } finally { await db.delete(events).where(eq(events.id, event!.id)); await db.delete(emailWebhookEvents).where(inArray(emailWebhookEvents.id, ids)); } });