/** * Tests for the Documenso webhook receiver route at * `src/app/api/webhooks/documenso/route.ts`. * * The receiver was previously only covered indirectly: the unit test * `webhook-event-map.test.ts` validates the static event-name map, and * `documents-expired-webhook.test.ts` calls handlers directly. Neither * exercised the route's auth, dedup, dispatch loop, or 200-on-failure * envelope. This file fills that gap. * * Refs: docs/audit-comprehensive-2026-05-05.md HIGH ยง19 (auditor-J Issue 2). */ import { describe, it, expect } from 'vitest'; import { eq } from 'drizzle-orm'; import { NextRequest } from 'next/server'; import { POST as documensoWebhook } from '@/app/api/webhooks/documenso/route'; import { db } from '@/lib/db'; import { documents, documentEvents } from '@/lib/db/schema/documents'; import { env } from '@/lib/env'; import { makeClient, makePort } from '../helpers/factories'; function buildRequest(body: Record, secret: string): NextRequest { return new NextRequest('http://localhost:3000/api/webhooks/documenso', { method: 'POST', headers: { 'content-type': 'application/json', 'x-documenso-secret': secret, }, body: JSON.stringify(body), }); } describe('Documenso webhook route', () => { it('returns 200 with ok:false when the secret header is missing or wrong', async () => { const req = buildRequest( { event: 'DOCUMENT_SIGNED', payload: { id: 'abc', recipients: [] } }, 'wrong-secret', ); const res = await documensoWebhook(req); expect(res.status).toBe(200); const body = await res.json(); expect(body).toMatchObject({ ok: false }); }); it('with a valid secret + DOCUMENT_SIGNED writes a documentEvents row', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const documensoId = `docu-test-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId: port.id, clientId: client.id, documentType: 'eoi', title: 'Webhook test EOI', status: 'sent', documensoId, createdBy: 'seed', }) .returning(); const req = buildRequest( { event: 'DOCUMENT_SIGNED', payload: { id: documensoId, recipients: [{ email: 'signer@test.invalid', signingStatus: 'SIGNED' }], }, }, env.DOCUMENSO_WEBHOOK_SECRET, ); const res = await documensoWebhook(req); expect(res.status).toBe(200); const events = await db .select() .from(documentEvents) .where(eq(documentEvents.documentId, doc!.id)); expect(events.length).toBeGreaterThanOrEqual(1); }); it('replays of the same body are no-ops (signatureHash dedup)', async () => { const port = await makePort(); const client = await makeClient({ portId: port.id }); const documensoId = `docu-dedup-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId: port.id, clientId: client.id, documentType: 'eoi', title: 'Dedup test EOI', status: 'sent', documensoId, createdBy: 'seed', }) .returning(); const body = { event: 'DOCUMENT_OPENED', payload: { id: documensoId, recipients: [{ email: 'opener@test.invalid', readStatus: 'OPENED' }], }, }; await documensoWebhook(buildRequest(body, env.DOCUMENSO_WEBHOOK_SECRET)); await documensoWebhook(buildRequest(body, env.DOCUMENSO_WEBHOOK_SECRET)); const events = await db .select() .from(documentEvents) .where(eq(documentEvents.documentId, doc!.id)); // The route's `handleDocumentOpened` writes an event with type // `'viewed'`. One row from the first call; the second should have // been refused by the signatureHash dedup guard. const viewedEvents = events.filter((e) => e.eventType === 'viewed'); expect(viewedEvents.length).toBe(1); }); });