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