mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
fix(pdf): address AcroForm import review feedback
This commit is contained in:
@@ -13,8 +13,10 @@ import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import { PDF } from '@libpdf/core';
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import { PDF, PdfString } from '@libpdf/core';
|
||||
import { type APIRequestContext, expect, type Page, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
||||
@@ -40,8 +42,9 @@ const API_REQUEST_METADATA: ApiRequestMetadata = {
|
||||
};
|
||||
|
||||
type TestUser = Awaited<ReturnType<typeof seedUser>>['user'];
|
||||
type TestTeam = Awaited<ReturnType<typeof seedUser>>['team'];
|
||||
|
||||
const seedUserWithApiToken = async (): Promise<{ token: string; user: TestUser }> => {
|
||||
const seedUserWithApiToken = async (): Promise<{ token: string; user: TestUser; team: TestTeam }> => {
|
||||
const { user, team } = await seedUser();
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
@@ -50,7 +53,7 @@ const seedUserWithApiToken = async (): Promise<{ token: string; user: TestUser }
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
return { token, user };
|
||||
return { token, user, team };
|
||||
};
|
||||
|
||||
const pdfHasFormFields = async (pdf: Uint8Array): Promise<boolean> => {
|
||||
@@ -60,19 +63,36 @@ const pdfHasFormFields = async (pdf: Uint8Array): Promise<boolean> => {
|
||||
return (form?.fieldCount ?? 0) > 0;
|
||||
};
|
||||
|
||||
const createSignedSignatureAcroFormPdf = (): Promise<Uint8Array> => {
|
||||
const pdf = PDF.create();
|
||||
const page = pdf.addPage({ size: 'letter' });
|
||||
const form = pdf.getOrCreateForm();
|
||||
const textField = form.createTextField('full_name');
|
||||
const signatureField = form.createSignatureField('signed_signature');
|
||||
|
||||
page.drawField(textField, { x: 100, y: 700, width: 200, height: 24 });
|
||||
signatureField.getDict().set('V', PdfString.fromString('fake-signature'));
|
||||
|
||||
return pdf.save();
|
||||
};
|
||||
|
||||
const uploadAcroFormEnvelope = async ({
|
||||
request,
|
||||
token,
|
||||
payload = ACROFORM_DOCUMENT_PAYLOAD,
|
||||
file = ACROFORM_FIXTURE,
|
||||
fileName = 'acroform-import-test.pdf',
|
||||
}: {
|
||||
request: APIRequestContext;
|
||||
token: string;
|
||||
payload?: TCreateEnvelopePayload;
|
||||
file?: Uint8Array;
|
||||
fileName?: string;
|
||||
}): Promise<TCreateEnvelopeResponse> => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
formData.append('files', new File([ACROFORM_FIXTURE], 'acroform-import-test.pdf', { type: 'application/pdf' }));
|
||||
formData.append('files', new File([file], fileName, { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -84,6 +104,23 @@ const uploadAcroFormEnvelope = async ({
|
||||
return (await res.json()) as TCreateEnvelopeResponse;
|
||||
};
|
||||
|
||||
const importAcroFormFieldsWithSession = ({
|
||||
page,
|
||||
teamId,
|
||||
envelopeId,
|
||||
}: {
|
||||
page: Page;
|
||||
teamId: number;
|
||||
envelopeId: string;
|
||||
}) =>
|
||||
page.context().request.post(`${WEBAPP_BASE_URL}/api/trpc/envelope.field.importFromPdf`, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-team-id': String(teamId),
|
||||
},
|
||||
data: JSON.stringify({ json: { envelopeId } }),
|
||||
});
|
||||
|
||||
const loadEnvelopeForImport = async (envelopeId: string) =>
|
||||
prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: envelopeId },
|
||||
@@ -237,4 +274,95 @@ test.describe('AcroForm Import', () => {
|
||||
expect(after.fields.length).toBeGreaterThanOrEqual(8);
|
||||
expect(after.fields.every((f) => f.recipientId === after.recipients[0].id)).toBe(true);
|
||||
});
|
||||
|
||||
test('import endpoint rejects template envelopes without mutating stored widgets', async ({ page, request }) => {
|
||||
const { token, user, team } = await seedUserWithApiToken();
|
||||
|
||||
const response = await uploadAcroFormEnvelope({
|
||||
request,
|
||||
token,
|
||||
payload: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'AcroForm template',
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({ page, email: user.email });
|
||||
|
||||
const res = await importAcroFormFieldsWithSession({
|
||||
page,
|
||||
teamId: team.id,
|
||||
envelopeId: response.id,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
|
||||
const after = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: response.id },
|
||||
include: {
|
||||
envelopeItems: { include: { documentData: true } },
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(after.fields).toHaveLength(0);
|
||||
|
||||
const pdfBuffer = await getFileServerSide(after.envelopeItems[0].documentData);
|
||||
|
||||
expect(await pdfHasFormFields(pdfBuffer)).toBe(true);
|
||||
});
|
||||
|
||||
test('import does not duplicate fields when signed signatures prevent flattening', async ({ request }) => {
|
||||
const { token } = await seedUserWithApiToken();
|
||||
const signedPdf = await createSignedSignatureAcroFormPdf();
|
||||
|
||||
const response = await uploadAcroFormEnvelope({
|
||||
request,
|
||||
token,
|
||||
file: signedPdf,
|
||||
fileName: 'signed-acroform.pdf',
|
||||
});
|
||||
|
||||
const envelope = await loadEnvelopeForImport(response.id);
|
||||
|
||||
const firstResult = await UNSAFE_importAcroFormFieldsFromEnvelope({
|
||||
envelope,
|
||||
apiRequestMetadata: API_REQUEST_METADATA,
|
||||
});
|
||||
|
||||
expect(firstResult.fieldsCreated).toBeGreaterThan(0);
|
||||
expect(firstResult.itemsProcessed).toBe(1);
|
||||
expect(firstResult.signedSignatureCount).toBe(1);
|
||||
|
||||
const afterFirst = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: response.id },
|
||||
include: {
|
||||
envelopeItems: { include: { documentData: true } },
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
const firstFieldCount = afterFirst.fields.length;
|
||||
|
||||
const preservedPdf = await getFileServerSide(afterFirst.envelopeItems[0].documentData);
|
||||
|
||||
expect(await pdfHasFormFields(preservedPdf)).toBe(true);
|
||||
|
||||
const secondEnvelope = await loadEnvelopeForImport(response.id);
|
||||
|
||||
const secondResult = await UNSAFE_importAcroFormFieldsFromEnvelope({
|
||||
envelope: secondEnvelope,
|
||||
apiRequestMetadata: API_REQUEST_METADATA,
|
||||
});
|
||||
|
||||
expect(secondResult.fieldsCreated).toBe(0);
|
||||
expect(secondResult.itemsProcessed).toBe(0);
|
||||
|
||||
const afterSecond = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: response.id },
|
||||
include: { fields: true },
|
||||
});
|
||||
|
||||
expect(afterSecond.fields).toHaveLength(firstFieldCount);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user