This commit is contained in:
David Nguyen
2026-08-26 12:10:29 +10:00
parent 9dc83bdb06
commit 6db71b13d4
70 changed files with 5964 additions and 892 deletions
@@ -0,0 +1,186 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
/**
* A recipient without a signing order sits in the LAST step — the convention
* `effectiveOrder` encodes and the server sorts by (NULLS LAST). The
* assistant scoping filters must agree with it:
*
* - an ordered assistant may assist a null-order recipient (they are in the
* strictly later tail step), and
* - a null-order assistant may assist NOBODY (nobody comes after the last
* step) — historically `signingOrder ?? 0` treated them as FIRST, letting
* their token prefill every ordered recipient's fields.
*
* Null orders are only produced via the API, which is why no editor-driven
* test covers this.
*/
const seedAssistantDocument = async (options: {
assistantOrder: number | null;
signerOrder: number | null;
internalVersion?: number;
}) => {
const { user, team } = await seedUser();
const { user: assistantUser } = await seedUser();
const { user: signerUser } = await seedUser();
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: [assistantUser, signerUser],
recipientsCreateOptions: [
{ signingOrder: options.assistantOrder, role: RecipientRole.ASSISTANT },
{ signingOrder: options.signerOrder, role: RecipientRole.SIGNER },
],
fields: [FieldType.TEXT],
updateDocumentOptions: {
internalVersion: options.internalVersion ?? 1,
documentMeta: {
upsert: {
create: { signingOrder: DocumentSigningOrder.SEQUENTIAL },
update: { signingOrder: DocumentSigningOrder.SEQUENTIAL },
},
},
},
});
// The seed returns recipients ordered by signingOrder (nulls last), so
// positional destructuring would swap roles — select by role instead.
const assistant = recipients.find((recipient) => recipient.role === RecipientRole.ASSISTANT);
const signer = recipients.find((recipient) => recipient.role === RecipientRole.SIGNER);
if (!assistant || !signer) {
throw new Error('Seeded recipients not found');
}
const signerTextField = signer.fields.find((field) => field.type === FieldType.TEXT);
if (!signerTextField) {
throw new Error('Seeded text field not found');
}
return { assistant, signer, signerTextField };
};
const callSignEnvelopeField = async (page: Page, input: { token: string; fieldId: number }) => {
return await page.context().request.post(`${WEBAPP_BASE_URL}/api/trpc/envelope.field.sign`, {
headers: { 'content-type': 'application/json' },
data: JSON.stringify({
json: {
token: input.token,
fieldId: input.fieldId,
fieldValue: {
type: FieldType.TEXT,
value: 'TEXT',
},
},
}),
});
};
test('[ASSISTANT_NULL_ORDER]: an ordered assistant can assist a null-order (tail-step) recipient', async () => {
const { assistant, signer, signerTextField } = await seedAssistantDocument({
assistantOrder: 1,
signerOrder: null,
});
// The tail-step recipient is strictly later, so they must be assistable.
const assistableRecipients = await getRecipientsForAssistant({ token: assistant.token });
expect(assistableRecipients.map((recipient) => recipient.id)).toContain(signer.id);
// Their non-signature fields must be visible to the assistant.
const fields = await getFieldsForToken({ token: assistant.token });
expect(fields.map((field) => field.id)).toContain(signerTextField.id);
// And prefillable.
await signFieldWithToken({
token: assistant.token,
fieldId: signerTextField.id,
value: 'TEXT',
});
const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: signerTextField.id } });
expect(fieldAfter.inserted).toBe(true);
});
test('[ASSISTANT_NULL_ORDER]: a null-order (tail-step) assistant cannot assist anyone', async () => {
const { assistant, signer, signerTextField } = await seedAssistantDocument({
assistantOrder: null,
signerOrder: 1,
});
// The null-order assistant sits in the last step: nobody comes after them.
const assistableRecipients = await getRecipientsForAssistant({ token: assistant.token });
expect(assistableRecipients.map((recipient) => recipient.id)).toEqual([assistant.id]);
// Every ordered recipient is in an EARLIER step — prefilling must fail.
await expect(
signFieldWithToken({
token: assistant.token,
fieldId: signerTextField.id,
value: 'TEXT',
}),
).rejects.toThrow();
const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: signerTextField.id } });
expect(fieldAfter.inserted).toBe(false);
expect(fieldAfter.id).not.toBe(signer.id); // sanity: distinct entities
});
test('[ASSISTANT_NULL_ORDER]: V2 route allows an ordered assistant to prefill a null-order recipient', async ({
page,
}) => {
const { assistant, signerTextField } = await seedAssistantDocument({
assistantOrder: 1,
signerOrder: null,
internalVersion: 2,
});
const response = await callSignEnvelopeField(page, {
token: assistant.token,
fieldId: signerTextField.id,
});
expect(response.ok()).toBeTruthy();
const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: signerTextField.id } });
expect(fieldAfter.inserted).toBe(true);
});
test('[ASSISTANT_NULL_ORDER]: V2 route rejects a null-order assistant prefilling an ordered recipient', async ({
page,
}) => {
const { assistant, signerTextField } = await seedAssistantDocument({
assistantOrder: null,
signerOrder: 1,
internalVersion: 2,
});
const response = await callSignEnvelopeField(page, {
token: assistant.token,
fieldId: signerTextField.id,
});
expect(response.ok()).toBeFalsy();
const fieldAfter = await prisma.field.findUniqueOrThrow({ where: { id: signerTextField.id } });
expect(fieldAfter.inserted).toBe(false);
});