Files
documenso/packages/app-tests/e2e/document-auth/grouped-next-recipient-dictation.spec.ts
T
2026-08-26 12:10:29 +10:00

155 lines
5.8 KiB
TypeScript

import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, SendStatus } from '@prisma/client';
/**
* Dictation lets a signer rewrite who signs next. It cannot be allowed to
* operate on a signing group, for two reasons the server enforces separately:
*
* 1. The next step must hold exactly one recipient (`nextGroup.length === 1`),
* otherwise there is no single "next signer" to rewrite.
* 2. A signer whose own step is still pending (a peer has not signed) does not
* advance the flow at all, so they never reach the dictation branch.
*
* Both are silent — passing `nextSigner` into a state that disallows dictation
* is ignored rather than rejected — which is exactly why they need asserting.
* The existing dictation specs all drive the UI and none use a grouped step.
*/
const DICTATED_SIGNER = {
name: 'Dictated Signer',
email: 'dictated-signer@example.com',
};
const expectRecipientUpdatedAuditLogCount = async (envelopeId: string, expected: number) => {
const auditLogs = await prisma.documentAuditLog.findMany({
where: {
envelopeId,
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
},
});
expect(auditLogs.length).toBe(expected);
};
const seedDictationDocument = async (signingOrders: number[]) => {
const { user, team } = await seedUser();
const signers = await Promise.all(signingOrders.map(async () => (await seedUser()).user));
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
teamId: team.id,
recipients: signers,
recipientsCreateOptions: signingOrders.map((signingOrder) => ({
signingOrder,
// The seed defaults every recipient to SENT; later steps of a real
// SEQUENTIAL document are NOT_SENT until their step unlocks.
sendStatus: signingOrder === 1 ? SendStatus.SENT : SendStatus.NOT_SENT,
})),
// No fields, so completion is not blocked by unsigned required fields.
fields: [],
updateDocumentOptions: {
documentMeta: {
upsert: {
create: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
allowDictateNextSigner: true,
},
update: {
signingOrder: DocumentSigningOrder.SEQUENTIAL,
allowDictateNextSigner: true,
},
},
},
},
});
return signers.map((signer) => {
const recipient = recipients.find((item) => item.email === signer.email);
if (!recipient) {
throw new Error(`Seeded recipient ${signer.email} not found`);
}
return recipient;
});
};
test('[NEXT_RECIPIENT_DICTATION]: dictation is ignored when the next step is a group', async () => {
// Steps: 1 = first, 2 = two grouped recipients.
const [first, groupA, groupB] = await seedDictationDocument([1, 2, 2]);
await completeDocumentWithToken({
token: first.token,
id: { type: 'envelopeId', id: first.envelopeId },
nextSigner: DICTATED_SIGNER,
});
const groupAAfter = await prisma.recipient.findUniqueOrThrow({ where: { id: groupA.id } });
const groupBAfter = await prisma.recipient.findUniqueOrThrow({ where: { id: groupB.id } });
// Neither member of the group may be rewritten.
expect(groupAAfter.email).toBe(groupA.email);
expect(groupAAfter.name).toBe(groupA.name);
expect(groupBAfter.email).toBe(groupB.email);
expect(groupBAfter.name).toBe(groupB.name);
// The dictated identity must not have leaked onto anyone.
const dictated = await prisma.recipient.findFirst({
where: { envelopeId: first.envelopeId, email: DICTATED_SIGNER.email },
});
expect(dictated).toBeNull();
// A rewrite that did not happen must not be recorded as having happened.
await expectRecipientUpdatedAuditLogCount(first.envelopeId, 0);
// The group is still activated as normal — only the rewrite is suppressed.
expect(groupAAfter.sendStatus).toBe(SendStatus.SENT);
expect(groupBAfter.sendStatus).toBe(SendStatus.SENT);
});
test('[NEXT_RECIPIENT_DICTATION]: a group member cannot dictate while a peer is still unsigned', async () => {
// Steps: 1 = two grouped recipients, 2 = last.
const [groupA, groupB, last] = await seedDictationDocument([1, 1, 2]);
// The first member of the group signs while their peer is still outstanding.
await completeDocumentWithToken({
token: groupA.token,
id: { type: 'envelopeId', id: groupA.envelopeId },
nextSigner: DICTATED_SIGNER,
});
const lastWhilePeerPending = await prisma.recipient.findUniqueOrThrow({
where: { id: last.id },
});
// The step never unlocked, so there was nothing to dictate.
expect(lastWhilePeerPending.email).toBe(last.email);
expect(lastWhilePeerPending.name).toBe(last.name);
expect(lastWhilePeerPending.sendStatus).toBe(SendStatus.NOT_SENT);
await expectRecipientUpdatedAuditLogCount(groupA.envelopeId, 0);
// The peer completing the group *does* advance to a single-recipient step,
// so dictation applies — the positive control for the assertions above.
await completeDocumentWithToken({
token: groupB.token,
id: { type: 'envelopeId', id: groupB.envelopeId },
nextSigner: DICTATED_SIGNER,
});
const lastAfterGroupComplete = await prisma.recipient.findUniqueOrThrow({
where: { id: last.id },
});
expect(lastAfterGroupComplete.email).toBe(DICTATED_SIGNER.email);
expect(lastAfterGroupComplete.name).toBe(DICTATED_SIGNER.name);
expect(lastAfterGroupComplete.sendStatus).toBe(SendStatus.SENT);
await expectRecipientUpdatedAuditLogCount(groupA.envelopeId, 1);
});