/** * ============================================================================= * UAT_NoticeCases_Services_Work -- Notice/case depth, services depth, * work detail and task operations * ============================================================================= * * Covers: * CASE-* : Notice case detail, edit, sub-pages (events, hearings, orders) * SVC-* : Services — bulk imports, bulk lock, task operations, subscriptions * WORK-* : Work detail — task status, comments, engagement detail * SEC-* : CSRF rejection and IDOR probes for all three modules * * Required .env additions: * NOTICE_CASE_A_ID= # already in main suite; also used here for sub-pages * CASE_DOCUMENT_A_ID= # a seeded notice case document id * SUBSCRIPTION_A_ID= # a seeded client service subscription id * TASK_A_ID= # a seeded service execution task id * ENGAGEMENT_A_ID= # a seeded service engagement id * * ============================================================================= */ const { test, expect } = require('@playwright/test'); require('dotenv').config(); const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); async function safeGoto(page, route) { const resp = await page.goto(route).catch(() => null); await page.waitForLoadState('domcontentloaded').catch(() => {}); return resp; } function idOr(envKey, fallback = '1') { return process.env[envKey] || fallback; } function skipIfMissing(envKey) { if (!process.env[envKey]) test.skip(true, `Set ${envKey} in .env after seeding`); } // --------------------------------------------------------------------------- // Notice cases — sub-pages not covered in existing vapt-targeted.spec.js // --------------------------------------------------------------------------- test.describe('CASE: Notice case sub-pages', () => { test('[V25-CASE-001] CASE-001 Notice case list loads', async ({ page }) => { await login(page, 'System Admin'); await safeGoto(page, '/notice-cases'); await expectNoBackendError(page); }); test('[V25-CASE-002] CASE-002 Notice case detail page loads', async ({ page }) => { skipIfMissing('NOTICE_CASE_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/notice-cases/${idOr('NOTICE_CASE_A_ID')}`); await expectNoBackendError(page); }); test('[V25-CASE-003] CASE-003 Notice case edit page loads', async ({ page }) => { skipIfMissing('NOTICE_CASE_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/notice-cases/${idOr('NOTICE_CASE_A_ID')}/edit`); await expectNoBackendError(page); }); test('[V25-CASE-004] CASE-004 Notice case events sub-page loads', async ({ page }) => { skipIfMissing('NOTICE_CASE_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/notice-cases/${idOr('NOTICE_CASE_A_ID')}/events`); await expectNoBackendError(page); }); test('[V25-CASE-005] CASE-005 Notice case hearings sub-page loads', async ({ page }) => { skipIfMissing('NOTICE_CASE_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/notice-cases/${idOr('NOTICE_CASE_A_ID')}/hearings`); await expectNoBackendError(page); }); test('[V25-CASE-006] CASE-006 Notice case orders sub-page loads', async ({ page }) => { skipIfMissing('NOTICE_CASE_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/notice-cases/${idOr('NOTICE_CASE_A_ID')}/orders`); await expectNoBackendError(page); }); test('[V25-CASE-007] CASE-007 Notice case document download requires auth', async ({ page }) => { skipIfMissing('CASE_DOCUMENT_A_ID'); // test as anonymous const resp = await safeGoto(page, `/notice-cases/documents/${idOr('CASE_DOCUMENT_A_ID')}/download`); const body = await readBody(page); await blockedOrNotFound(resp, body); }); test('[V25-CASE-008] CASE-008 Notice case document delete CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('CASE_DOCUMENT_A_ID'); const resp = await request.post( `${BASE_URL}/notice-cases/documents/${idOr('CASE_DOCUMENT_A_ID')}/delete`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-CASE-009] CASE-009 Notice case upload CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('NOTICE_CASE_A_ID'); const resp = await request.post( `${BASE_URL}/notice-cases/${idOr('NOTICE_CASE_A_ID')}/documents/upload`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-CASE-010] CASE-010 Client cannot access notice cases', async ({ page }) => { await login(page, 'Client'); const resp = await safeGoto(page, '/notice-cases'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); test('[V25-CASE-011] CASE-011 IDOR: cross-case document download blocked', async ({ page }) => { await login(page, 'Staff'); const resp = await safeGoto(page, '/notice-cases/999999999/documents/999999/download'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); test('[V25-CASE-012] CASE-012 Non-existent case detail returns safe response', async ({ page }) => { await login(page, 'System Admin'); const resp = await safeGoto(page, '/notice-cases/999999999'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); }); // --------------------------------------------------------------------------- // Services — bulk imports, bulk lock, subscription ops // --------------------------------------------------------------------------- test.describe('SVC: Services depth', () => { test('[V25-SVC-001] SVC-001 Bulk imports page loads', async ({ page }) => { await login(page, 'Firm Admin'); await safeGoto(page, '/services/bulk-imports'); await expectNoBackendError(page); }); test('[V25-SVC-002] SVC-002 Bulk imports service-master template downloads', async ({ page }) => { await login(page, 'Firm Admin'); const [download] = await Promise.all([ page.waitForEvent('download'), page.goto('/services/bulk-imports/templates/service-master.xlsx').catch(() => null), ]); expect(await download.suggestedFilename()).toMatch(/service.*template|xlsx/i); }); test('[V25-SVC-003] SVC-003 Bulk imports engagement-assignments template downloads', async ({ page }) => { await login(page, 'Firm Admin'); const [download] = await Promise.all([ page.waitForEvent('download'), page.goto('/services/bulk-imports/templates/engagement-assignments.xlsx').catch(() => null), ]); expect(await download.suggestedFilename()).toMatch(/assignment|template|xlsx/i); }); test('[V25-SVC-004] SVC-004 Bulk imports firm-task-templates template downloads', async ({ page }) => { await login(page, 'Firm Admin'); const [download] = await Promise.all([ page.waitForEvent('download'), page.goto('/services/bulk-imports/templates/firm-task-templates.xlsx').catch(() => null), ]); expect(await download.suggestedFilename()).toMatch(/task|template|xlsx/i); }); test('[V25-SVC-005] SVC-005 Bulk import preview CSRF-less POST is rejected', async ({ request }) => { const resp = await request.post( `${BASE_URL}/services/bulk-imports/service-master`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-SVC-006] SVC-006 Subscription detail loads', async ({ page }) => { skipIfMissing('SUBSCRIPTION_A_ID'); await login(page, 'Firm Admin'); await safeGoto(page, `/services/${idOr('SUBSCRIPTION_A_ID')}`); await expectNoBackendError(page); }); test('[V25-SVC-007] SVC-007 Subscription edit page loads', async ({ page }) => { skipIfMissing('SUBSCRIPTION_A_ID'); await login(page, 'Firm Admin'); await safeGoto(page, `/services/${idOr('SUBSCRIPTION_A_ID')}/edit`); await expectNoBackendError(page); }); test('[V25-SVC-008] SVC-008 Subscription lock CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('SUBSCRIPTION_A_ID'); const resp = await request.post( `${BASE_URL}/services/${idOr('SUBSCRIPTION_A_ID')}/lock`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-SVC-009] SVC-009 Bulk lock CSRF-less POST is rejected', async ({ request }) => { const resp = await request.post( `${BASE_URL}/services/bulk-lock`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-SVC-010] SVC-010 Subscription generate CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('SUBSCRIPTION_A_ID'); const resp = await request.post( `${BASE_URL}/services/subscriptions/${idOr('SUBSCRIPTION_A_ID')}/generate`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-SVC-011] SVC-011 Client cannot access services admin', async ({ page }) => { await login(page, 'Client'); const resp = await safeGoto(page, '/services/bulk-imports'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); test('[V25-SVC-012] SVC-012 Non-existent subscription ID returns safe response', async ({ page }) => { await login(page, 'Firm Admin'); const resp = await safeGoto(page, '/services/999999999'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); test('[V25-SVC-013] SVC-013 Task bulk-update CSRF-less POST is rejected', async ({ request }) => { const resp = await request.post( `${BASE_URL}/services/tasks/bulk-update`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); }); // --------------------------------------------------------------------------- // Work detail — task operations // --------------------------------------------------------------------------- test.describe('WORK: Work detail and task operations', () => { test('[V25-WORK-001] WORK-001 Engagement work detail page loads', async ({ page }) => { skipIfMissing('ENGAGEMENT_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/work/engagements/${idOr('ENGAGEMENT_A_ID')}`); await expectNoBackendError(page); }); test('[V25-WORK-002] WORK-002 Task status update CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('TASK_A_ID'); const resp = await request.post( `${BASE_URL}/work/tasks/${idOr('TASK_A_ID')}/status`, { form: { status: 'completed', csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-WORK-003] WORK-003 Task comment POST CSRF-less is rejected', async ({ request }) => { skipIfMissing('TASK_A_ID'); const resp = await request.post( `${BASE_URL}/work/tasks/${idOr('TASK_A_ID')}/comment`, { form: { message: 'test comment', csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-WORK-004] WORK-004 Task comments list page loads', async ({ page }) => { skipIfMissing('TASK_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/services/tasks/${idOr('TASK_A_ID')}/comments`); await expectNoBackendError(page); }); test('[V25-WORK-005] WORK-005 Task edit page loads', async ({ page }) => { skipIfMissing('TASK_A_ID'); await login(page, 'System Admin'); await safeGoto(page, `/services/tasks/${idOr('TASK_A_ID')}/edit`); await expectNoBackendError(page); }); test('[V25-WORK-006] WORK-006 Task upload CSRF-less POST is rejected', async ({ request }) => { skipIfMissing('TASK_A_ID'); const resp = await request.post( `${BASE_URL}/documents/tasks/${idOr('TASK_A_ID')}/upload`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-WORK-007] WORK-007 Non-existent task returns safe response', async ({ page }) => { await login(page, 'System Admin'); const resp = await safeGoto(page, '/services/tasks/999999999/edit'); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); test('[V25-WORK-008] WORK-008 IDOR: Staff cannot update task assigned to another user via direct POST', async ({ request }) => { skipIfMissing('TASK_A_ID'); // Raw API attempt without valid session for a different user const resp = await request.post( `${BASE_URL}/work/tasks/${idOr('TASK_A_ID')}/status`, { form: { status: 'completed', csrf_token: 'invalid' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); expect([400, 401, 403, 422].includes(resp.status())).toBeTruthy(); }); test('[V25-WORK-009] WORK-009 Client cannot access work detail', async ({ page }) => { skipIfMissing('ENGAGEMENT_A_ID'); await login(page, 'Client'); const resp = await safeGoto(page, `/work/engagements/${idOr('ENGAGEMENT_A_ID')}`); await expectNoBackendError(page); await expectBlockedOrSafe(page, resp); }); });