import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { prisma } from '@documenso/prisma'; import { type APIRequestContext, expect, test } from '@playwright/test'; import { FieldType, SigningStatus } from '@prisma/client'; import { apiSeedPendingDocument } from '../fixtures/api-seeds'; const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); type SeededEnvelopes = { assistantToken: string; otherEnvelopeFieldId: number; }; /** * Seeds two unrelated pending envelopes: * - Envelope A has an ASSISTANT (with a token) plus a SIGNER. * - Envelope B is owned by a different user and has a SIGNER with a TEXT field. * * Returns the assistant's token from envelope A and the TEXT field id from * envelope B so callers can exercise signing routes across envelopes. */ const seedTwoPendingEnvelopes = async (request: APIRequestContext): Promise => { const envelopeA = await apiSeedPendingDocument(request, { title: '[TEST] Envelope A', recipients: [ { email: `assistant-${Date.now()}@documenso.com`, name: 'Assistant', role: 'ASSISTANT', signingOrder: 1, }, { email: `signer-a-${Date.now()}@documenso.com`, name: 'Signer A', role: 'SIGNER', signingOrder: 2, }, ], fieldsPerRecipient: [ [], // SIGNER needs a SIGNATURE field so distribution succeeds. [{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 5, height: 5 }], ], }); const assistant = envelopeA.distributeResult.recipients.find((r) => r.role === 'ASSISTANT'); if (!assistant) { throw new Error('Assistant recipient not found in envelope A'); } const envelopeB = await apiSeedPendingDocument(request, { title: '[TEST] Envelope B', recipients: [ { email: `signer-b-${Date.now()}@documenso.com`, name: 'Signer B', role: 'SIGNER', signingOrder: 1, }, ], // A TEXT field is used as the cross-envelope target. The V2 route has a // separate guard that blocks assistants from signing SIGNATURE fields, // which would mask whether the recipient lookup itself was scoped. fieldsPerRecipient: [ [ { type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 5, height: 5 }, { type: FieldType.TEXT, page: 1, positionX: 5, positionY: 15, width: 5, height: 5 }, ], ], }); const otherEnvelope = await prisma.envelope.findUniqueOrThrow({ where: { id: envelopeB.envelope.id }, include: { fields: true }, }); const textField = otherEnvelope.fields.find((f) => f.type === FieldType.TEXT); if (!textField) { throw new Error('TEXT field not found in envelope B'); } return { assistantToken: assistant.token, otherEnvelopeFieldId: textField.id, }; }; const trpcMutation = async (request: APIRequestContext, procedure: string, input: Record) => { return await request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, { headers: { 'content-type': 'application/json' }, data: JSON.stringify({ json: input }), }); }; test.describe('[ASSISTANT_SIGNING_AUTH]: cross-envelope field access', () => { test('envelope.field.sign (V2) rejects fieldId from another envelope', async ({ request }) => { const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request); const res = await trpcMutation(request, 'envelope.field.sign', { token: assistantToken, fieldId: otherEnvelopeFieldId, fieldValue: { type: FieldType.TEXT, value: 'TEXT' }, }); expect(res.ok()).toBeFalsy(); const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: otherEnvelopeFieldId }, }); expect(fieldAfter.inserted).toBe(false); expect(fieldAfter.customText).toBe(''); }); test('field.signFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => { const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request); const res = await trpcMutation(request, 'field.signFieldWithToken', { token: assistantToken, fieldId: otherEnvelopeFieldId, value: 'TEXT', isBase64: false, }); expect(res.ok()).toBeFalsy(); const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: otherEnvelopeFieldId }, }); expect(fieldAfter.inserted).toBe(false); expect(fieldAfter.customText).toBe(''); }); test('field.removeSignedFieldWithToken (V1) rejects fieldId from another envelope', async ({ request }) => { const { assistantToken, otherEnvelopeFieldId } = await seedTwoPendingEnvelopes(request); // Pre-insert the field so a successful (incorrect) uninsert is detectable. await prisma.field.update({ where: { id: otherEnvelopeFieldId }, data: { inserted: true, customText: 'pre-existing-value' }, }); const res = await trpcMutation(request, 'field.removeSignedFieldWithToken', { token: assistantToken, fieldId: otherEnvelopeFieldId, }); expect(res.ok()).toBeFalsy(); const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: otherEnvelopeFieldId }, include: { recipient: true }, }); expect(fieldAfter.inserted).toBe(true); expect(fieldAfter.customText).toBe('pre-existing-value'); expect(fieldAfter.recipient.signingStatus).toBe(SigningStatus.NOT_SIGNED); }); });