import { describe, it, expect, vi, beforeEach } from 'vitest'; // Validation-path tests for uploadDocumentForSigning. The // heavy-integration paths (storage put, Documenso round-trip, signer // updates) are exercised by Playwright realapi specs; these tests // pin the input-validation contract so a regression in the // recipient/field/PDF guards is caught at unit-test time. // Stub the heavy dependencies BEFORE importing the service so its // module-level imports resolve to the stubs. vi.mock('@/lib/db', () => ({ db: { query: { interests: { findFirst: vi.fn().mockResolvedValue({ id: 'int-1', portId: 'port-1', clientId: 'c-1' }), }, ports: { findFirst: vi.fn().mockResolvedValue({ id: 'port-1', name: 'Test Port' }) }, }, }, })); vi.mock('@/lib/services/berth-pdf-parser', () => ({ isPdfMagic: (b: Buffer) => b.slice(0, 5).toString() === '%PDF-', })); import { uploadDocumentForSigning, type CustomDocumentRecipient, } from '@/lib/services/custom-document-upload.service'; const PDF_HEADER = Buffer.from('%PDF-1.7\n'); const NON_PDF = Buffer.from('this is not a PDF'); const baseArgs = { interestId: 'int-1', portId: 'port-1', portSlug: 'test-port', documentType: 'contract' as const, title: 'Sales Contract', pdfBuffer: PDF_HEADER, filename: 'contract.pdf', recipients: [ { name: 'Buyer', email: 'buyer@example.com', role: 'SIGNER', signingOrder: 1 }, { name: 'Seller', email: 'seller@example.com', role: 'SIGNER', signingOrder: 2 }, ] satisfies CustomDocumentRecipient[], fields: [ { recipientIndex: 0, type: 'SIGNATURE' as const, pageNumber: 1, pageX: 10, pageY: 80, pageWidth: 30, pageHeight: 5, }, ], meta: { userId: 'user-1', portId: 'port-1', ipAddress: '127.0.0.1', userAgent: 'test', }, }; describe('uploadDocumentForSigning validation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('rejects empty recipient list', async () => { await expect(uploadDocumentForSigning({ ...baseArgs, recipients: [] })).rejects.toThrow( /at least one recipient/i, ); }); it('rejects empty field list', async () => { await expect(uploadDocumentForSigning({ ...baseArgs, fields: [] })).rejects.toThrow( /at least one field/i, ); }); it('rejects empty PDF buffer', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, pdfBuffer: Buffer.alloc(0) }), ).rejects.toThrow(/PDF buffer is empty/); }); it('rejects oversized PDF', async () => { const oversized = Buffer.alloc(51 * 1024 * 1024, 0x20); oversized.write('%PDF-1.7', 0); await expect(uploadDocumentForSigning({ ...baseArgs, pdfBuffer: oversized })).rejects.toThrow( /exceeds.*MB cap/i, ); }); it('rejects non-PDF magic bytes', async () => { await expect(uploadDocumentForSigning({ ...baseArgs, pdfBuffer: NON_PDF })).rejects.toThrow( /not a PDF/, ); }); it('rejects out-of-range recipientIndex on a field', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, fields: [{ ...baseArgs.fields[0]!, recipientIndex: 5 }], }), ).rejects.toThrow(/out of range/); }); it('rejects negative recipientIndex on a field', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, fields: [{ ...baseArgs.fields[0]!, recipientIndex: -1 }], }), ).rejects.toThrow(/out of range/); }); it('rejects duplicate signingOrder across recipients', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: 'A', email: 'a@x.com', role: 'SIGNER', signingOrder: 1 }, { name: 'B', email: 'b@x.com', role: 'SIGNER', signingOrder: 1 }, ], }), ).rejects.toThrow(/Duplicate signingOrder/); }); // P1.2 — pre-flight validation extends to recipient email + name // shape. Service mirrors the dialog's pre-Submit checks so direct API // hits also reject early. it('rejects recipient with blank email', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: 'Buyer', email: '', role: 'SIGNER', signingOrder: 1 }, { name: 'Seller', email: 'seller@example.com', role: 'SIGNER', signingOrder: 2 }, ], }), ).rejects.toThrow(/missing an email/i); }); it('rejects recipient with whitespace-only email', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: 'Buyer', email: ' ', role: 'SIGNER', signingOrder: 1 }, { name: 'Seller', email: 'seller@example.com', role: 'SIGNER', signingOrder: 2 }, ], }), ).rejects.toThrow(/missing an email/i); }); it('rejects recipient with malformed email', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: 'Buyer', email: 'not-an-email', role: 'SIGNER', signingOrder: 1 }, { name: 'Seller', email: 'seller@example.com', role: 'SIGNER', signingOrder: 2 }, ], }), ).rejects.toThrow(/invalid email/i); }); it('rejects recipient with blank name', async () => { await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: '', email: 'buyer@example.com', role: 'SIGNER', signingOrder: 1 }, { name: 'Seller', email: 'seller@example.com', role: 'SIGNER', signingOrder: 2 }, ], }), ).rejects.toThrow(/missing a name/i); }); it('accepts duplicate emails across recipients (Documenso dedupes by email)', async () => { // The validation guard does NOT reject same-email recipients — at // the field-placement step the email→recipientId map collapses them // to a single Documenso recipientId by design. Other guards (PDF, // recipient row insert, Documenso round-trip) prevent this test // from reaching success in unit-mode; we only assert that the // recipient-validation block does NOT throw early. await expect( uploadDocumentForSigning({ ...baseArgs, recipients: [ { name: 'Buyer One', email: 'shared@example.com', role: 'SIGNER', signingOrder: 1 }, { name: 'Buyer Two', email: 'shared@example.com', role: 'SIGNER', signingOrder: 2 }, ], }), ).rejects.not.toThrow(/missing an email|invalid email|missing a name/i); }); });