From f60a99d0bd5cc4cfffcc79e0673e4602a2afc6ca Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 2 Mar 2026 15:04:55 +0000 Subject: [PATCH] fix(tests): update route validation functions to ensure canonical success responses in import/save regression tests --- .../import-save-route-regression.spec.ts | 347 +++++++++++------- 1 file changed, 211 insertions(+), 136 deletions(-) diff --git a/tests/integration/import-save-route-regression.spec.ts b/tests/integration/import-save-route-regression.spec.ts index 67f7c3d1..f010b5d6 100644 --- a/tests/integration/import-save-route-regression.spec.ts +++ b/tests/integration/import-save-route-regression.spec.ts @@ -32,9 +32,9 @@ function expectPredictableRouteMiss(status: number): void { expect([404, 405]).toContain(status); } -function expectCanonicalNon404(status: number): void { - expect(status).not.toBe(404); - expect(status).toBeLessThan(500); +function expectCanonicalSuccess(status: number, endpoint: string): void { + expect(status, `${endpoint} should return a 2xx success response`).toBeGreaterThanOrEqual(200); + expect(status, `${endpoint} should return a 2xx success response`).toBeLessThan(300); } async function readSessionId(response: import('@playwright/test').APIResponse): Promise { @@ -44,80 +44,111 @@ async function readSessionId(response: import('@playwright/test').APIResponse): return sessionId as string; } +async function createBackupAndTrack( + page: import('@playwright/test').Page, + headers: Record, + createdBackupFilenames: string[] +): Promise { + const backupBeforeCommit = await page.request.post('/api/v1/backups', { + headers, + data: {}, + }); + expectCanonicalSuccess(backupBeforeCommit.status(), 'POST /api/v1/backups'); + + const payload = (await backupBeforeCommit.json()) as { filename?: string }; + const filename = payload.filename; + expect(filename).toBeTruthy(); + createdBackupFilenames.push(filename as string); +} + +async function cleanupCreatedBackups( + page: import('@playwright/test').Page, + headers: Record, + createdBackupFilenames: string[] +): Promise { + for (const filename of createdBackupFilenames) { + const cleanup = await page.request.delete(`/api/v1/backups/${encodeURIComponent(filename)}`, { headers }); + expectCanonicalSuccess(cleanup.status(), `DELETE /api/v1/backups/${filename}`); + } +} + test.describe('Import/Save Route Regression Coverage', () => { test('Caddy import flow stages use canonical routes and reject route drift', async ({ page, adminUser }) => { await loginUser(page, adminUser); const headers = getStorageStateAuthHeaders(); + const createdBackupFilenames: string[] = []; - await test.step('Open Caddy import page and validate route-negative probes', async () => { - await page.goto('/tasks/import/caddyfile', { waitUntil: 'domcontentloaded' }); - await expect(page.getByRole('heading', { level: 1 })).toContainText(/import/i); - await expect(page.getByRole('button', { name: /parse|review/i })).toBeVisible(); + try { + await test.step('Open Caddy import page and validate route-negative probes', async () => { + await page.goto('/tasks/import/caddyfile', { waitUntil: 'domcontentloaded' }); + await expect(page.getByRole('heading', { level: 1 })).toContainText(/import/i); + await expect(page.getByRole('button', { name: /parse|review/i })).toBeVisible(); - const wrongStatusMethod = await page.request.post('/api/v1/import/status', { - headers, - data: {}, + const wrongStatusMethod = await page.request.post('/api/v1/import/status', { + headers, + data: {}, + }); + expectPredictableRouteMiss(wrongStatusMethod.status()); + + const wrongUploadMethod = await page.request.get('/api/v1/import/upload', { headers }); + expectPredictableRouteMiss(wrongUploadMethod.status()); + + const wrongCancelMethod = await page.request.post('/api/v1/import/cancel', { + headers, + data: {}, + }); + expectPredictableRouteMiss(wrongCancelMethod.status()); }); - expectPredictableRouteMiss(wrongStatusMethod.status()); - const wrongUploadMethod = await page.request.get('/api/v1/import/upload', { headers }); - expectPredictableRouteMiss(wrongUploadMethod.status()); + await test.step('Run canonical Caddy import status/upload/cancel path', async () => { + const statusResponse = await page.request.get('/api/v1/import/status', { headers }); + expectCanonicalSuccess(statusResponse.status(), 'GET /api/v1/import/status'); - const wrongCancelMethod = await page.request.post('/api/v1/import/cancel', { - headers, - data: {}, + const uploadForCancel = await page.request.post('/api/v1/import/upload', { + headers, + data: { content: SAMPLE_CADDYFILE }, + }); + expectCanonicalSuccess(uploadForCancel.status(), 'POST /api/v1/import/upload'); + const cancelSessionId = await readSessionId(uploadForCancel); + + const cancelResponse = await page.request.delete('/api/v1/import/cancel', { + headers, + params: { session_uuid: cancelSessionId }, + }); + expectCanonicalSuccess(cancelResponse.status(), 'DELETE /api/v1/import/cancel'); }); - expectPredictableRouteMiss(wrongCancelMethod.status()); - }); - await test.step('Run canonical Caddy import status/upload/cancel path', async () => { - const statusResponse = await page.request.get('/api/v1/import/status', { headers }); - expectCanonicalNon404(statusResponse.status()); + await test.step('Run canonical Caddy preview/backup-before-commit/commit/post-state path', async () => { + const uploadForCommit = await page.request.post('/api/v1/import/upload', { + headers, + data: { content: SAMPLE_CADDYFILE }, + }); + expectCanonicalSuccess(uploadForCommit.status(), 'POST /api/v1/import/upload'); + const commitSessionId = await readSessionId(uploadForCommit); - const uploadForCancel = await page.request.post('/api/v1/import/upload', { - headers, - data: { content: SAMPLE_CADDYFILE }, + const previewResponse = await page.request.get('/api/v1/import/preview', { headers }); + expectCanonicalSuccess(previewResponse.status(), 'GET /api/v1/import/preview'); + + await createBackupAndTrack(page, headers, createdBackupFilenames); + + const commitResponse = await page.request.post('/api/v1/import/commit', { + headers, + data: { + session_uuid: commitSessionId, + resolutions: {}, + names: {}, + }, + }); + expectCanonicalSuccess(commitResponse.status(), 'POST /api/v1/import/commit'); + + const postState = await page.request.get('/api/v1/import/status', { headers }); + expectCanonicalSuccess(postState.status(), 'GET /api/v1/import/status'); }); - expectCanonicalNon404(uploadForCancel.status()); - const cancelSessionId = await readSessionId(uploadForCancel); - - const cancelResponse = await page.request.delete('/api/v1/import/cancel', { - headers, - params: { session_uuid: cancelSessionId }, + } finally { + await test.step('Cleanup created backup artifacts', async () => { + await cleanupCreatedBackups(page, headers, createdBackupFilenames); }); - expectCanonicalNon404(cancelResponse.status()); - }); - - await test.step('Run canonical Caddy preview/backup-before-commit/commit/post-state path', async () => { - const uploadForCommit = await page.request.post('/api/v1/import/upload', { - headers, - data: { content: SAMPLE_CADDYFILE }, - }); - expectCanonicalNon404(uploadForCommit.status()); - const commitSessionId = await readSessionId(uploadForCommit); - - const previewResponse = await page.request.get('/api/v1/import/preview', { headers }); - expectCanonicalNon404(previewResponse.status()); - - const backupBeforeCommit = await page.request.post('/api/v1/backups', { - headers, - data: {}, - }); - expectCanonicalNon404(backupBeforeCommit.status()); - - const commitResponse = await page.request.post('/api/v1/import/commit', { - headers, - data: { - session_uuid: commitSessionId, - resolutions: {}, - names: {}, - }, - }); - expectCanonicalNon404(commitResponse.status()); - - const postState = await page.request.get('/api/v1/import/status', { headers }); - expectCanonicalNon404(postState.status()); - }); + } }); test('NPM and JSON import critical routes pass canonical methods and reject drift', async ({ page, adminUser }) => { @@ -142,20 +173,20 @@ test.describe('Import/Save Route Regression Coverage', () => { headers, data: { content: SAMPLE_NPM_OR_JSON_EXPORT }, }); - expectCanonicalNon404(npmUploadForCancel.status()); + expectCanonicalSuccess(npmUploadForCancel.status(), 'POST /api/v1/import/npm/upload'); const npmCancelSession = await readSessionId(npmUploadForCancel); const npmCancel = await page.request.post('/api/v1/import/npm/cancel', { headers, data: { session_uuid: npmCancelSession }, }); - expectCanonicalNon404(npmCancel.status()); + expectCanonicalSuccess(npmCancel.status(), 'POST /api/v1/import/npm/cancel'); const npmUploadForCommit = await page.request.post('/api/v1/import/npm/upload', { headers, data: { content: SAMPLE_NPM_OR_JSON_EXPORT }, }); - expectCanonicalNon404(npmUploadForCommit.status()); + expectCanonicalSuccess(npmUploadForCommit.status(), 'POST /api/v1/import/npm/upload'); const npmCommitSession = await readSessionId(npmUploadForCommit); const npmCommit = await page.request.post('/api/v1/import/npm/commit', { @@ -166,7 +197,7 @@ test.describe('Import/Save Route Regression Coverage', () => { names: {}, }, }); - expectCanonicalNon404(npmCommit.status()); + expectCanonicalSuccess(npmCommit.status(), 'POST /api/v1/import/npm/commit'); }); await test.step('JSON import upload/commit/cancel with route-mismatch checks', async () => { @@ -187,20 +218,20 @@ test.describe('Import/Save Route Regression Coverage', () => { headers, data: { content: SAMPLE_NPM_OR_JSON_EXPORT }, }); - expectCanonicalNon404(jsonUploadForCancel.status()); + expectCanonicalSuccess(jsonUploadForCancel.status(), 'POST /api/v1/import/json/upload'); const jsonCancelSession = await readSessionId(jsonUploadForCancel); const jsonCancel = await page.request.post('/api/v1/import/json/cancel', { headers, data: { session_uuid: jsonCancelSession }, }); - expectCanonicalNon404(jsonCancel.status()); + expectCanonicalSuccess(jsonCancel.status(), 'POST /api/v1/import/json/cancel'); const jsonUploadForCommit = await page.request.post('/api/v1/import/json/upload', { headers, data: { content: SAMPLE_NPM_OR_JSON_EXPORT }, }); - expectCanonicalNon404(jsonUploadForCommit.status()); + expectCanonicalSuccess(jsonUploadForCommit.status(), 'POST /api/v1/import/json/upload'); const jsonCommitSession = await readSessionId(jsonUploadForCommit); const jsonCommit = await page.request.post('/api/v1/import/json/commit', { @@ -211,7 +242,7 @@ test.describe('Import/Save Route Regression Coverage', () => { names: {}, }, }); - expectCanonicalNon404(jsonCommit.status()); + expectCanonicalSuccess(jsonCommit.status(), 'POST /api/v1/import/json/commit'); }); }); @@ -220,80 +251,124 @@ test.describe('Import/Save Route Regression Coverage', () => { const headers = getStorageStateAuthHeaders(); let createdProxyUUID = ''; - await test.step('System settings save path succeeds on canonical route', async () => { - await page.goto('/settings/system', { waitUntil: 'domcontentloaded' }); - await expect(page.getByRole('heading', { name: /system settings/i })).toBeVisible(); + try { + await test.step('System settings save path succeeds on canonical route', async () => { + await page.goto('/settings/system', { waitUntil: 'domcontentloaded' }); + await expect(page.getByRole('heading', { name: /system settings/i })).toBeVisible(); - const saveButton = page.getByRole('button', { name: /save settings/i }).first(); - await expect(saveButton).toBeEnabled(); + const caddyApiInput = page.locator('#caddy-api'); + await expect(caddyApiInput).toBeVisible(); - const saveResponsePromise = page.waitForResponse( - (response) => - response.url().includes('/api/v1/settings') && - ['POST', 'PATCH'].includes(response.request().method()) - ); + const originalCaddyApi = await caddyApiInput.inputValue(); + const requestMarker = `route-regression-${Date.now()}-${Math.floor(Math.random() * 1000)}`; + const updatedCaddyApi = `http://localhost:2019?${requestMarker}=1`; + await caddyApiInput.fill(updatedCaddyApi); - await saveButton.click(); - const saveResponse = await saveResponsePromise; - expectCanonicalNon404(saveResponse.status()); + const saveButton = page.getByRole('button', { name: /save settings/i }).first(); + await expect(saveButton).toBeEnabled(); - const wrongSettingsMethod = await page.request.delete('/api/v1/settings', { headers }); - expectPredictableRouteMiss(wrongSettingsMethod.status()); - }); + const saveResponsePromise = page.waitForResponse((response) => { + const request = response.request(); + let payload: { key?: string; value?: string } | undefined; + try { + payload = request.postDataJSON() as { key?: string; value?: string }; + } catch { + return false; + } + return ( + response.url().includes('/api/v1/settings') && + request.method() === 'POST' && + payload.key === 'caddy.admin_api' && + payload.value === updatedCaddyApi + ); + }); - await test.step('Proxy-host save path succeeds on canonical route and rejects wrong method/path', async () => { - const unique = `${Date.now()}-${Math.floor(Math.random() * 1000)}`; - const createResponse = await page.request.post('/api/v1/proxy-hosts', { - headers, - data: { - name: `PR3 Route Regression ${unique}`, - domain_names: `pr3-route-${unique}.example.test`, - forward_host: 'localhost', - forward_port: 8080, - forward_scheme: 'http', - websocket_support: false, - enabled: true, - }, + await saveButton.click(); + const saveResponse = await saveResponsePromise; + expectCanonicalSuccess(saveResponse.status(), 'POST /api/v1/settings (caddy.admin_api)'); + + // Deterministic UI effect: reloading should preserve the value we just saved. + await page.reload({ waitUntil: 'domcontentloaded' }); + await expect(page.locator('#caddy-api')).toHaveValue(updatedCaddyApi); + + await caddyApiInput.fill(originalCaddyApi); + const restoreResponsePromise = page.waitForResponse((response) => { + const request = response.request(); + let payload: { key?: string; value?: string } | undefined; + try { + payload = request.postDataJSON() as { key?: string; value?: string }; + } catch { + return false; + } + return ( + response.url().includes('/api/v1/settings') && + request.method() === 'POST' && + payload.key === 'caddy.admin_api' && + payload.value === originalCaddyApi + ); + }); + await saveButton.click(); + const restoreResponse = await restoreResponsePromise; + expectCanonicalSuccess(restoreResponse.status(), 'POST /api/v1/settings (restore caddy.admin_api)'); + + const wrongSettingsMethod = await page.request.delete('/api/v1/settings', { headers }); + expectPredictableRouteMiss(wrongSettingsMethod.status()); }); - expectCanonicalNon404(createResponse.status()); - expect([200, 201]).toContain(createResponse.status()); - const created = (await createResponse.json()) as { uuid?: string }; - createdProxyUUID = created.uuid || ''; - expect(createdProxyUUID).toBeTruthy(); + await test.step('Proxy-host save path succeeds on canonical route and rejects wrong method/path', async () => { + const unique = `${Date.now()}-${Math.floor(Math.random() * 1000)}`; + const createResponse = await page.request.post('/api/v1/proxy-hosts', { + headers, + data: { + name: `PR3 Route Regression ${unique}`, + domain_names: `pr3-route-${unique}.example.test`, + forward_host: 'localhost', + forward_port: 8080, + forward_scheme: 'http', + websocket_support: false, + enabled: true, + }, + }); + expectCanonicalSuccess(createResponse.status(), 'POST /api/v1/proxy-hosts'); + expect([200, 201]).toContain(createResponse.status()); - const updateResponse = await page.request.put(`/api/v1/proxy-hosts/${createdProxyUUID}`, { - headers, - data: { - name: `PR3 Route Regression Updated ${unique}`, - domain_names: `pr3-route-${unique}.example.test`, - forward_host: 'localhost', - forward_port: 8081, - forward_scheme: 'http', - websocket_support: false, - enabled: true, - }, - }); - expectCanonicalNon404(updateResponse.status()); - - const wrongProxyMethod = await page.request.post(`/api/v1/proxy-hosts/${createdProxyUUID}`, { - headers, - data: {}, - }); - expectPredictableRouteMiss(wrongProxyMethod.status()); - - const wrongProxyPath = await page.request.put('/api/v1/proxy-host', { - headers, - data: {}, - }); - expectPredictableRouteMiss(wrongProxyPath.status()); - }); - - if (createdProxyUUID) { - await test.step('Cleanup created proxy host', async () => { - const cleanup = await page.request.delete(`/api/v1/proxy-hosts/${createdProxyUUID}`, { headers }); - expectCanonicalNon404(cleanup.status()); + const created = (await createResponse.json()) as { uuid?: string }; + createdProxyUUID = created.uuid || ''; + expect(createdProxyUUID).toBeTruthy(); + + const updateResponse = await page.request.put(`/api/v1/proxy-hosts/${createdProxyUUID}`, { + headers, + data: { + name: `PR3 Route Regression Updated ${unique}`, + domain_names: `pr3-route-${unique}.example.test`, + forward_host: 'localhost', + forward_port: 8081, + forward_scheme: 'http', + websocket_support: false, + enabled: true, + }, + }); + expectCanonicalSuccess(updateResponse.status(), `PUT /api/v1/proxy-hosts/${createdProxyUUID}`); + + const wrongProxyMethod = await page.request.post(`/api/v1/proxy-hosts/${createdProxyUUID}`, { + headers, + data: {}, + }); + expectPredictableRouteMiss(wrongProxyMethod.status()); + + const wrongProxyPath = await page.request.put('/api/v1/proxy-host', { + headers, + data: {}, + }); + expectPredictableRouteMiss(wrongProxyPath.status()); }); + } finally { + if (createdProxyUUID) { + await test.step('Cleanup created proxy host', async () => { + const cleanup = await page.request.delete(`/api/v1/proxy-hosts/${createdProxyUUID}`, { headers }); + expectCanonicalSuccess(cleanup.status(), `DELETE /api/v1/proxy-hosts/${createdProxyUUID}`); + }); + } } }); });