import fs from 'node:fs'; import path from 'node:path'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; import { EnvelopeType, FieldType, RecipientRole } from '@documenso/prisma/client'; import { seedUser } from '@documenso/prisma/seed/users'; import type { TCreateEnvelopePayload, TCreateEnvelopeResponse, } from '@documenso/trpc/server/envelope-router/create-envelope.types'; import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types'; import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types'; import { type APIRequestContext, expect, test } from '@playwright/test'; import type { Team, User } from '@prisma/client'; const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`; test.describe.configure({ mode: 'parallel', }); test.describe('Envelope distribute validation', () => { let user: User, team: Team, token: string; test.beforeEach(async () => { ({ user, team } = await seedUser()); ({ token } = await createApiToken({ userId: user.id, teamId: team.id, tokenName: 'test', expiresIn: null, })); }); const createEnvelope = async (request: APIRequestContext, authToken: string) => { const payload: TCreateEnvelopePayload = { type: EnvelopeType.DOCUMENT, title: 'Test Document', }; const formData = new FormData(); formData.append('payload', JSON.stringify(payload)); const pdfData = fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')); formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' })); const res = await request.post(`${baseUrl}/envelope/create`, { headers: { Authorization: `Bearer ${authToken}` }, multipart: formData, }); expect(res.ok()).toBeTruthy(); return (await res.json()) as TCreateEnvelopeResponse; }; const getEnvelope = async (request: APIRequestContext, authToken: string, envelopeId: string) => { const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, { headers: { Authorization: `Bearer ${authToken}` }, }); expect(res.ok()).toBeTruthy(); return (await res.json()) as TGetEnvelopeResponse; }; const createRecipients = async ( request: APIRequestContext, authToken: string, envelopeId: string, recipients: TCreateEnvelopeRecipientsRequest['data'], ) => { const res = await request.post(`${baseUrl}/envelope/recipient/create-many`, { headers: { Authorization: `Bearer ${authToken}` }, data: { envelopeId, data: recipients, } satisfies TCreateEnvelopeRecipientsRequest, }); expect(res.ok()).toBeTruthy(); return (await res.json()).data; }; const createFields = async ( request: APIRequestContext, authToken: string, envelopeId: string, envelopeItemId: string, fields: Array<{ recipientId: number; type: FieldType }>, ) => { const res = await request.post(`${baseUrl}/envelope/field/create-many`, { headers: { Authorization: `Bearer ${authToken}` }, data: { envelopeId, data: fields.map((field, index) => ({ recipientId: field.recipientId, envelopeItemId, type: field.type, page: 1, positionX: 10, positionY: 10 + index * 10, width: 10, height: 10, })), }, }); expect(res.ok()).toBeTruthy(); return (await res.json()).data; }; test('should fail to distribute when signer has no fields', async ({ request }) => { const envelope = await createEnvelope(request, token); // Create a signer without any fields await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, ]); // Try to distribute without adding any fields const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeFalsy(); expect(distributeRes.status()).toBe(400); const errorResponse = await distributeRes.json(); expect(errorResponse.message).toContain('missing required fields'); expect(errorResponse.message).toContain('Signers must have at least one signature field'); }); test('should fail to distribute when signer has non-signature fields only', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create a signer const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, ]); // Add only a TEXT field (not a signature field) await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.TEXT }, ]); // Try to distribute const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeFalsy(); expect(distributeRes.status()).toBe(400); const errorResponse = await distributeRes.json(); expect(errorResponse.message).toContain('missing required fields'); }); test('should succeed when signer has SIGNATURE field', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create a signer const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, ]); // Add a SIGNATURE field await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, ]); // Distribute should succeed const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeTruthy(); expect(distributeRes.status()).toBe(200); const response = await distributeRes.json(); expect(response.success).toBe(true); }); // Note: FREE_SIGNATURE field type is not supported via the v2 API for field creation, // so we only test with SIGNATURE fields here. The v1 tests cover FREE_SIGNATURE // using direct Prisma creation. test('should succeed when VIEWER has no fields', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create a signer and a viewer const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, { email: 'viewer@example.com', name: 'Test Viewer', role: RecipientRole.VIEWER, accessAuth: [], actionAuth: [], }, ]); // Add signature field only for the signer (viewer has no fields) await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, ]); // Distribute should succeed const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeTruthy(); expect(distributeRes.status()).toBe(200); }); test('should succeed when CC has no fields', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create a signer and a CC recipient const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, { email: 'cc@example.com', name: 'Test CC', role: RecipientRole.CC, accessAuth: [], actionAuth: [], }, ]); // Add signature field only for the signer (CC has no fields) await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, ]); // Distribute should succeed const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeTruthy(); expect(distributeRes.status()).toBe(200); }); test('should succeed when APPROVER has no fields', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create a signer and an approver const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer@example.com', name: 'Test Signer', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, { email: 'approver@example.com', name: 'Test Approver', role: RecipientRole.APPROVER, accessAuth: [], actionAuth: [], }, ]); // Add signature field only for the signer (approver has no fields) await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, ]); // Distribute should succeed const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeTruthy(); expect(distributeRes.status()).toBe(200); }); test('should fail when one of multiple signers is missing signature field', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create two signers const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer1@example.com', name: 'Test Signer 1', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, { email: 'signer2@example.com', name: 'Test Signer 2', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, ]); // Add signature field only for the first signer await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, ]); // Distribute should fail because second signer has no signature field const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeFalsy(); expect(distributeRes.status()).toBe(400); const errorResponse = await distributeRes.json(); expect(errorResponse.message).toContain('missing required fields'); }); test('should succeed when all signers have signature fields', async ({ request }) => { const envelope = await createEnvelope(request, token); const envelopeData = await getEnvelope(request, token, envelope.id); // Create two signers const recipients = await createRecipients(request, token, envelope.id, [ { email: 'signer1@example.com', name: 'Test Signer 1', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, { email: 'signer2@example.com', name: 'Test Signer 2', role: RecipientRole.SIGNER, accessAuth: [], actionAuth: [], }, ]); // Add signature fields for both signers await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [ { recipientId: recipients[0].id, type: FieldType.SIGNATURE }, { recipientId: recipients[1].id, type: FieldType.SIGNATURE }, ]); // Distribute should succeed const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, { headers: { Authorization: `Bearer ${token}` }, data: { envelopeId: envelope.id }, }); expect(distributeRes.ok()).toBeTruthy(); expect(distributeRes.status()).toBe(200); }); });