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/); }); });