mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
636 lines
18 KiB
TypeScript
636 lines
18 KiB
TypeScript
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
|
import { prisma } from '@documenso/prisma';
|
|
import {
|
|
DocumentSigningOrder,
|
|
DocumentStatus,
|
|
FieldType,
|
|
RecipientRole,
|
|
SendStatus,
|
|
SigningStatus,
|
|
} from '@documenso/prisma/client';
|
|
import { seedBlankDocument, seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import { expect, test } from '@playwright/test';
|
|
import { nanoid } from 'nanoid';
|
|
|
|
import { signSignaturePad } from '../../fixtures/signature';
|
|
|
|
test.describe('Document API', () => {
|
|
test('sendDocument: should respect sendCompletionEmails setting', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const { document } = await seedPendingDocumentWithFullFields({
|
|
owner: user,
|
|
recipients: ['signer@example.com'],
|
|
teamId: team.id,
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
// Test with sendCompletionEmails: false
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {
|
|
sendCompletionEmails: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
|
|
// Verify email settings were updated
|
|
const updatedDocument = await prisma.envelope.findUnique({
|
|
where: { id: document.id },
|
|
include: { documentMeta: true },
|
|
});
|
|
|
|
expect(updatedDocument?.documentMeta?.emailSettings).toMatchObject({
|
|
documentCompleted: false,
|
|
ownerDocumentCompleted: false,
|
|
});
|
|
|
|
// Test with sendCompletionEmails: true
|
|
const response2 = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {
|
|
sendCompletionEmails: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(response2.ok()).toBeTruthy();
|
|
expect(response2.status()).toBe(200);
|
|
|
|
// Verify email settings were updated
|
|
const updatedDocument2 = await prisma.envelope.findUnique({
|
|
where: { id: document.id },
|
|
include: { documentMeta: true },
|
|
});
|
|
|
|
expect(updatedDocument2?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
|
documentCompleted: true,
|
|
ownerDocumentCompleted: true,
|
|
});
|
|
});
|
|
|
|
test('sendDocument: should not modify email settings when sendCompletionEmails is not provided', async ({
|
|
request,
|
|
}) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const { document } = await seedPendingDocumentWithFullFields({
|
|
owner: user,
|
|
recipients: ['signer@example.com'],
|
|
teamId: team.id,
|
|
});
|
|
|
|
// Set initial email settings
|
|
await prisma.documentMeta.upsert({
|
|
where: { id: document.documentMetaId },
|
|
create: {
|
|
id: document.documentMetaId,
|
|
emailSettings: {
|
|
documentCompleted: true,
|
|
ownerDocumentCompleted: false,
|
|
},
|
|
},
|
|
update: {
|
|
id: document.documentMetaId,
|
|
emailSettings: {
|
|
documentCompleted: true,
|
|
ownerDocumentCompleted: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {
|
|
sendEmail: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
|
|
// Verify email settings were not modified
|
|
const updatedDocument = await prisma.envelope.findUnique({
|
|
where: { id: document.id },
|
|
include: { documentMeta: true },
|
|
});
|
|
|
|
expect(updatedDocument?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
|
documentCompleted: true,
|
|
ownerDocumentCompleted: false,
|
|
});
|
|
});
|
|
|
|
test('sendDocument: should fail when signer has no signature field', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
// Add a signer recipient without any fields
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email: 'signer@example.com',
|
|
name: 'Test Signer',
|
|
role: RecipientRole.SIGNER,
|
|
token: 'test-token-1',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeFalsy();
|
|
expect(response.status()).toBe(400);
|
|
});
|
|
|
|
test('sendDocument: should fail when signer has only non-signature fields', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
// Add a signer recipient with only a TEXT field (not signature)
|
|
const recipient = await prisma.recipient.create({
|
|
data: {
|
|
email: 'signer@example.com',
|
|
name: 'Test Signer',
|
|
role: RecipientRole.SIGNER,
|
|
token: 'test-token-2',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
// Add a TEXT field (not a signature field)
|
|
await prisma.field.create({
|
|
data: {
|
|
type: FieldType.TEXT,
|
|
page: 1,
|
|
positionX: 100,
|
|
positionY: 100,
|
|
width: 50,
|
|
height: 50,
|
|
customText: '',
|
|
inserted: false,
|
|
recipientId: recipient.id,
|
|
envelopeId: document.id,
|
|
envelopeItemId: document.envelopeItems[0].id,
|
|
},
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeFalsy();
|
|
expect(response.status()).toBe(400);
|
|
});
|
|
|
|
test('sendDocument: should succeed when signer has signature field', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const { document } = await seedPendingDocumentWithFullFields({
|
|
owner: user,
|
|
recipients: ['signer@example.com'],
|
|
teamId: team.id,
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
test('sendDocument: should succeed when signer has FREE_SIGNATURE field', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
// Add a signer recipient
|
|
const recipient = await prisma.recipient.create({
|
|
data: {
|
|
email: 'signer@example.com',
|
|
name: 'Test Signer',
|
|
role: RecipientRole.SIGNER,
|
|
token: 'test-token-3',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
// Add a FREE_SIGNATURE field
|
|
await prisma.field.create({
|
|
data: {
|
|
type: FieldType.FREE_SIGNATURE,
|
|
page: 1,
|
|
positionX: 100,
|
|
positionY: 100,
|
|
width: 50,
|
|
height: 50,
|
|
customText: '',
|
|
inserted: false,
|
|
recipientId: recipient.id,
|
|
envelopeId: document.id,
|
|
envelopeItemId: document.envelopeItems[0].id,
|
|
},
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
test('sendDocument: should succeed when non-signer roles have no fields', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
// Add a signer with signature field
|
|
const signer = await prisma.recipient.create({
|
|
data: {
|
|
email: 'signer@example.com',
|
|
name: 'Test Signer',
|
|
role: RecipientRole.SIGNER,
|
|
token: 'test-token-4',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
await prisma.field.create({
|
|
data: {
|
|
type: FieldType.SIGNATURE,
|
|
page: 1,
|
|
positionX: 1,
|
|
positionY: 1,
|
|
width: 1,
|
|
height: 1,
|
|
customText: '',
|
|
inserted: false,
|
|
recipientId: signer.id,
|
|
envelopeId: document.id,
|
|
envelopeItemId: document.envelopeItems[0].id,
|
|
fieldMeta: { type: 'signature', fontSize: 14 },
|
|
},
|
|
});
|
|
|
|
// Add a viewer without any fields
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email: 'viewer@example.com',
|
|
name: 'Test Viewer',
|
|
role: RecipientRole.VIEWER,
|
|
token: 'test-token-5',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
// Add an approver without any fields
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email: 'approver@example.com',
|
|
name: 'Test Approver',
|
|
role: RecipientRole.APPROVER,
|
|
token: 'test-token-6',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
// Add a CC without any fields
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email: 'cc@example.com',
|
|
name: 'Test CC',
|
|
role: RecipientRole.CC,
|
|
token: 'test-token-7',
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
test('sendDocument: should complete document immediately when all recipients are CC', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
// Add two CC recipients without any fields, mirroring the production
|
|
// state where CC recipients are created pre-signed.
|
|
for (const email of ['cc1@example.com', 'cc2@example.com']) {
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email,
|
|
name: 'Test CC',
|
|
role: RecipientRole.CC,
|
|
signingStatus: SigningStatus.SIGNED,
|
|
sendStatus: SendStatus.SENT,
|
|
token: nanoid(),
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
|
|
// The document seals asynchronously and completes without anyone signing.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const updatedDocument = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
});
|
|
|
|
return updatedDocument.status;
|
|
},
|
|
{ timeout: 30_000 },
|
|
)
|
|
.toBe(DocumentStatus.COMPLETED);
|
|
});
|
|
|
|
test('sendDocument: should not block initial sequential send when CC recipient is first in signing order', async ({
|
|
request,
|
|
page,
|
|
}) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
// Create a blank document and get it with envelope items
|
|
const blankDocument = await seedBlankDocument(user, team.id);
|
|
const document = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: blankDocument.id },
|
|
include: { envelopeItems: true },
|
|
});
|
|
|
|
await prisma.documentMeta.update({
|
|
where: { id: document.documentMetaId },
|
|
data: { signingOrder: DocumentSigningOrder.SEQUENTIAL },
|
|
});
|
|
|
|
// CC recipient first in the signing order, mirroring the production
|
|
// state where CC recipients are created pre-signed.
|
|
await prisma.recipient.create({
|
|
data: {
|
|
email: 'cc@example.com',
|
|
name: 'Test CC',
|
|
role: RecipientRole.CC,
|
|
signingOrder: 1,
|
|
signingStatus: SigningStatus.SIGNED,
|
|
sendStatus: SendStatus.SENT,
|
|
token: nanoid(),
|
|
envelopeId: document.id,
|
|
},
|
|
});
|
|
|
|
const [signerA, signerB] = await Promise.all(
|
|
[
|
|
{ email: 'signer-a@example.com', name: 'Signer A', signingOrder: 2 },
|
|
{ email: 'signer-b@example.com', name: 'Signer B', signingOrder: 3 },
|
|
].map(async ({ email, name, signingOrder }) =>
|
|
prisma.recipient.create({
|
|
data: {
|
|
email,
|
|
name,
|
|
role: RecipientRole.SIGNER,
|
|
signingOrder,
|
|
token: nanoid(),
|
|
envelopeId: document.id,
|
|
fields: {
|
|
create: {
|
|
type: FieldType.SIGNATURE,
|
|
page: 1,
|
|
positionX: signingOrder * 10,
|
|
positionY: 10,
|
|
width: 5,
|
|
height: 5,
|
|
customText: '',
|
|
inserted: false,
|
|
envelopeId: document.id,
|
|
envelopeItemId: document.envelopeItems[0].id,
|
|
fieldMeta: { type: 'signature', fontSize: 14 },
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const response = await request.post(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {},
|
|
},
|
|
);
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
expect(response.status()).toBe(200);
|
|
|
|
// The CC recipient at order 1 must not block signer A at order 2.
|
|
await page.goto(`/sign/${signerA.token}`);
|
|
await expect(page).not.toHaveURL(`/sign/${signerA.token}/waiting`);
|
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
|
|
|
// Signer B at order 3 must still wait for signer A.
|
|
await page.goto(`/sign/${signerB.token}`);
|
|
await expect(page).toHaveURL(`/sign/${signerB.token}/waiting`);
|
|
|
|
// Sign as signer A then signer B.
|
|
for (const signer of [signerA, signerB]) {
|
|
await page.goto(`/sign/${signer.token}`);
|
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
|
await signSignaturePad(page);
|
|
|
|
const signerField = await prisma.field.findFirstOrThrow({
|
|
where: { recipientId: signer.id },
|
|
});
|
|
|
|
await page.locator(`#field-${signerField.id}`).getByRole('button').click();
|
|
|
|
await page.getByRole('button', { name: 'Complete' }).click();
|
|
await page.getByRole('button', { name: 'Sign' }).click();
|
|
await page.waitForURL(`/sign/${signer.token}/complete`);
|
|
}
|
|
|
|
// The document completes without any action from the CC recipient.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const updatedDocument = await prisma.envelope.findFirstOrThrow({
|
|
where: { id: document.id },
|
|
});
|
|
|
|
return updatedDocument.status;
|
|
},
|
|
{ timeout: 30_000 },
|
|
)
|
|
.toBe(DocumentStatus.COMPLETED);
|
|
});
|
|
});
|