mirror of
https://github.com/documenso/documenso.git
synced 2026-08-27 00:32:29 +10:00
267 lines
9.1 KiB
TypeScript
267 lines
9.1 KiB
TypeScript
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { type APIRequestContext, expect, test } from '@playwright/test';
|
|
import { FieldType } from '@prisma/client';
|
|
|
|
import { apiSeedPendingDocument } from '../fixtures/api-seeds';
|
|
|
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
|
|
|
type SeededGroupEnvelope = {
|
|
assistantToken: string;
|
|
assistantOwnTextFieldId: number;
|
|
peerTextFieldId: number;
|
|
peerSignatureFieldId: number;
|
|
laterTextFieldId: number;
|
|
laterSignatureFieldId: number;
|
|
};
|
|
|
|
/**
|
|
* Seeds a pending SEQUENTIAL envelope where the ASSISTANT shares a signing
|
|
* step (duplicate signingOrder) with a SIGNER:
|
|
*
|
|
* - Step 1: ASSISTANT (own TEXT field) + "Peer Signer" (SIGNATURE + TEXT).
|
|
* - Step 2: "Later Signer" (SIGNATURE + TEXT).
|
|
*
|
|
* Product rule under signing groups: assistants only assist STRICTLY LATER
|
|
* steps — never their own group peers — and never insert SIGNATURE fields
|
|
* belonging to anyone else.
|
|
*/
|
|
const seedGroupedAssistantEnvelope = async (request: APIRequestContext): Promise<SeededGroupEnvelope> => {
|
|
const timestamp = Date.now();
|
|
|
|
const peerEmail = `peer-signer-${timestamp}@documenso.com`;
|
|
const laterEmail = `later-signer-${timestamp}@documenso.com`;
|
|
|
|
const { envelope, distributeResult } = await apiSeedPendingDocument(request, {
|
|
title: '[TEST] Grouped assistant envelope',
|
|
meta: {
|
|
signingOrder: 'SEQUENTIAL',
|
|
},
|
|
recipients: [
|
|
{
|
|
email: `assistant-${timestamp}@documenso.com`,
|
|
name: 'Assistant',
|
|
role: 'ASSISTANT',
|
|
signingOrder: 1,
|
|
},
|
|
{
|
|
email: peerEmail,
|
|
name: 'Peer Signer',
|
|
role: 'SIGNER',
|
|
signingOrder: 1,
|
|
},
|
|
{
|
|
email: laterEmail,
|
|
name: 'Later Signer',
|
|
role: 'SIGNER',
|
|
signingOrder: 2,
|
|
},
|
|
],
|
|
fieldsPerRecipient: [
|
|
[{ type: FieldType.TEXT, page: 1, positionX: 5, positionY: 5, width: 5, height: 5 }],
|
|
[
|
|
{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 15, width: 5, height: 5 },
|
|
{ type: FieldType.TEXT, page: 1, positionX: 5, positionY: 25, width: 5, height: 5 },
|
|
],
|
|
[
|
|
{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 35, width: 5, height: 5 },
|
|
{ type: FieldType.TEXT, page: 1, positionX: 5, positionY: 45, width: 5, height: 5 },
|
|
],
|
|
],
|
|
});
|
|
|
|
const assistant = distributeResult.recipients.find((r) => r.role === 'ASSISTANT');
|
|
const peer = distributeResult.recipients.find((r) => r.email === peerEmail);
|
|
const later = distributeResult.recipients.find((r) => r.email === laterEmail);
|
|
|
|
if (!assistant || !peer || !later) {
|
|
throw new Error('Seeded recipients not found');
|
|
}
|
|
|
|
const fields = await prisma.field.findMany({
|
|
where: { envelopeId: envelope.id },
|
|
});
|
|
|
|
const findField = (recipientId: number, type: FieldType) => {
|
|
const field = fields.find((f) => f.recipientId === recipientId && f.type === type);
|
|
|
|
if (!field) {
|
|
throw new Error(`Field ${type} not found for recipient ${recipientId}`);
|
|
}
|
|
|
|
return field;
|
|
};
|
|
|
|
return {
|
|
assistantToken: assistant.token,
|
|
assistantOwnTextFieldId: findField(assistant.id, FieldType.TEXT).id,
|
|
peerTextFieldId: findField(peer.id, FieldType.TEXT).id,
|
|
peerSignatureFieldId: findField(peer.id, FieldType.SIGNATURE).id,
|
|
laterTextFieldId: findField(later.id, FieldType.TEXT).id,
|
|
laterSignatureFieldId: findField(later.id, FieldType.SIGNATURE).id,
|
|
};
|
|
};
|
|
|
|
const trpcMutation = async (request: APIRequestContext, procedure: string, input: Record<string, unknown>) => {
|
|
return await request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, {
|
|
headers: { 'content-type': 'application/json' },
|
|
data: JSON.stringify({ json: input }),
|
|
});
|
|
};
|
|
|
|
test.describe('[ASSISTANT_SIGNING_GROUPS]: same-step (group peer) field access', () => {
|
|
test('field.signFieldWithToken (V1) rejects a group peer field', async ({ request }) => {
|
|
const { assistantToken, peerTextFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const res = await trpcMutation(request, 'field.signFieldWithToken', {
|
|
token: assistantToken,
|
|
fieldId: peerTextFieldId,
|
|
value: 'TEXT',
|
|
isBase64: false,
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: peerTextFieldId },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(false);
|
|
expect(fieldAfter.customText).toBe('');
|
|
});
|
|
|
|
test('field.removeSignedFieldWithToken (V1) rejects a group peer field', async ({ request }) => {
|
|
const { assistantToken, peerTextFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
// Pre-insert the peer's field so a successful (incorrect) uninsert is detectable.
|
|
await prisma.field.update({
|
|
where: { id: peerTextFieldId },
|
|
data: { inserted: true, customText: 'pre-existing-value' },
|
|
});
|
|
|
|
const res = await trpcMutation(request, 'field.removeSignedFieldWithToken', {
|
|
token: assistantToken,
|
|
fieldId: peerTextFieldId,
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: peerTextFieldId },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(true);
|
|
expect(fieldAfter.customText).toBe('pre-existing-value');
|
|
});
|
|
|
|
test('envelope.field.sign (V2) rejects a group peer field', async ({ request }) => {
|
|
const { assistantToken, peerTextFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const res = await trpcMutation(request, 'envelope.field.sign', {
|
|
token: assistantToken,
|
|
fieldId: peerTextFieldId,
|
|
fieldValue: { type: FieldType.TEXT, value: 'TEXT' },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: peerTextFieldId },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(false);
|
|
});
|
|
|
|
test('getFieldsForToken excludes group peer fields but keeps own and later-step fields', async ({ request }) => {
|
|
const {
|
|
assistantToken,
|
|
assistantOwnTextFieldId,
|
|
peerTextFieldId,
|
|
peerSignatureFieldId,
|
|
laterTextFieldId,
|
|
laterSignatureFieldId,
|
|
} = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const fields = await getFieldsForToken({ token: assistantToken });
|
|
const fieldIds = fields.map((field) => field.id);
|
|
|
|
// Own fields and strictly-later non-signature fields remain visible.
|
|
expect(fieldIds).toContain(assistantOwnTextFieldId);
|
|
expect(fieldIds).toContain(laterTextFieldId);
|
|
|
|
// Group peer fields are never visible to the assistant.
|
|
expect(fieldIds).not.toContain(peerTextFieldId);
|
|
expect(fieldIds).not.toContain(peerSignatureFieldId);
|
|
|
|
// Signature fields of other recipients are never visible to the assistant.
|
|
expect(fieldIds).not.toContain(laterSignatureFieldId);
|
|
});
|
|
});
|
|
|
|
test.describe('[ASSISTANT_SIGNING_GROUPS]: signature fields of other recipients', () => {
|
|
test('field.signFieldWithToken (V1) rejects inserting a later recipient signature field', async ({ request }) => {
|
|
const { assistantToken, laterSignatureFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const res = await trpcMutation(request, 'field.signFieldWithToken', {
|
|
token: assistantToken,
|
|
fieldId: laterSignatureFieldId,
|
|
value: 'John Doe',
|
|
isBase64: false,
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: laterSignatureFieldId },
|
|
include: { signature: true },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(false);
|
|
expect(fieldAfter.signature).toBeNull();
|
|
});
|
|
});
|
|
|
|
test.describe('[ASSISTANT_SIGNING_GROUPS]: preserved assistant abilities', () => {
|
|
test('field.signFieldWithToken (V1) still allows filling the assistant own field', async ({ request }) => {
|
|
const { assistantToken, assistantOwnTextFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const res = await trpcMutation(request, 'field.signFieldWithToken', {
|
|
token: assistantToken,
|
|
fieldId: assistantOwnTextFieldId,
|
|
value: 'MY OWN TEXT',
|
|
isBase64: false,
|
|
});
|
|
|
|
expect(res.ok(), await res.text()).toBeTruthy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: assistantOwnTextFieldId },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(true);
|
|
expect(fieldAfter.customText).toBe('MY OWN TEXT');
|
|
});
|
|
|
|
test('field.signFieldWithToken (V1) still allows prefilling a later recipient text field', async ({ request }) => {
|
|
const { assistantToken, laterTextFieldId } = await seedGroupedAssistantEnvelope(request);
|
|
|
|
const res = await trpcMutation(request, 'field.signFieldWithToken', {
|
|
token: assistantToken,
|
|
fieldId: laterTextFieldId,
|
|
value: 'PREFILLED FOR LATER SIGNER',
|
|
isBase64: false,
|
|
});
|
|
|
|
expect(res.ok(), await res.text()).toBeTruthy();
|
|
|
|
const fieldAfter = await prisma.field.findUniqueOrThrow({
|
|
where: { id: laterTextFieldId },
|
|
});
|
|
|
|
expect(fieldAfter.inserted).toBe(true);
|
|
expect(fieldAfter.customText).toBe('PREFILLED FOR LATER SIGNER');
|
|
});
|
|
});
|