mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
feat: validate signers have signature fields before distribution (#2411)
API users were inadvertently sending documents without signature fields, causing confusion for recipients and breaking their signing flows. - Add getRecipientsWithMissingFields helper in recipients.ts - Add server-side validation in sendDocument to block distribution - Fix v1 API to return 400 instead of 500 for validation errors - Consolidate UI signature field checks to use isSignatureFieldType - Add E2E tests for both v1 and v2 APIs
This commit is contained in:
@@ -4,7 +4,11 @@ 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 { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import {
|
||||
seedBlankDocument,
|
||||
seedPendingDocumentWithFullFields,
|
||||
} from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
test.describe('Document API', () => {
|
||||
@@ -145,4 +149,293 @@ test.describe('Document API', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,6 +171,24 @@ test.describe('Template Field Prefill API v1', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Add SIGNATURE field (required for distribution)
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
envelopeItemId: firstEnvelopeItem.id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
// 6. Sign in as the user
|
||||
await apiSignin({
|
||||
page,
|
||||
@@ -444,6 +462,24 @@ test.describe('Template Field Prefill API v1', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Add SIGNATURE field (required for distribution)
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
envelopeItemId: firstEnvelopeItem.id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
// 6. Sign in as the user
|
||||
await apiSignin({
|
||||
page,
|
||||
|
||||
@@ -221,7 +221,7 @@ test.describe('Document Access API V1', () => {
|
||||
);
|
||||
|
||||
expect(resB.ok()).toBeFalsy();
|
||||
expect(resB.status()).toBe(500);
|
||||
expect(resB.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should allow authorized access to document send endpoint', async ({ request }) => {
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import type { Team, User } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { EnvelopeType, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
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';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
||||
|
||||
test.describe.configure({
|
||||
mode: 'parallel',
|
||||
});
|
||||
|
||||
test.describe('Envelope distribute validation', () => {
|
||||
let user: User, team: Team, token: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
({ user, team } = await seedUser());
|
||||
({ token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
}));
|
||||
});
|
||||
|
||||
const createEnvelope = async (request: APIRequestContext, authToken: string) => {
|
||||
const payload: TCreateEnvelopePayload = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Test Document',
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
const pdfData = fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf'));
|
||||
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()) as TCreateEnvelopeResponse;
|
||||
};
|
||||
|
||||
const getEnvelope = async (request: APIRequestContext, authToken: string, envelopeId: string) => {
|
||||
const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()) as TGetEnvelopeResponse;
|
||||
};
|
||||
|
||||
const createRecipients = async (
|
||||
request: APIRequestContext,
|
||||
authToken: string,
|
||||
envelopeId: string,
|
||||
recipients: TCreateEnvelopeRecipientsRequest['data'],
|
||||
) => {
|
||||
const res = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
data: {
|
||||
envelopeId,
|
||||
data: recipients,
|
||||
} satisfies TCreateEnvelopeRecipientsRequest,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()).data;
|
||||
};
|
||||
|
||||
const createFields = async (
|
||||
request: APIRequestContext,
|
||||
authToken: string,
|
||||
envelopeId: string,
|
||||
envelopeItemId: string,
|
||||
fields: Array<{ recipientId: number; type: FieldType }>,
|
||||
) => {
|
||||
const res = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
data: {
|
||||
envelopeId,
|
||||
data: fields.map((field, index) => ({
|
||||
recipientId: field.recipientId,
|
||||
envelopeItemId,
|
||||
type: field.type,
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 10 + index * 10,
|
||||
width: 10,
|
||||
height: 10,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()).data;
|
||||
};
|
||||
|
||||
test('should fail to distribute when signer has no fields', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
|
||||
// Create a signer without any fields
|
||||
await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Try to distribute without adding any fields
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeFalsy();
|
||||
expect(distributeRes.status()).toBe(400);
|
||||
|
||||
const errorResponse = await distributeRes.json();
|
||||
expect(errorResponse.message).toContain('missing required fields');
|
||||
expect(errorResponse.message).toContain('Signers must have at least one signature field');
|
||||
});
|
||||
|
||||
test('should fail to distribute when signer has non-signature fields only', async ({
|
||||
request,
|
||||
}) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create a signer
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add only a TEXT field (not a signature field)
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.TEXT },
|
||||
]);
|
||||
|
||||
// Try to distribute
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeFalsy();
|
||||
expect(distributeRes.status()).toBe(400);
|
||||
|
||||
const errorResponse = await distributeRes.json();
|
||||
expect(errorResponse.message).toContain('missing required fields');
|
||||
});
|
||||
|
||||
test('should succeed when signer has SIGNATURE field', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create a signer
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add a SIGNATURE field
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should succeed
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeTruthy();
|
||||
expect(distributeRes.status()).toBe(200);
|
||||
|
||||
const response = await distributeRes.json();
|
||||
expect(response.success).toBe(true);
|
||||
});
|
||||
|
||||
// Note: FREE_SIGNATURE field type is not supported via the v2 API for field creation,
|
||||
// so we only test with SIGNATURE fields here. The v1 tests cover FREE_SIGNATURE
|
||||
// using direct Prisma creation.
|
||||
|
||||
test('should succeed when VIEWER has no fields', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create a signer and a viewer
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'viewer@example.com',
|
||||
name: 'Test Viewer',
|
||||
role: RecipientRole.VIEWER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add signature field only for the signer (viewer has no fields)
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should succeed
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeTruthy();
|
||||
expect(distributeRes.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('should succeed when CC has no fields', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create a signer and a CC recipient
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'cc@example.com',
|
||||
name: 'Test CC',
|
||||
role: RecipientRole.CC,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add signature field only for the signer (CC has no fields)
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should succeed
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeTruthy();
|
||||
expect(distributeRes.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('should succeed when APPROVER has no fields', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create a signer and an approver
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'Test Signer',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'approver@example.com',
|
||||
name: 'Test Approver',
|
||||
role: RecipientRole.APPROVER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add signature field only for the signer (approver has no fields)
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should succeed
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeTruthy();
|
||||
expect(distributeRes.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('should fail when one of multiple signers is missing signature field', async ({
|
||||
request,
|
||||
}) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create two signers
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer1@example.com',
|
||||
name: 'Test Signer 1',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'signer2@example.com',
|
||||
name: 'Test Signer 2',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add signature field only for the first signer
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should fail because second signer has no signature field
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeFalsy();
|
||||
expect(distributeRes.status()).toBe(400);
|
||||
|
||||
const errorResponse = await distributeRes.json();
|
||||
expect(errorResponse.message).toContain('missing required fields');
|
||||
});
|
||||
|
||||
test('should succeed when all signers have signature fields', async ({ request }) => {
|
||||
const envelope = await createEnvelope(request, token);
|
||||
const envelopeData = await getEnvelope(request, token, envelope.id);
|
||||
|
||||
// Create two signers
|
||||
const recipients = await createRecipients(request, token, envelope.id, [
|
||||
{
|
||||
email: 'signer1@example.com',
|
||||
name: 'Test Signer 1',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
{
|
||||
email: 'signer2@example.com',
|
||||
name: 'Test Signer 2',
|
||||
role: RecipientRole.SIGNER,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Add signature fields for both signers
|
||||
await createFields(request, token, envelope.id, envelopeData.envelopeItems[0].id, [
|
||||
{ recipientId: recipients[0].id, type: FieldType.SIGNATURE },
|
||||
{ recipientId: recipients[1].id, type: FieldType.SIGNATURE },
|
||||
]);
|
||||
|
||||
// Distribute should succeed
|
||||
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id },
|
||||
});
|
||||
|
||||
expect(distributeRes.ok()).toBeTruthy();
|
||||
expect(distributeRes.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -171,6 +171,24 @@ test.describe('Template Field Prefill API v2', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Add SIGNATURE field (required for distribution)
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
envelopeItemId: firstEnvelopeItem.id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
// 6. Sign in as the user
|
||||
await apiSignin({
|
||||
page,
|
||||
@@ -441,6 +459,24 @@ test.describe('Template Field Prefill API v2', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Add SIGNATURE field (required for distribution)
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
envelopeItemId: firstEnvelopeItem.id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
// 6. Sign in as the user
|
||||
await apiSignin({
|
||||
page,
|
||||
|
||||
@@ -195,6 +195,31 @@ test.describe('Document API V2', () => {
|
||||
}) => {
|
||||
const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']);
|
||||
|
||||
// Get the recipient created during seeding.
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
envelopeId: doc.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a signature field for the recipient so distribution validation can run.
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: doc.id,
|
||||
envelopeItemId: doc.envelopeItems[0].id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) },
|
||||
@@ -207,6 +232,31 @@ test.describe('Document API V2', () => {
|
||||
test('should allow authorized access to document distribute endpoint', async ({ request }) => {
|
||||
const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']);
|
||||
|
||||
// Get the recipient created during seeding.
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
envelopeId: doc.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a signature field for the recipient so distribution validation can run.
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: doc.id,
|
||||
envelopeItemId: doc.envelopeItems[0].id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) },
|
||||
@@ -3678,6 +3728,26 @@ test.describe('Document API V2', () => {
|
||||
internalVersion: 2,
|
||||
});
|
||||
|
||||
const [recipient] = doc.recipients;
|
||||
|
||||
// add signing field for recipient (fieldMeta required for v2 envelopes)
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
page: 1,
|
||||
type: FieldType.SIGNATURE,
|
||||
inserted: false,
|
||||
customText: '',
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
envelopeId: doc.id,
|
||||
envelopeItemId: doc.envelopeItems[0].id,
|
||||
recipientId: recipient.id,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
const payload: TUseEnvelopePayload = {
|
||||
envelopeId: doc.id,
|
||||
distributeDocument: true,
|
||||
@@ -3741,6 +3811,31 @@ test.describe('Document API V2', () => {
|
||||
}) => {
|
||||
const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']);
|
||||
|
||||
// Get the recipient created during seeding.
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
envelopeId: doc.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a signature field for the recipient so distribution validation can pass.
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: doc.id,
|
||||
envelopeItemId: doc.envelopeItems[0].id,
|
||||
recipientId: recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 1,
|
||||
positionY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: { type: 'signature', fontSize: 14 },
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
data: { envelopeId: doc.id },
|
||||
|
||||
Reference in New Issue
Block a user