mirror of
https://github.com/documenso/documenso.git
synced 2026-08-27 00:32:29 +10:00
187 lines
6.4 KiB
TypeScript
187 lines
6.4 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 { 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);
|
|
});
|