mirror of
https://github.com/documenso/documenso.git
synced 2026-08-27 00:32:29 +10:00
fix: wip
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user