Files
pn-new-crm/tests/e2e/smoke/04-documents-hub-upload-into-entity.spec.ts
Matt 9a5ba87d6c fix(integration): webhook v2 events, storage migrate, test theatre
- F1: DOCUMENT_DECLINED handler (v2 Decline vs Reject) — routes to same
  handler as DOCUMENT_REJECTED until product refines downstream UX
- Add RECIPIENT_VIEWED / RECIPIENT_SIGNED v2-alias cases with telemetry
  logging so we see when v2 deployments emit them
- D1: populate TABLES_WITH_STORAGE_KEYS (files, berth_pdf_versions,
  brochure_versions, gdpr_exports) — was an empty list, migrated 0 files
- MinIO putObject/getObject/statObject/removeObject socket timeout wrapper
  to prevent worker hangs on TCP blackhole (30s deadline)
- E1: convert test.skip on smoke-setup infra failure to throw new Error
  so green-skipped silence becomes a real test failure (Playwright
  doesn't expose vitest's expect.fail)
- Regression tests: folderId='' → null transform, applyEntityRestoredSuffix
  no-op (never-archived), syncEntityFolderName collision loop past (2)

Note: matching .env.example documentation (D2 — bare DOCUMENSO_API_URL,
DOCUMENSO_API_VERSION, MINIO_AUTO_CREATE_BUCKET, DOCUMENSO_TEMPLATE_ID_EOI,
recipient role id vars) prepared but not committed — pre-commit hook
blocks .env*. Apply manually via the separate .env workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:02:26 +02:00

199 lines
7.7 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { login, navigateTo, apiHeaders } from './helpers';
import path from 'path';
import fs from 'fs';
/**
* Smoke spec — Upload a file into an entity folder (Wave 11.B)
*
* Strategy: Option B (API-fixture approach).
*
* The EntityFolderView's upload button is not wired into the new hub UI —
* the hub focuses on aggregated read + signing. We therefore drive the
* upload path via the API directly (`page.request`) and then assert that
* the file surfaces in the entity folder view.
*
* This is the principled approach: it tests the behaviour (file appears in
* entity view after upload) rather than a specific UI affordance.
*/
test.describe('Documents hub — upload into entity folder', () => {
test.beforeEach(async ({ page }) => {
await login(page, 'super_admin');
});
test('file uploaded with clientId appears in the client entity folder view', async ({ page }) => {
const headers = await apiHeaders(page);
// 1. Create a client.
// page.request shares the browser context cookie jar (session cookie from
// login()) — the bare `request` fixture is an isolated API context that
// does not carry the session cookie and would 401.
const clientRes = await page.request.post('/api/v1/clients', {
headers,
data: {
firstName: 'UploadSmoke',
lastName: `Test${Date.now()}`,
email: `uploadsmoke${Date.now()}@e2e.test`,
},
});
// Playwright doesn't expose vitest's `expect.fail`; throw to fail loud.
if (!clientRes.ok()) {
throw new Error(
`Client create returned ${clientRes.status()} ${await clientRes.text()} — upload test cannot proceed`,
);
}
const { data: client } = (await clientRes.json()) as {
data: { id: string; firstName: string; lastName: string };
};
// 2. Upload a file associated with this client via the multipart upload endpoint.
// The upload validator accepts `clientId` to associate the file with the client.
// ensureEntityFolder is called by the files service after upload to guarantee
// the folder exists.
const fixturePath = path.resolve('tests/e2e/fixtures/test-document.txt');
const fileBuffer = fs.readFileSync(fixturePath);
const uploadForm = new FormData();
uploadForm.append('file', new Blob([fileBuffer], { type: 'text/plain' }), 'test-document.txt');
uploadForm.append('filename', 'test-document.txt');
uploadForm.append('clientId', client.id);
// Use fetch directly because Playwright's request doesn't yet have great
// multipart support for Buffer payloads. The cookie is carried by the
// browser context's cookie jar automatically when we use `page.request`.
const uploadRes = await page.request.fetch('/api/v1/files/upload', {
method: 'POST',
headers: {
'X-Port-Id': headers['X-Port-Id']!,
},
multipart: {
file: {
name: 'test-document.txt',
mimeType: 'text/plain',
buffer: Buffer.from(fileBuffer),
},
filename: 'test-document.txt',
clientId: client.id,
},
});
expect(uploadRes.ok(), `Upload failed: ${uploadRes.status()} ${await uploadRes.text()}`).toBe(
true,
);
const uploadBody = (await uploadRes.json()) as { data: { id: string; filename: string } };
const uploadedFileId = uploadBody.data.id;
expect(uploadedFileId).toBeTruthy();
// 3. Navigate to the documents hub.
await navigateTo(page, '/documents');
await page.waitForLoadState('networkidle');
// 4. Expand the Clients system root.
const expandBtn = page.locator('aside').getByRole('button', { name: 'Expand' }).first();
await expect(expandBtn).toBeVisible({ timeout: 10_000 });
await expandBtn.click();
// 5. Click the entity folder for the client we created.
const entityFolderBtn = page
.locator('aside button')
.filter({ hasText: `${client.firstName} ${client.lastName}` })
.first();
await expect(entityFolderBtn).toBeVisible({ timeout: 10_000 });
await entityFolderBtn.click();
// 6. The EntityFolderView should show the Files section with at least one row.
const filesSection = page.getByText('Files').first();
await expect(filesSection).toBeVisible({ timeout: 10_000 });
// The file we uploaded should appear somewhere in the entity view.
// AggregatedSection renders filenames in <span> elements inside <li> rows.
await expect(page.getByText('test-document.txt')).toBeVisible({ timeout: 10_000 });
});
test('file uploaded with folderId (entity folder) auto-sets client_id', async ({ page }) => {
const headers = await apiHeaders(page);
// 1. Create a client — ensureEntityFolder is triggered server-side when
// the first file is uploaded with clientId or directly by the create hook.
// page.request shares the browser context cookie jar (session cookie from
// login()) — the bare `request` fixture is an isolated API context that
// does not carry the session cookie and would 401.
const clientRes = await page.request.post('/api/v1/clients', {
headers,
data: {
firstName: 'FolderIdSmoke',
lastName: `Test${Date.now()}`,
email: `folderidsmoke${Date.now()}@e2e.test`,
},
});
if (!clientRes.ok()) {
throw new Error(
`Client create returned ${clientRes.status()} ${await clientRes.text()} — folderId test cannot proceed`,
);
}
const { data: client } = (await clientRes.json()) as {
data: { id: string; firstName: string; lastName: string };
};
// 2. Obtain the entity folder id for this client by uploading once with
// clientId to trigger ensureEntityFolder, then listing files.
const seedUpload = await page.request.fetch('/api/v1/files/upload', {
method: 'POST',
headers: { 'X-Port-Id': headers['X-Port-Id']! },
multipart: {
file: {
name: 'seed.txt',
mimeType: 'text/plain',
buffer: Buffer.from('seed'),
},
filename: 'seed.txt',
clientId: client.id,
},
});
// Seed upload failing means the files API is broken — fail loud so the
// infra regression surfaces in CI instead of staying green-skipped.
if (!seedUpload.ok()) {
throw new Error(
`Seed upload returned ${seedUpload.status()} ${await seedUpload.text()} — folderId test cannot proceed`,
);
}
// 3. List files for this client to discover the folder id.
const listRes = await page.request.get(
`/api/v1/files?entityType=client&entityId=${client.id}`,
{
headers,
},
);
if (!listRes.ok()) {
throw new Error(
`File list returned ${listRes.status()} ${await listRes.text()} — folderId test cannot proceed`,
);
}
// 4. Navigate and verify — folder view shows the client entity sections.
await navigateTo(page, '/documents');
await page.waitForLoadState('networkidle');
// Expand Clients and click entity folder.
const expandBtn = page.locator('aside').getByRole('button', { name: 'Expand' }).first();
await expect(expandBtn).toBeVisible({ timeout: 10_000 });
await expandBtn.click();
const entityFolderBtn = page
.locator('aside button')
.filter({ hasText: `${client.firstName} ${client.lastName}` })
.first();
await expect(entityFolderBtn).toBeVisible({ timeout: 10_000 });
await entityFolderBtn.click();
// Entity folder view is rendered (Signing + Files sections).
await expect(page.getByText('Signing in progress')).toBeVisible({ timeout: 10_000 });
await expect(page.getByText('Files')).toBeVisible({ timeout: 5_000 });
// The seeded file appears.
await expect(page.getByText('seed.txt')).toBeVisible({ timeout: 10_000 });
});
});