- 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>
199 lines
7.7 KiB
TypeScript
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 });
|
|
});
|
|
});
|