import { describe, it, expect, vi, beforeEach } from 'vitest'; /** * Signing-automation orchestrator validation tests. Heavy integration * paths (actual Documenso round-trip, real signer dispatch) are * exercised by realapi Playwright specs. These tests pin the input- * validation contract: refuses to enable on completed / cancelled / * already-automated documents, refuses when documensoId is null, * refuses with fewer than two signers. */ // vi.hoisted lets us declare mock helpers that the hoisted vi.mock // factory can reference safely — without hoisted, the factory sees // undefined for any top-level const. const mocks = vi.hoisted(() => ({ docFindFirst: vi.fn(), signersFindFirst: vi.fn(), updateCall: vi.fn(() => ({ set: vi.fn(() => ({ where: vi.fn() })) })), selectCall: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ orderBy: vi.fn(() => Promise.resolve([])) })), })), })), })); vi.mock('@/lib/db', () => ({ db: { query: { documents: { findFirst: mocks.docFindFirst }, documentSigners: { findFirst: mocks.signersFindFirst }, ports: { findFirst: vi.fn().mockResolvedValue({ id: 'port-1', name: 'Test Port' }) }, }, update: mocks.updateCall, select: mocks.selectCall, }, })); vi.mock('@/lib/services/port-config', () => ({ getPortDocumensoConfig: vi .fn() .mockResolvedValue({ signingOrder: 'SEQUENTIAL', developerName: 'Dev' }), })); vi.mock('@/lib/services/documenso-client', () => ({ getDocument: vi .fn() .mockResolvedValue({ id: 'env-1', status: 'PENDING', recipients: [], signingOrder: 'SEQUENTIAL', }), distributeEnvelopeV2: vi .fn() .mockResolvedValue({ id: 'env-1', status: 'PENDING', recipients: [], signingOrder: 'SEQUENTIAL', }), })); vi.mock('@/lib/services/document-signing-emails.service', () => ({ sendSigningInvitation: vi.fn().mockResolvedValue(undefined), })); vi.mock('@/lib/services/documenso-signers', () => ({ DOC_TYPE_LABEL: { eoi: 'EOI', contract: 'Contract', reservation_agreement: 'Reservation' }, })); vi.mock('@/lib/audit', () => ({ createAuditLog: vi.fn(), })); import { enableSigningAutomation, disableSigningAutomation, } from '@/lib/services/signing-automation.service'; const meta = { userId: 'user-1', portId: 'port-1', ipAddress: '127.0.0.1', userAgent: 'test', }; describe('enableSigningAutomation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('throws NotFound when document does not exist', async () => { mocks.docFindFirst.mockResolvedValue(null); await expect(enableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow(/document/i); }); it('throws Conflict when document has no Documenso envelope', async () => { mocks.docFindFirst.mockResolvedValue({ id: 'doc-1', documensoId: null, status: 'draft', automationMode: 'manual', documentType: 'eoi', invitationMessage: null, }); await expect(enableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow( /no Documenso envelope/i, ); }); it('throws Conflict when document is completed', async () => { mocks.docFindFirst.mockResolvedValue({ id: 'doc-1', documensoId: 'env-1', status: 'completed', automationMode: 'manual', documentType: 'eoi', invitationMessage: null, }); await expect(enableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow(/completed/); }); it('throws Conflict when automation is already enabled', async () => { mocks.docFindFirst.mockResolvedValue({ id: 'doc-1', documensoId: 'env-1', status: 'sent', automationMode: 'sequential_auto', documentType: 'eoi', invitationMessage: null, }); await expect(enableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow( /already enabled/i, ); }); it('throws Conflict when fewer than two signers exist', async () => { mocks.docFindFirst.mockResolvedValue({ id: 'doc-1', documensoId: 'env-1', status: 'sent', automationMode: 'manual', documentType: 'eoi', invitationMessage: null, }); // mockSelect.from.where.orderBy resolves to [] -> ensureSigningUrls // returns 0 signers -> service throws "at least two". await expect(enableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow( /at least two signers/i, ); }); }); describe('disableSigningAutomation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('throws NotFound when document does not exist', async () => { mocks.docFindFirst.mockResolvedValue(null); await expect(disableSigningAutomation('doc-1', 'port-1', meta)).rejects.toThrow(/document/i); }); it('is a no-op when document is already manual (idempotent)', async () => { mocks.docFindFirst.mockResolvedValue({ id: 'doc-1', automationMode: 'manual' }); await expect(disableSigningAutomation('doc-1', 'port-1', meta)).resolves.toBeUndefined(); // No update should fire when already manual. expect(mocks.updateCall).not.toHaveBeenCalled(); }); });