mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
561 lines
18 KiB
TypeScript
561 lines
18 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import type { Team, User } from '@prisma/client';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { pick } from 'remeda';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
|
import { prisma } from '@documenso/prisma';
|
|
import {
|
|
DocumentDistributionMethod,
|
|
DocumentSigningOrder,
|
|
DocumentStatus,
|
|
DocumentVisibility,
|
|
EnvelopeType,
|
|
FieldType,
|
|
FolderType,
|
|
RecipientRole,
|
|
} from '@documenso/prisma/client';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
|
import type {
|
|
TCreateEnvelopePayload,
|
|
TCreateEnvelopeResponse,
|
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
|
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
|
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
|
|
|
import { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
|
|
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
|
|
|
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
|
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
|
|
|
test.describe.configure({
|
|
mode: 'parallel',
|
|
});
|
|
|
|
test.describe('API V2 Envelopes', () => {
|
|
let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;
|
|
|
|
test.beforeEach(async () => {
|
|
({ user: userA, team: teamA } = await seedUser());
|
|
({ token: tokenA } = await createApiToken({
|
|
userId: userA.id,
|
|
teamId: teamA.id,
|
|
tokenName: 'userA',
|
|
expiresIn: null,
|
|
}));
|
|
|
|
({ user: userB, team: teamB } = await seedUser());
|
|
({ token: tokenB } = await createApiToken({
|
|
userId: userB.id,
|
|
teamId: teamB.id,
|
|
tokenName: 'userB',
|
|
expiresIn: null,
|
|
}));
|
|
});
|
|
|
|
test.describe('Envelope create endpoint', () => {
|
|
test('should fail on invalid form', async ({ request }) => {
|
|
const payload = {
|
|
type: 'Invalid Type',
|
|
title: 'Test Envelope',
|
|
};
|
|
|
|
const formData = new FormData();
|
|
formData.append('payload', JSON.stringify(payload));
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${tokenB}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('should create envelope with single file', async ({ request }) => {
|
|
const payload = {
|
|
type: EnvelopeType.TEMPLATE,
|
|
title: 'Test Envelope',
|
|
} satisfies TCreateEnvelopePayload;
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append('payload', JSON.stringify(payload));
|
|
|
|
const files = [
|
|
{
|
|
name: 'field-font-alignment.pdf',
|
|
data: fs.readFileSync(
|
|
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
|
|
),
|
|
},
|
|
];
|
|
|
|
for (const file of files) {
|
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
|
}
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${tokenB}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(res.status()).toBe(200);
|
|
|
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
|
|
|
const envelope = await prisma.envelope.findUnique({
|
|
where: {
|
|
id: response.id,
|
|
},
|
|
include: {
|
|
envelopeItems: true,
|
|
},
|
|
});
|
|
|
|
expect(envelope).toBeDefined();
|
|
expect(envelope?.title).toBe('Test Envelope');
|
|
expect(envelope?.type).toBe(EnvelopeType.TEMPLATE);
|
|
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
|
|
expect(envelope?.envelopeItems.length).toBe(1);
|
|
expect(envelope?.envelopeItems[0].title).toBe('field-font-alignment.pdf');
|
|
expect(envelope?.envelopeItems[0].documentDataId).toBeDefined();
|
|
});
|
|
|
|
test('should create envelope with multiple file', async ({ request }) => {
|
|
const folder = await prisma.folder.create({
|
|
data: {
|
|
name: 'Test Folder',
|
|
teamId: teamA.id,
|
|
userId: userA.id,
|
|
type: FolderType.DOCUMENT,
|
|
},
|
|
});
|
|
|
|
const payload = {
|
|
title: 'Envelope Title',
|
|
type: EnvelopeType.DOCUMENT,
|
|
externalId: 'externalId',
|
|
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
|
globalAccessAuth: ['ACCOUNT'],
|
|
formValues: {
|
|
hello: 'world',
|
|
},
|
|
folderId: folder.id,
|
|
recipients: [
|
|
{
|
|
email: userA.email,
|
|
name: 'Name',
|
|
role: RecipientRole.SIGNER,
|
|
accessAuth: ['TWO_FACTOR_AUTH'],
|
|
signingOrder: 1,
|
|
fields: [
|
|
{
|
|
type: FieldType.SIGNATURE,
|
|
identifier: 'field-font-alignment.pdf',
|
|
page: 1,
|
|
positionX: 0,
|
|
positionY: 0,
|
|
width: 0,
|
|
height: 0,
|
|
},
|
|
{
|
|
type: FieldType.SIGNATURE,
|
|
identifier: 0,
|
|
page: 1,
|
|
positionX: 0,
|
|
positionY: 0,
|
|
width: 0,
|
|
height: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
meta: {
|
|
subject: 'Subject',
|
|
message: 'Message',
|
|
timezone: 'Europe/Berlin',
|
|
dateFormat: 'dd.MM.yyyy',
|
|
distributionMethod: DocumentDistributionMethod.NONE,
|
|
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
|
allowDictateNextSigner: true,
|
|
redirectUrl: 'https://documenso.com',
|
|
language: 'de',
|
|
typedSignatureEnabled: true,
|
|
uploadSignatureEnabled: false,
|
|
drawSignatureEnabled: false,
|
|
emailReplyTo: userA.email,
|
|
emailSettings: {
|
|
recipientSigningRequest: false,
|
|
recipientRemoved: false,
|
|
recipientSigned: false,
|
|
documentPending: false,
|
|
documentCompleted: false,
|
|
documentDeleted: false,
|
|
ownerDocumentCompleted: true,
|
|
},
|
|
},
|
|
attachments: [
|
|
{
|
|
label: 'Test Attachment',
|
|
data: 'https://documenso.com',
|
|
type: 'link',
|
|
},
|
|
],
|
|
} satisfies TCreateEnvelopePayload;
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append('payload', JSON.stringify(payload));
|
|
|
|
const files = [
|
|
{
|
|
name: 'field-meta.pdf',
|
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-meta.pdf')),
|
|
},
|
|
{
|
|
name: 'field-font-alignment.pdf',
|
|
data: fs.readFileSync(
|
|
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
|
|
),
|
|
},
|
|
];
|
|
|
|
for (const file of files) {
|
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
|
}
|
|
|
|
// Should error since folder is not owned by the user.
|
|
const invalidRes = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${tokenB}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(invalidRes.ok()).toBeFalsy();
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(res.status()).toBe(200);
|
|
|
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
|
|
|
const envelope = await prisma.envelope.findUniqueOrThrow({
|
|
where: {
|
|
id: response.id,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
envelopeItems: true,
|
|
recipients: true,
|
|
fields: true,
|
|
envelopeAttachments: true,
|
|
},
|
|
});
|
|
|
|
console.log(userB.email);
|
|
|
|
expect(envelope.envelopeItems.length).toBe(2);
|
|
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
|
|
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
|
|
|
|
expect(envelope.title).toBe(payload.title);
|
|
expect(envelope.type).toBe(payload.type);
|
|
expect(envelope.externalId).toBe(payload.externalId);
|
|
expect(envelope.visibility).toBe(payload.visibility);
|
|
expect(envelope.authOptions).toEqual({
|
|
globalAccessAuth: payload.globalAccessAuth,
|
|
globalActionAuth: [],
|
|
});
|
|
expect(envelope.formValues).toEqual(payload.formValues);
|
|
expect(envelope.folderId).toBe(payload.folderId);
|
|
|
|
expect(envelope.documentMeta.subject).toBe(payload.meta.subject);
|
|
expect(envelope.documentMeta.message).toBe(payload.meta.message);
|
|
expect(envelope.documentMeta.timezone).toBe(payload.meta.timezone);
|
|
expect(envelope.documentMeta.dateFormat).toBe(payload.meta.dateFormat);
|
|
expect(envelope.documentMeta.distributionMethod).toBe(payload.meta.distributionMethod);
|
|
expect(envelope.documentMeta.signingOrder).toBe(payload.meta.signingOrder);
|
|
expect(envelope.documentMeta.allowDictateNextSigner).toBe(
|
|
payload.meta.allowDictateNextSigner,
|
|
);
|
|
expect(envelope.documentMeta.redirectUrl).toBe(payload.meta.redirectUrl);
|
|
expect(envelope.documentMeta.language).toBe(payload.meta.language);
|
|
expect(envelope.documentMeta.typedSignatureEnabled).toBe(payload.meta.typedSignatureEnabled);
|
|
expect(envelope.documentMeta.uploadSignatureEnabled).toBe(
|
|
payload.meta.uploadSignatureEnabled,
|
|
);
|
|
expect(envelope.documentMeta.drawSignatureEnabled).toBe(payload.meta.drawSignatureEnabled);
|
|
expect(envelope.documentMeta.emailReplyTo).toBe(payload.meta.emailReplyTo);
|
|
expect(envelope.documentMeta.emailSettings).toEqual(payload.meta.emailSettings);
|
|
|
|
expect([
|
|
{
|
|
label: envelope.envelopeAttachments[0].label,
|
|
data: envelope.envelopeAttachments[0].data,
|
|
type: envelope.envelopeAttachments[0].type,
|
|
},
|
|
]).toEqual(payload.attachments);
|
|
|
|
const field = envelope.fields[0];
|
|
const recipient = envelope.recipients[0];
|
|
|
|
expect({
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
accessAuth: recipient.authOptions?.accessAuth,
|
|
}).toEqual(
|
|
pick(payload.recipients[0], ['email', 'name', 'role', 'signingOrder', 'accessAuth']),
|
|
);
|
|
|
|
expect({
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX.toNumber(),
|
|
positionY: field.positionY.toNumber(),
|
|
width: field.width.toNumber(),
|
|
height: field.height.toNumber(),
|
|
}).toEqual(
|
|
pick(payload.recipients[0].fields[0], [
|
|
'type',
|
|
'page',
|
|
'positionX',
|
|
'positionY',
|
|
'width',
|
|
'height',
|
|
]),
|
|
);
|
|
|
|
// Expect string based ID to work.
|
|
expect(field.envelopeItemId).toBe(
|
|
envelope.envelopeItems.find((item) => item.title === 'field-font-alignment.pdf')?.id,
|
|
);
|
|
|
|
// Expect index based ID to work.
|
|
expect(envelope.fields[1].envelopeItemId).toBe(
|
|
envelope.envelopeItems.find((item) => item.title === 'field-meta.pdf')?.id,
|
|
);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Creates envelopes with the two field test PDFs.
|
|
*/
|
|
test('Envelope full test', async ({ request }) => {
|
|
// Step 1: Create initial envelope with Prisma (with first envelope item)
|
|
const alignmentPdf = fs.readFileSync(
|
|
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
|
|
);
|
|
|
|
const fieldMetaPdf = fs.readFileSync(
|
|
path.join(__dirname, '../../../../../assets/field-meta.pdf'),
|
|
);
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append(
|
|
'payload',
|
|
JSON.stringify({
|
|
type: EnvelopeType.DOCUMENT,
|
|
title: 'Envelope Full Field Test',
|
|
} satisfies TCreateEnvelopePayload),
|
|
);
|
|
|
|
// Only add one file for now.
|
|
formData.append(
|
|
'files',
|
|
new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }),
|
|
);
|
|
|
|
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
|
expect(createEnvelopeRequest.status()).toBe(200);
|
|
|
|
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
|
|
|
const getEnvelopeRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
});
|
|
|
|
const createdEnvelope: TGetEnvelopeResponse = await getEnvelopeRequest.json();
|
|
|
|
// Might as well testing access control here as well.
|
|
const unauthRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
|
|
headers: { Authorization: `Bearer ${tokenB}` },
|
|
});
|
|
|
|
expect(unauthRequest.ok()).toBeFalsy();
|
|
expect(unauthRequest.status()).toBe(404);
|
|
|
|
// Step 2: Create second envelope item via API
|
|
const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
|
|
envelopeId: createdEnvelope.id,
|
|
};
|
|
|
|
const createEnvelopeItemFormData = new FormData();
|
|
createEnvelopeItemFormData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
|
|
createEnvelopeItemFormData.append(
|
|
'files',
|
|
new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }),
|
|
);
|
|
|
|
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
multipart: createEnvelopeItemFormData,
|
|
});
|
|
|
|
expect(createItemsRes.ok()).toBeTruthy();
|
|
expect(createItemsRes.status()).toBe(200);
|
|
|
|
// Step 3: Update envelope via API
|
|
const updateEnvelopeRequest: TUpdateEnvelopeRequest = {
|
|
envelopeId: createdEnvelope.id,
|
|
data: {
|
|
title: 'Envelope Full Field Test',
|
|
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
|
},
|
|
};
|
|
|
|
const updateRes = await request.post(`${baseUrl}/envelope/update`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
data: updateEnvelopeRequest,
|
|
});
|
|
|
|
expect(updateRes.ok()).toBeTruthy();
|
|
expect(updateRes.status()).toBe(200);
|
|
|
|
// Step 4: Create recipient via API
|
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
|
envelopeId: createdEnvelope.id,
|
|
data: [
|
|
{
|
|
email: userA.email,
|
|
name: userA.name || '',
|
|
role: RecipientRole.SIGNER,
|
|
accessAuth: [],
|
|
actionAuth: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
data: createRecipientsRequest,
|
|
});
|
|
|
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
|
expect(createRecipientsRes.status()).toBe(200);
|
|
|
|
// Step 5: Get envelope to retrieve recipients and envelope items
|
|
const getRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
});
|
|
|
|
expect(getRes.ok()).toBeTruthy();
|
|
expect(getRes.status()).toBe(200);
|
|
|
|
const envelopeResponse = (await getRes.json()) as TGetEnvelopeResponse;
|
|
|
|
const recipientId = envelopeResponse.recipients[0].id;
|
|
const alignmentItem = envelopeResponse.envelopeItems.find(
|
|
(item: { order: number }) => item.order === 1,
|
|
);
|
|
const fieldMetaItem = envelopeResponse.envelopeItems.find(
|
|
(item: { order: number }) => item.order === 2,
|
|
);
|
|
|
|
expect(recipientId).toBeDefined();
|
|
expect(alignmentItem).toBeDefined();
|
|
expect(fieldMetaItem).toBeDefined();
|
|
|
|
if (!alignmentItem || !fieldMetaItem) {
|
|
throw new Error('Envelope items not found');
|
|
}
|
|
|
|
// Step 6: Create fields for first PDF (alignment fields)
|
|
const alignmentFieldsRequest = {
|
|
envelopeId: createdEnvelope.id,
|
|
data: formatAlignmentTestFields.map((field) => ({
|
|
recipientId,
|
|
envelopeItemId: alignmentItem.id,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
fieldMeta: field.fieldMeta,
|
|
})),
|
|
};
|
|
|
|
const createAlignmentFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
data: alignmentFieldsRequest,
|
|
});
|
|
|
|
expect(createAlignmentFieldsRes.ok()).toBeTruthy();
|
|
expect(createAlignmentFieldsRes.status()).toBe(200);
|
|
|
|
// Step 7: Create fields for second PDF (field-meta fields)
|
|
const fieldMetaFieldsRequest = {
|
|
envelopeId: createdEnvelope.id,
|
|
data: FIELD_META_TEST_FIELDS.map((field) => ({
|
|
recipientId,
|
|
envelopeItemId: fieldMetaItem.id,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
fieldMeta: field.fieldMeta,
|
|
})),
|
|
};
|
|
|
|
const createFieldMetaFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
data: fieldMetaFieldsRequest,
|
|
});
|
|
|
|
expect(createFieldMetaFieldsRes.ok()).toBeTruthy();
|
|
expect(createFieldMetaFieldsRes.status()).toBe(200);
|
|
|
|
// Step 8: Verify final envelope structure
|
|
const finalGetRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
|
|
headers: { Authorization: `Bearer ${tokenA}` },
|
|
});
|
|
|
|
expect(finalGetRes.ok()).toBeTruthy();
|
|
const finalEnvelope = (await finalGetRes.json()) as TGetEnvelopeResponse;
|
|
|
|
// Verify structure
|
|
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
|
expect(finalEnvelope.recipients.length).toBe(1);
|
|
expect(finalEnvelope.fields.length).toBe(
|
|
formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
|
|
);
|
|
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
|
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
|
|
|
console.log({
|
|
createdEnvelopeId: finalEnvelope.id,
|
|
userEmail: userA.email,
|
|
});
|
|
});
|
|
});
|