diff --git a/.env.example b/.env.example index 922ce40..31b28a7 100644 --- a/.env.example +++ b/.env.example @@ -141,9 +141,9 @@ BRANCH_A_CODE=UAT-BA BRANCH_B_CODE=UAT-BB TENANT_B_NAME=UAT Tenant B -TENANT_B_ADMIN_EMAIL=uat.firmadmin@vavalam.com +TENANT_B_ADMIN_EMAIL=uat.firmadminb@vavalam.com TENANT_B_ADMIN_PASSWORD=Password@123 -TENANT_B_ADMIN_IMAP_USER=uat.firmadmin@vavalam.com +TENANT_B_ADMIN_IMAP_USER=uat.firmadminb@vavalam.com TENANT_B_ADMIN_IMAP_PASSWORD= # ========================= @@ -186,3 +186,34 @@ CONTEXT_HEADER_SECRET= MASTER_EXCEL=Audit_Firm_ERP_Master_UAT_VAPT_Checklist.xlsx UPDATED_EXCEL=results/Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2_4_Results.xlsx API_CHECK_RESULTS_FILE=results/api-check-results.json + + +# ========================= +# V2.5 OPTIONAL DIRECT IDS +# These are only needed for direct-ID/deep module tests. If blank, affected tests skip gracefully. +# ========================= +EMPLOYEE_A_ID= +EMPLOYEE_B_ID= +PAYROLL_RUN_ID= +SALARY_STRUCTURE_ID= +LEAVE_TYPE_ID= +LEAVE_REQUEST_ID= +ATTENDANCE_ID= +REG_REQUEST_ID= +ONBOARDING_TASK_ID= +OFFBOARDING_REQ_ID= +EMP_DOCUMENT_ID= +INVOICE_A_ID= +PAYMENT_A_ID= +PARTNER_TASK_A_ID= +CONSULTANT_A_ID= +CONSULTANT_B_ID= +CONVERSION_REQUEST_ID= +SERVICE_REQUEST_ID= +CONSULTANT_DOCUMENT_ID= +DOCUMENT_A_ID= +DOCUMENT_B_ID= +CASE_DOCUMENT_ID= +SUBSCRIPTION_A_ID= +TASK_A_ID= +WORK_ITEM_A_ID= diff --git a/.gitignore b/.gitignore index 63a19ce..67f7c54 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ results/ playwright-report/ test-results/ *.log +*.sqlite +*.db diff --git a/Dockerfile b/Dockerfile index 45d761a..9b1ccd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,11 @@ COPY . . RUN mkdir -p results test-results playwright-report -CMD ["bash", "-lc", "node run-api-checks.js && npx playwright test --reporter=list --workers=1; npm run report:json; npm run update:excel; echo 'Tests completed. Starting result server.'; node scripts/result-server.js"] \ No newline at end of file +# SQLite-first execution flow: +# 1. API checks +# 2. Import test matrix into SQLite +# 3. Run Playwright; custom reporter records every test result immediately in SQLite +# 4. Export SQLite to JSON and Excel +# 5. Update original master Excel from merged JSON +# 6. Start single-page result download server on port 3000 +CMD ["bash", "-lc", "node run-api-checks.js; npm run db:import-cases; npx playwright test --workers=1; npm run db:summary; npm run db:export-json; npm run db:export-excel; npm run update:excel; echo 'Tests completed. Starting result server.'; node scripts/result-server.js"] diff --git a/MERGE_NOTES_v2.5_SQLite.md b/MERGE_NOTES_v2.5_SQLite.md new file mode 100644 index 0000000..2cb1f2c --- /dev/null +++ b/MERGE_NOTES_v2.5_SQLite.md @@ -0,0 +1,48 @@ +# v2.5 SQLite Merged Suite + +This package merges the v2.4.2 SQLite-first base suite with Claude's v2.5 new-module Playwright tests. + +## Coverage retained + +- v2.4.2 generated Excel/JSON matrix: 1296 variants. +- v2.5 new module tests added: 142 tests. +- Expected SQLite `test_cases` after import: 1438 rows. + +## How the merge avoids duplicate IDs + +Some v2.5 tests had IDs that overlap existing matrix IDs, especially `SVC-001`, `DOC-001`, etc. To avoid collisions, every v2.5 test is namespaced in its Playwright title and SQLite `variant_id` as: + +```text +V25- +``` + +Example: + +```text +[V25-SVC-001] SVC-001 Bulk imports page loads +``` + +## SQLite-first flow + +```text +data/generated-test-matrix.json ++ data/v2_5_test_cases.json + ↓ +SQLite test_cases + ↓ +Playwright execution + ↓ +SQLite test_results / test_attachments + ↓ +JSON + Excel exports + download dashboard +``` + +## Important command + +Do not override reporters with `--reporter=list`. Run: + +```bash +npx playwright test --workers=1 +``` + +The SQLite reporter will record every test as it completes. diff --git a/SEED_V2_5_1_README.md b/SEED_V2_5_1_README.md new file mode 100644 index 0000000..bdaa84f --- /dev/null +++ b/SEED_V2_5_1_README.md @@ -0,0 +1,43 @@ +# v2.5.1 Deep Seed Extension + +This suite now includes a seed extension for the additional v2.5.1 deep test cases. + +## Files + +```text +seed/seed_uat_data_v2_5_1.py +``` + +## Run order inside ERP container + +Copy the file into the ERP app under `scripts/` and run: + +```bash +cd /app +PYTHONPATH=/app python scripts/seed_uat_data.py +PYTHONPATH=/app python scripts/seed_uat_data_v2_5_1.py +``` + +The first command is your existing core seed. The second command adds supporting data for the new v2.5.1 tests. + +## What it seeds + +- Email settings, templates, queue, logs and inbox sample +- Marketplace/public leads including converted lead +- Platform billing plan, account, subscription, invoice and payment +- RBAC limited role and UAT permissions +- Audit log sample events +- Locked previous financial year and backup export placeholder +- Client-visible, deleted and multi-version document samples +- Storage-agent security node/job +- Attendance, leave balance, leave request, payroll run and payslip +- Draft, issued and cancelled billing invoices with payment +- Deep notice case with hearing/event and client/internal note separation + +## Idempotency + +The extension uses UAT-coded lookup values and can be rerun safely. If a table or column is not present in your current ERP build, it skips that section and prints a warning. + +## Important + +This is for UAT only. Do not run against live production data. diff --git a/SQLITE_RESULTS_README.md b/SQLITE_RESULTS_README.md new file mode 100644 index 0000000..52c8d22 --- /dev/null +++ b/SQLITE_RESULTS_README.md @@ -0,0 +1,89 @@ +# SQLite-first UAT/VAPT Results + +This build records Playwright UAT/VAPT execution directly into SQLite. + +## Output files + +After a run, the important files are: + +```text +results/uat_vapt_results.sqlite +results/UAT_VAPT_SQLite_Export.xlsx +results/merged-results.json +results/Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2_4_Results.xlsx +results/playwright-results.json +``` + +## How it works + +1. `data/generated-test-matrix.json` is imported into SQLite table `test_cases`. +2. A new row is created in `test_runs`. +3. Every Playwright test result is inserted immediately into `test_results` by `reporters/sqlite-reporter.js`. +4. Screenshots, traces, and videos are recorded in `test_attachments`. +5. SQLite is exported to JSON and Excel after the run. + +## Important command + +Use this command, without overriding the reporter: + +```bash +cd /tests +node run-api-checks.js +npm run db:import-cases +npx playwright test --workers=1 +npm run db:summary +npm run db:export-json +npm run db:export-excel +npm run update:excel +node scripts/result-server.js +``` + +Do not run with `--reporter=list`, because that disables the configured SQLite/json/html reporters. + +## Query SQLite + +Summary: + +```bash +sqlite3 results/uat_vapt_results.sqlite "SELECT status, COUNT(*) FROM test_results GROUP BY status;" +``` + +Latest failures: + +```bash +sqlite3 results/uat_vapt_results.sqlite " +SELECT tc.variant_id, tc.module, tc.scenario, tr.status, substr(tr.error_message,1,250) AS error +FROM test_results tr +LEFT JOIN test_cases tc ON tc.variant_id = tr.variant_id +WHERE tr.run_id = (SELECT MAX(id) FROM test_runs) + AND tr.status != 'passed' +ORDER BY tc.module, tc.variant_id +LIMIT 100; +" +``` + +Not-run cases: + +```bash +sqlite3 results/uat_vapt_results.sqlite " +SELECT tc.variant_id, tc.module, tc.scenario +FROM test_cases tc +LEFT JOIN test_results tr + ON tr.variant_id = tc.variant_id + AND tr.run_id = (SELECT MAX(id) FROM test_runs) +WHERE tr.id IS NULL AND tc.automation != 'manual' +ORDER BY tc.variant_id; +" +``` + +## Result dashboard + +The Dockerfile starts `scripts/result-server.js` on port `3000` after tests complete. In Coolify expose port `3000` and protect it using Basic Auth or IP restriction. + +Suggested persistent volumes: + +```text +/tests/results +/tests/test-results +/tests/playwright-report +``` diff --git a/V2_5_1_ADDITIONAL_TESTS.md b/V2_5_1_ADDITIONAL_TESTS.md new file mode 100644 index 0000000..f3249bd --- /dev/null +++ b/V2_5_1_ADDITIONAL_TESTS.md @@ -0,0 +1,33 @@ +# v2.5.1 Deep Gap Tests Added + +This release extends the SQLite-first v2.5 merged suite with additional ERP gap tests. + +Added automated cases: 302 + +New spec files: +- tests/api-auth-rbac.spec.js (35 cases) +- tests/audit-logs.spec.js (20 cases) +- tests/billing-business-rules.spec.js (27 cases) +- tests/document-security-deep.spec.js (28 cases) +- tests/email-integration.spec.js (26 cases) +- tests/employee-hr-business-rules.spec.js (20 cases) +- tests/marketplace-leads.spec.js (22 cases) +- tests/notice-case-business-rules.spec.js (23 cases) +- tests/platform-billing.spec.js (35 cases) +- tests/system-settings-tenancy.spec.js (36 cases) +- tests/work-lifecycle-e2e.spec.js (30 cases) + +SQLite import now loads both: +- data/v2_5_test_cases.json +- data/v2_5_1_test_cases.json + +Do not run with `--reporter=list`; allow `playwright.config.js` to load the SQLite reporter. + +Recommended run: +```bash +npm run db:import-cases +npx playwright test --workers=1 +npm run db:summary +npm run db:export-json +npm run db:export-excel +``` diff --git a/data/summary.json b/data/summary.json index 10556a2..8c02b6f 100644 --- a/data/summary.json +++ b/data/summary.json @@ -1,5 +1,5 @@ { - "suiteVersion": "2.4.0", + "suiteVersion": "2.5.0-sqlite-merged", "sourceRows": 272, "generatedVariants": 1296, "automatedVariants": 1291, @@ -9,5 +9,32 @@ "fyLockBackupStatus": "UNTESTED - all test.fixme(), require human verification before counting as coverage", "checklistReconciled": true, "checklistFile": "Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2.4.xlsx", - "totalChecklistRows": 302 -} + "totalChecklistRows": 302, + "v25NewSpecs": { + "employees": { + "file": "tests/employees.spec.js", + "tests": 53, + "status": "runnable-sqlite-recorded" + }, + "partners_billing": { + "file": "tests/partners-billing.spec.js", + "tests": 25, + "status": "runnable-sqlite-recorded" + }, + "consultants_documents": { + "file": "tests/consultants-documents.spec.js", + "tests": 30, + "status": "runnable-sqlite-recorded" + }, + "noticecases_services_work": { + "file": "tests/noticecases-services-work.spec.js", + "tests": 34, + "status": "runnable-sqlite-recorded" + } + }, + "v25TotalNewTests": 142, + "sqliteTotalImportedCasesExpected": 1438, + "v2_5_1_additional_cases": 302, + "v2_5_1_additional_unique_variant_ids": 302, + "total_expected_cases_after_v2_5_1": 1740 +} \ No newline at end of file diff --git a/data/v2_5_1_test_cases.json b/data/v2_5_1_test_cases.json new file mode 100644 index 0000000..b2800a1 --- /dev/null +++ b/data/v2_5_1_test_cases.json @@ -0,0 +1,6042 @@ +[ + { + "sourceId": "EMAIL-001", + "variantId": "V251-EMAIL-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email settings page loads", + "steps": "Execute GET /email/settings as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/settings", + "variantName": "Email settings page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-002", + "variantId": "V251-EMAIL-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email test settings endpoint visible/safe", + "steps": "Execute GET /email/settings/test as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/settings/test", + "variantName": "Email test settings endpoint visible/safe", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-003", + "variantId": "V251-EMAIL-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email logs page loads", + "steps": "Execute GET /email/logs as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/logs", + "variantName": "Email logs page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-004", + "variantId": "V251-EMAIL-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue page loads", + "steps": "Execute GET /email/queue as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/queue", + "variantName": "Email queue page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-005", + "variantId": "V251-EMAIL-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email inbox page loads", + "steps": "Execute GET /email/inbox as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/inbox", + "variantName": "Email inbox page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-006", + "variantId": "V251-EMAIL-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email templates page loads", + "steps": "Execute GET /email/templates as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/templates", + "variantName": "Email templates page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-007", + "variantId": "V251-EMAIL-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email template detail invalid ID safe", + "steps": "Execute GET /email/templates/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/templates/999999", + "variantName": "Email template detail invalid ID safe", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-008", + "variantId": "V251-EMAIL-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email inbox invalid message detail safe", + "steps": "Execute GET /email/inbox/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/inbox/999999", + "variantName": "Email inbox invalid message detail safe", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-009", + "variantId": "V251-EMAIL-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "SMTP settings CSRF-less save rejected", + "steps": "Execute POST /email/settings as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/settings", + "variantName": "SMTP settings CSRF-less save rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-010", + "variantId": "V251-EMAIL-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "SMTP test CSRF-less POST rejected", + "steps": "Execute POST /email/settings/test as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/settings/test", + "variantName": "SMTP test CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-011", + "variantId": "V251-EMAIL-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email queue process CSRF-less POST rejected", + "steps": "Execute POST /email/queue/process as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/queue/process", + "variantName": "Email queue process CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-012", + "variantId": "V251-EMAIL-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox fetch CSRF-less POST rejected", + "steps": "Execute POST /email/inbox/fetch as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/inbox/fetch", + "variantName": "Inbox fetch CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-013", + "variantId": "V251-EMAIL-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox map-all CSRF-less POST rejected", + "steps": "Execute POST /email/inbox/map-all as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/inbox/map-all", + "variantName": "Inbox map-all CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-014", + "variantId": "V251-EMAIL-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox message map CSRF-less POST rejected", + "steps": "Execute POST /email/inbox/999999/map as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/inbox/999999/map", + "variantName": "Inbox message map CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-015", + "variantId": "V251-EMAIL-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email template save CSRF-less POST rejected", + "steps": "Execute POST /email/templates as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/templates", + "variantName": "Email template save CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-016", + "variantId": "V251-EMAIL-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email template update invalid ID CSRF-less POST rejected", + "steps": "Execute POST /email/templates/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/templates/999999", + "variantName": "Email template update invalid ID CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "sourceId": "EMAIL-017", + "variantId": "V251-EMAIL-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Staff", + "scenario": "Staff cannot access email settings", + "steps": "Execute GET /email/settings as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/settings", + "variantName": "Staff cannot access email settings", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "EMAIL-018", + "variantId": "V251-EMAIL-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Client", + "scenario": "Client cannot access email logs", + "steps": "Execute GET /email/logs as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/logs", + "variantName": "Client cannot access email logs", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "EMAIL-019", + "variantId": "V251-EMAIL-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Consultant", + "scenario": "Consultant cannot access email inbox", + "steps": "Execute GET /email/inbox as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/inbox", + "variantName": "Consultant cannot access email inbox", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "EMAIL-020", + "variantId": "V251-EMAIL-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Partner", + "scenario": "Partner email queue access is blocked or safe", + "steps": "Execute GET /email/queue as Partner and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/queue", + "variantName": "Partner email queue access is blocked or safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Partner" + }, + { + "sourceId": "EMAIL-021", + "variantId": "V251-EMAIL-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Invalid email attachment download does not crash", + "steps": "Execute GET /email/inbox/999999/attachments/999999/download as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/inbox/999999/attachments/999999/download", + "variantName": "Invalid email attachment download does not crash", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-022", + "variantId": "V251-EMAIL-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email logs do not expose SMTP password", + "steps": "Execute GET /email/logs as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/logs", + "variantName": "Email logs do not expose SMTP password", + "variantType": "security", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-023", + "variantId": "V251-EMAIL-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email settings page does not render SMTP password in clear text", + "steps": "Execute GET /email/settings as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/email/settings", + "variantName": "Email settings page does not render SMTP password in clear text", + "variantType": "security", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-024", + "variantId": "V251-EMAIL-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue invalid retry action safe", + "steps": "Execute GET /email/queue/999999/retry as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/queue/999999/retry", + "variantName": "Email queue invalid retry action safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-025", + "variantId": "V251-EMAIL-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue invalid delete action safe", + "steps": "Execute GET /email/queue/999999/delete as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/queue/999999/delete", + "variantName": "Email queue invalid delete action safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "EMAIL-026", + "variantId": "V251-EMAIL-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email template preview invalid ID safe", + "steps": "Execute GET /email/templates/999999/preview as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/email/templates/999999/preview", + "variantName": "Email template preview invalid ID safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-001", + "variantId": "V251-MKT-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Marketplace dashboard loads", + "steps": "Execute GET /marketplace as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/marketplace", + "variantName": "Marketplace dashboard loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-002", + "variantId": "V251-MKT-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Leads list loads", + "steps": "Execute GET /marketplace/leads as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/marketplace/leads", + "variantName": "Leads list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-003", + "variantId": "V251-MKT-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "New lead page loads", + "steps": "Execute GET /marketplace/leads/new as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/marketplace/leads/new", + "variantName": "New lead page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-004", + "variantId": "V251-MKT-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Public", + "scenario": "Public lead page loads without auth", + "steps": "Execute GET /marketplace/public-lead as Public and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/marketplace/public-lead", + "variantName": "Public lead page loads without auth", + "variantType": "positive", + "automation": "automated", + "_kind": "public", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-005", + "variantId": "V251-MKT-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Invalid lead detail safe", + "steps": "Execute GET /marketplace/leads/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999", + "variantName": "Invalid lead detail safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-006", + "variantId": "V251-MKT-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead assign invalid ID safe", + "steps": "Execute GET /marketplace/leads/999999/assign as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/assign", + "variantName": "Lead assign invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-007", + "variantId": "V251-MKT-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead status invalid ID safe", + "steps": "Execute GET /marketplace/leads/999999/status as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/status", + "variantName": "Lead status invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-008", + "variantId": "V251-MKT-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead convert invalid ID safe", + "steps": "Execute GET /marketplace/leads/999999/convert-client as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/convert-client", + "variantName": "Lead convert invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "MKT-009", + "variantId": "V251-MKT-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Staff", + "scenario": "Staff lead list access blocked or safe", + "steps": "Execute GET /marketplace/leads as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads", + "variantName": "Staff lead list access blocked or safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "MKT-010", + "variantId": "V251-MKT-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Client", + "scenario": "Client lead list access blocked", + "steps": "Execute GET /marketplace/leads as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads", + "variantName": "Client lead list access blocked", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "MKT-011", + "variantId": "V251-MKT-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Consultant", + "scenario": "Consultant internal leads access blocked", + "steps": "Execute GET /marketplace/leads as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads", + "variantName": "Consultant internal leads access blocked", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "MKT-012", + "variantId": "V251-MKT-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Public", + "scenario": "Public route does not expose internal list", + "steps": "Execute GET /marketplace/public-lead as Public and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/public-lead", + "variantName": "Public route does not expose internal list", + "variantType": "security", + "automation": "automated", + "_kind": "public-no-sensitive", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-013", + "variantId": "V251-MKT-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Public lead blank CSRF-less POST handled safely", + "steps": "Execute GET /marketplace/public-lead as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/public-lead", + "variantName": "Public lead blank CSRF-less POST handled safely", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-014", + "variantId": "V251-MKT-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Internal lead create CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/new as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/new", + "variantName": "Internal lead create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-015", + "variantId": "V251-MKT-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead assign CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/999999/assign as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/assign", + "variantName": "Lead assign CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-016", + "variantId": "V251-MKT-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead status CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/999999/status as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/status", + "variantName": "Lead status CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-017", + "variantId": "V251-MKT-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead convert-client CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/999999/convert-client as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/convert-client", + "variantName": "Lead convert-client CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-018", + "variantId": "V251-MKT-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead duplicate check invalid POST safe", + "steps": "Execute GET /marketplace/leads/check-duplicate as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/check-duplicate", + "variantName": "Lead duplicate check invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-019", + "variantId": "V251-MKT-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead note add CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/999999/notes as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/notes", + "variantName": "Lead note add CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-020", + "variantId": "V251-MKT-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead attachment upload CSRF-less POST rejected", + "steps": "Execute GET /marketplace/leads/999999/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999/upload", + "variantName": "Lead attachment upload CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-021", + "variantId": "V251-MKT-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant lead probe safe", + "steps": "Execute GET /marketplace/leads/999999?tenant_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/999999?tenant_id=999999", + "variantName": "Cross-tenant lead probe safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "MKT-022", + "variantId": "V251-MKT-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead export requires authorization", + "steps": "Execute GET /marketplace/leads/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/marketplace/leads/export", + "variantName": "Lead export requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-001", + "variantId": "V251-PBILL-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Plans page loads", + "steps": "Execute GET /platform-billing/plans as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/plans", + "variantName": "Plans page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-002", + "variantId": "V251-PBILL-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Accounts page loads", + "steps": "Execute GET /platform-billing/accounts as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/accounts", + "variantName": "Accounts page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-003", + "variantId": "V251-PBILL-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Audit firm subscriptions page loads", + "steps": "Execute GET /platform-billing/audit-firm-subscriptions as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/audit-firm-subscriptions", + "variantName": "Audit firm subscriptions page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-004", + "variantId": "V251-PBILL-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Client dashboard subscriptions page loads", + "steps": "Execute GET /platform-billing/client-dashboard-subscriptions as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/client-dashboard-subscriptions", + "variantName": "Client dashboard subscriptions page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-005", + "variantId": "V251-PBILL-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Consultant subscriptions page loads", + "steps": "Execute GET /platform-billing/consultant-subscriptions as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/consultant-subscriptions", + "variantName": "Consultant subscriptions page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-006", + "variantId": "V251-PBILL-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Subscriptions page loads", + "steps": "Execute GET /platform-billing/subscriptions as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/subscriptions", + "variantName": "Subscriptions page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-007", + "variantId": "V251-PBILL-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invoices page loads", + "steps": "Execute GET /platform-billing/invoices as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/invoices", + "variantName": "Invoices page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-008", + "variantId": "V251-PBILL-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid invoice detail/post safe", + "steps": "Execute GET /platform-billing/invoices/999999/post as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/invoices/999999/post", + "variantName": "Invalid invoice detail/post safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-009", + "variantId": "V251-PBILL-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid invoice payments safe", + "steps": "Execute GET /platform-billing/invoices/999999/payments as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/invoices/999999/payments", + "variantName": "Invalid invoice payments safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-010", + "variantId": "V251-PBILL-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid plan detail safe", + "steps": "Execute GET /platform-billing/plans/999999 as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/platform-billing/plans/999999", + "variantName": "Invalid plan detail safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "PBILL-011", + "variantId": "V251-PBILL-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Firm", + "scenario": "Firm Admin cannot manage platform plans", + "steps": "Execute GET /platform-billing/plans as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/plans", + "variantName": "Firm Admin cannot manage platform plans", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "PBILL-012", + "variantId": "V251-PBILL-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Staff", + "scenario": "Staff cannot access platform accounts", + "steps": "Execute GET /platform-billing/accounts as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/accounts", + "variantName": "Staff cannot access platform accounts", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "PBILL-013", + "variantId": "V251-PBILL-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Client", + "scenario": "Client cannot access platform invoices list", + "steps": "Execute GET /platform-billing/invoices as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices", + "variantName": "Client cannot access platform invoices list", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "PBILL-014", + "variantId": "V251-PBILL-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Consultant", + "scenario": "Consultant cannot access platform invoice list", + "steps": "Execute GET /platform-billing/invoices as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices", + "variantName": "Consultant cannot access platform invoice list", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "PBILL-015", + "variantId": "V251-PBILL-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Partner", + "scenario": "Partner cannot post platform invoice", + "steps": "Execute GET /platform-billing/invoices/999999/post as Partner and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/post", + "variantName": "Partner cannot post platform invoice", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Partner" + }, + { + "sourceId": "PBILL-016", + "variantId": "V251-PBILL-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/plans as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/plans", + "variantName": "Plan create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-017", + "variantId": "V251-PBILL-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan update invalid ID CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/plans/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/plans/999999", + "variantName": "Plan update invalid ID CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-018", + "variantId": "V251-PBILL-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Account create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/accounts as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/accounts", + "variantName": "Account create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-019", + "variantId": "V251-PBILL-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Audit firm subscription create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/audit-firm-subscriptions as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/audit-firm-subscriptions", + "variantName": "Audit firm subscription create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-020", + "variantId": "V251-PBILL-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Client dashboard subscription create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/client-dashboard-subscriptions as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/client-dashboard-subscriptions", + "variantName": "Client dashboard subscription create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-021", + "variantId": "V251-PBILL-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Consultant subscription create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/consultant-subscriptions as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/consultant-subscriptions", + "variantName": "Consultant subscription create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-022", + "variantId": "V251-PBILL-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Platform subscription create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/subscriptions as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/subscriptions", + "variantName": "Platform subscription create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-023", + "variantId": "V251-PBILL-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice create CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/invoices as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices", + "variantName": "Invoice create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-024", + "variantId": "V251-PBILL-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice post CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/invoices/999999/post as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/post", + "variantName": "Invoice post CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-025", + "variantId": "V251-PBILL-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice payment CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/payments", + "variantName": "Invoice payment CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-026", + "variantId": "V251-PBILL-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice cancel CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/invoices/999999/cancel as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/cancel", + "variantName": "Invoice cancel CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-027", + "variantId": "V251-PBILL-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Payment over-amount invalid POST safe", + "steps": "Execute GET /platform-billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/payments", + "variantName": "Payment over-amount invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-028", + "variantId": "V251-PBILL-028", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Duplicate plan code invalid POST safe", + "steps": "Execute GET /platform-billing/plans as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/plans", + "variantName": "Duplicate plan code invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-029", + "variantId": "V251-PBILL-029", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Duplicate invoice number invalid POST safe", + "steps": "Execute GET /platform-billing/invoices as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices", + "variantName": "Duplicate invoice number invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-030", + "variantId": "V251-PBILL-030", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice export requires auth", + "steps": "Execute GET /platform-billing/invoices/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/export", + "variantName": "Invoice export requires auth", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-031", + "variantId": "V251-PBILL-031", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice PDF invalid ID safe", + "steps": "Execute GET /platform-billing/invoices/999999/pdf as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/pdf", + "variantName": "Invoice PDF invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-032", + "variantId": "V251-PBILL-032", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Cross-account invoice IDOR blocked", + "steps": "Execute GET /platform-billing/invoices/999999?account_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/invoices/999999?account_id=999999", + "variantName": "Cross-account invoice IDOR blocked", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-033", + "variantId": "V251-PBILL-033", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Payment receipt invalid ID safe", + "steps": "Execute GET /platform-billing/payments/999999/receipt as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/payments/999999/receipt", + "variantName": "Payment receipt invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-034", + "variantId": "V251-PBILL-034", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Billing account delete CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/accounts/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/accounts/999999/delete", + "variantName": "Billing account delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "PBILL-035", + "variantId": "V251-PBILL-035", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan delete CSRF-less POST rejected", + "steps": "Execute GET /platform-billing/plans/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/platform-billing/plans/999999/delete", + "variantName": "Plan delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-001", + "variantId": "V251-SYS-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Tenants page loads", + "steps": "Execute GET /system-settings/tenants as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/tenants", + "variantName": "Tenants page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-002", + "variantId": "V251-SYS-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Branches page loads", + "steps": "Execute GET /system-settings/branches as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/branches", + "variantName": "Branches page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-003", + "variantId": "V251-SYS-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Branding page loads", + "steps": "Execute GET /system-settings/branding as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/branding", + "variantName": "Branding page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-004", + "variantId": "V251-SYS-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Financial years page loads", + "steps": "Execute GET /system-settings/financial-years as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/financial-years", + "variantName": "Financial years page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-005", + "variantId": "V251-SYS-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "RBAC page loads", + "steps": "Execute GET /system-settings/rbac as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/rbac", + "variantName": "RBAC page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-006", + "variantId": "V251-SYS-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Roles page loads", + "steps": "Execute GET /system-settings/rbac/roles as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/rbac/roles", + "variantName": "Roles page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-007", + "variantId": "V251-SYS-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Permissions page loads", + "steps": "Execute GET /system-settings/rbac/permissions as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/rbac/permissions", + "variantName": "Permissions page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-008", + "variantId": "V251-SYS-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Audit logs page loads", + "steps": "Execute GET /system-settings/audit-logs as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/audit-logs", + "variantName": "Audit logs page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-009", + "variantId": "V251-SYS-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid tenant context switch safe", + "steps": "Execute GET /system-settings/context/tenant/999999 as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/context/tenant/999999", + "variantName": "Invalid tenant context switch safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-010", + "variantId": "V251-SYS-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid branch context switch safe", + "steps": "Execute GET /system-settings/context/branch/999999 as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/context/branch/999999", + "variantName": "Invalid branch context switch safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-011", + "variantId": "V251-SYS-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid financial year context switch safe", + "steps": "Execute GET /system-settings/context/financial-year/INVALID as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/context/financial-year/INVALID", + "variantName": "Invalid financial year context switch safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-012", + "variantId": "V251-SYS-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "FY backup export page/action safe", + "steps": "Execute GET /system-settings/financial-years/backup-export as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/financial-years/backup-export", + "variantName": "FY backup export page/action safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "SYS-013", + "variantId": "V251-SYS-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Staff", + "scenario": "Staff cannot access tenants", + "steps": "Execute GET /system-settings/tenants as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/tenants", + "variantName": "Staff cannot access tenants", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "SYS-014", + "variantId": "V251-SYS-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Client", + "scenario": "Client cannot access branches", + "steps": "Execute GET /system-settings/branches as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/branches", + "variantName": "Client cannot access branches", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "SYS-015", + "variantId": "V251-SYS-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Consultant", + "scenario": "Consultant cannot access financial years", + "steps": "Execute GET /system-settings/financial-years as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years", + "variantName": "Consultant cannot access financial years", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "SYS-016", + "variantId": "V251-SYS-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Partner", + "scenario": "Partner cannot manage RBAC roles unless permitted", + "steps": "Execute GET /system-settings/rbac/roles as Partner and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "variantName": "Partner cannot manage RBAC roles unless permitted", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Partner" + }, + { + "sourceId": "SYS-017", + "variantId": "V251-SYS-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Manager", + "scenario": "Manager cannot switch unauthorized tenant", + "steps": "Execute GET /system-settings/context/tenant/999999 as Manager and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/context/tenant/999999", + "variantName": "Manager cannot switch unauthorized tenant", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Manager" + }, + { + "sourceId": "SYS-018", + "variantId": "V251-SYS-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Firm Admin", + "scenario": "Firm Admin cannot switch to unauthorized branch", + "steps": "Execute GET /system-settings/context/branch/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/context/branch/999999", + "variantName": "Firm Admin cannot switch to unauthorized branch", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "SYS-019", + "variantId": "V251-SYS-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Tenant create CSRF-less POST rejected", + "steps": "Execute GET /system-settings/tenants as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/tenants", + "variantName": "Tenant create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-020", + "variantId": "V251-SYS-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Tenant update invalid ID CSRF-less POST rejected", + "steps": "Execute GET /system-settings/tenants/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/tenants/999999", + "variantName": "Tenant update invalid ID CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-021", + "variantId": "V251-SYS-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branch create CSRF-less POST rejected", + "steps": "Execute GET /system-settings/branches as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/branches", + "variantName": "Branch create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-022", + "variantId": "V251-SYS-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branch update invalid ID CSRF-less POST rejected", + "steps": "Execute GET /system-settings/branches/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/branches/999999", + "variantName": "Branch update invalid ID CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-023", + "variantId": "V251-SYS-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branding save CSRF-less POST rejected", + "steps": "Execute GET /system-settings/branding as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/branding", + "variantName": "Branding save CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-024", + "variantId": "V251-SYS-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Financial year create CSRF-less POST rejected", + "steps": "Execute GET /system-settings/financial-years as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years", + "variantName": "Financial year create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-025", + "variantId": "V251-SYS-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Make current FY CSRF-less POST rejected", + "steps": "Execute GET /system-settings/financial-years/999999/make-current as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/make-current", + "variantName": "Make current FY CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-026", + "variantId": "V251-SYS-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Lock FY CSRF-less POST rejected", + "steps": "Execute GET /system-settings/financial-years/999999/lock as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/lock", + "variantName": "Lock FY CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-027", + "variantId": "V251-SYS-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Unlock FY CSRF-less POST rejected", + "steps": "Execute GET /system-settings/financial-years/999999/unlock as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/unlock", + "variantName": "Unlock FY CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-028", + "variantId": "V251-SYS-028", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "FY backup export CSRF-less POST rejected", + "steps": "Execute GET /system-settings/financial-years/999999/backup-export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/backup-export", + "variantName": "FY backup export CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-029", + "variantId": "V251-SYS-029", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "FY backup download invalid ID safe", + "steps": "Execute GET /system-settings/financial-years/backups/999999/download as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/financial-years/backups/999999/download", + "variantName": "FY backup download invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-030", + "variantId": "V251-SYS-030", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Role create CSRF-less POST rejected", + "steps": "Execute GET /system-settings/rbac/roles as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "variantName": "Role create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-031", + "variantId": "V251-SYS-031", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Role update invalid ID CSRF-less POST rejected", + "steps": "Execute GET /system-settings/rbac/roles/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999", + "variantName": "Role update invalid ID CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-032", + "variantId": "V251-SYS-032", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Permission assign CSRF-less POST rejected", + "steps": "Execute GET /system-settings/rbac/roles/999999/permissions as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999/permissions", + "variantName": "Permission assign CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-033", + "variantId": "V251-SYS-033", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Permission remove CSRF-less POST rejected", + "steps": "Execute GET /system-settings/rbac/roles/999999/permissions/999999/remove as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999/permissions/999999/remove", + "variantName": "Permission remove CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-034", + "variantId": "V251-SYS-034", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Duplicate role invalid POST safe", + "steps": "Execute GET /system-settings/rbac/roles as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "variantName": "Duplicate role invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-035", + "variantId": "V251-SYS-035", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Unauthorized branch context query safe", + "steps": "Execute GET /system-settings/context/branch/999999?tenant_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/context/branch/999999?tenant_id=999999", + "variantName": "Unauthorized branch context query safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "SYS-036", + "variantId": "V251-SYS-036", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Audit log export requires authorization", + "steps": "Execute GET /system-settings/audit-logs/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "variantName": "Audit log export requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "sourceId": "API-001", + "variantId": "V251-API-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API auth token rejects blank credentials", + "steps": "Execute POST /api/auth/token as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/token", + "variantName": "API auth token rejects blank credentials", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-002", + "variantId": "V251-API-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API refresh rejects missing refresh token", + "steps": "Execute POST /api/auth/refresh as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/refresh", + "variantName": "API refresh rejects missing refresh token", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-003", + "variantId": "V251-API-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API logout without token handled safely", + "steps": "Execute POST /api/auth/logout as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/logout", + "variantName": "API logout without token handled safely", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-004", + "variantId": "V251-API-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API me requires authentication", + "steps": "Execute GET /api/auth/me as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/me", + "variantName": "API me requires authentication", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-005", + "variantId": "V251-API-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API forgot-password rejects invalid email safely", + "steps": "Execute POST /api/auth/forgot-password as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/forgot-password", + "variantName": "API forgot-password rejects invalid email safely", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-006", + "variantId": "V251-API-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API reset-password rejects bogus token", + "steps": "Execute POST /api/auth/reset-password as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/reset-password", + "variantName": "API reset-password rejects bogus token", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-007", + "variantId": "V251-API-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API invite accept rejects bogus token", + "steps": "Execute POST /api/auth/invite/accept as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/invite/accept", + "variantName": "API invite accept rejects bogus token", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-008", + "variantId": "V251-API-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users requires authorization", + "steps": "Execute GET /api/users as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/users", + "variantName": "API users requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-009", + "variantId": "V251-API-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users create CSRF/auth rejected", + "steps": "Execute POST /api/users as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/users", + "variantName": "API users create CSRF/auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-010", + "variantId": "V251-API-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC roles requires authorization", + "steps": "Execute GET /api/rbac/roles as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/rbac/roles", + "variantName": "API RBAC roles requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-011", + "variantId": "V251-API-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC roles create auth rejected", + "steps": "Execute POST /api/rbac/roles as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/rbac/roles", + "variantName": "API RBAC roles create auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-012", + "variantId": "V251-API-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC permissions requires authorization", + "steps": "Execute GET /api/rbac/permissions as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/rbac/permissions", + "variantName": "API RBAC permissions requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-013", + "variantId": "V251-API-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenancy tenants requires authorization", + "steps": "Execute GET /api/tenancy/tenants as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/tenancy/tenants", + "variantName": "API tenancy tenants requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-014", + "variantId": "V251-API-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenancy branches requires authorization", + "steps": "Execute GET /api/tenancy/branches as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/tenancy/branches", + "variantName": "API tenancy branches requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-015", + "variantId": "V251-API-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API clients requires authorization", + "steps": "Execute GET /api/v1/clients as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients", + "variantName": "API clients requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-016", + "variantId": "V251-API-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API clients create auth rejected", + "steps": "Execute POST /api/v1/clients as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients", + "variantName": "API clients create auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-017", + "variantId": "V251-API-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client invalid ID safe", + "steps": "Execute GET /api/v1/clients/999999 as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "variantName": "API client invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-018", + "variantId": "V251-API-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client update invalid ID auth rejected", + "steps": "Execute PUT /api/v1/clients/999999 as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "variantName": "API client update invalid ID auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "PUT", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-019", + "variantId": "V251-API-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client delete invalid ID auth rejected", + "steps": "Execute DELETE /api/v1/clients/999999 as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "variantName": "API client delete invalid ID auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "DELETE", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-020", + "variantId": "V251-API-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API cross-tenant query does not leak", + "steps": "Execute GET /api/v1/clients?tenant_id=999999 as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients?tenant_id=999999", + "variantName": "API cross-tenant query does not leak", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-021", + "variantId": "V251-API-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API roles duplicate invalid POST safe", + "steps": "Execute POST /api/rbac/roles as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/rbac/roles", + "variantName": "API roles duplicate invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-022", + "variantId": "V251-API-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API permission elevation attempt rejected", + "steps": "Execute POST /api/rbac/roles/999999/permissions as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/rbac/roles/999999/permissions", + "variantName": "API permission elevation attempt rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-023", + "variantId": "V251-API-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenant create auth rejected", + "steps": "Execute POST /api/tenancy/tenants as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/tenancy/tenants", + "variantName": "API tenant create auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-024", + "variantId": "V251-API-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API branch create auth rejected", + "steps": "Execute POST /api/tenancy/branches as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/tenancy/branches", + "variantName": "API branch create auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-025", + "variantId": "V251-API-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API malformed JSON safe", + "steps": "Execute POST /api/auth/token as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/token", + "variantName": "API malformed JSON safe", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-026", + "variantId": "V251-API-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API token response does not leak password", + "steps": "Execute POST /api/auth/token as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/token", + "variantName": "API token response does not leak password", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-027", + "variantId": "V251-API-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API forgot password does not disclose account existence", + "steps": "Execute POST /api/auth/forgot-password as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/forgot-password", + "variantName": "API forgot password does not disclose account existence", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-028", + "variantId": "V251-API-028", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API reset token replay rejected", + "steps": "Execute POST /api/auth/reset-password as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/reset-password", + "variantName": "API reset token replay rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-029", + "variantId": "V251-API-029", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API rate-limit/bruteforce endpoint safe", + "steps": "Execute POST /api/auth/token as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/token", + "variantName": "API rate-limit/bruteforce endpoint safe", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-030", + "variantId": "V251-API-030", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API CORS preflight handled safely", + "steps": "Execute OPTIONS /api/auth/me as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/auth/me", + "variantName": "API CORS preflight handled safely", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "OPTIONS", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-031", + "variantId": "V251-API-031", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API export clients requires authorization", + "steps": "Execute GET /api/v1/clients/export as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/v1/clients/export", + "variantName": "API export clients requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-032", + "variantId": "V251-API-032", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users invalid ID safe", + "steps": "Execute GET /api/users/999999 as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/users/999999", + "variantName": "API users invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-033", + "variantId": "V251-API-033", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users role update auth rejected", + "steps": "Execute POST /api/users/999999/roles as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/users/999999/roles", + "variantName": "API users role update auth rejected", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-034", + "variantId": "V251-API-034", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API permission denied response does not include stack trace", + "steps": "Execute GET /api/users as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/users", + "variantName": "API permission denied response does not include stack trace", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "API-035", + "variantId": "V251-API-035", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API unknown endpoint returns safe 404", + "steps": "Execute GET /api/does-not-exist-uat-vapt as API/Anonymous and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/api/does-not-exist-uat-vapt", + "variantName": "API unknown endpoint returns safe 404", + "variantType": "security", + "automation": "automated", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-001", + "variantId": "V251-AUD-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs page loads for System Admin", + "steps": "Execute GET /system-settings/audit-logs as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/audit-logs", + "variantName": "Audit logs page loads for System Admin", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-002", + "variantId": "V251-AUD-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Firm Admin", + "scenario": "Firm Admin audit logs page loads or safely restricted", + "steps": "Execute GET /system-settings/audit-logs as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/audit-logs", + "variantName": "Firm Admin audit logs page loads or safely restricted", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "AUD-003", + "variantId": "V251-AUD-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Staff", + "scenario": "Staff cannot access audit logs", + "steps": "Execute GET /system-settings/audit-logs as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "variantName": "Staff cannot access audit logs", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "AUD-004", + "variantId": "V251-AUD-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Client", + "scenario": "Client cannot access audit logs", + "steps": "Execute GET /system-settings/audit-logs as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "variantName": "Client cannot access audit logs", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "AUD-005", + "variantId": "V251-AUD-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit log invalid detail safe", + "steps": "Execute GET /system-settings/audit-logs/999999 as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/audit-logs/999999", + "variantName": "Audit log invalid detail safe", + "variantType": "positive", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-006", + "variantId": "V251-AUD-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Staff", + "scenario": "Audit log export requires permission", + "steps": "Execute GET /system-settings/audit-logs/export as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "variantName": "Audit log export requires permission", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "AUD-007", + "variantId": "V251-AUD-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit log filters do not crash", + "steps": "Execute GET /system-settings/audit-logs?module=clients&action=create as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/system-settings/audit-logs?module=clients&action=create", + "variantName": "Audit log filters do not crash", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-008", + "variantId": "V251-AUD-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose password", + "steps": "Execute GET /system-settings/audit-logs as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "variantName": "Audit logs do not expose password", + "variantType": "security", + "automation": "automated", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-009", + "variantId": "V251-AUD-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose OTP", + "steps": "Execute GET /system-settings/audit-logs as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "variantName": "Audit logs do not expose OTP", + "variantType": "security", + "automation": "automated", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-010", + "variantId": "V251-AUD-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose reset token", + "steps": "Execute GET /system-settings/audit-logs as System Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "variantName": "Audit logs do not expose reset token", + "variantType": "security", + "automation": "automated", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "sourceId": "AUD-011", + "variantId": "V251-AUD-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log export CSRF-less POST rejected", + "steps": "Execute GET /system-settings/audit-logs/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "variantName": "Audit log export CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-012", + "variantId": "V251-AUD-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log delete invalid ID rejected", + "steps": "Execute GET /system-settings/audit-logs/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs/999999/delete", + "variantName": "Audit log delete invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-013", + "variantId": "V251-AUD-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log tamper invalid POST rejected", + "steps": "Execute GET /system-settings/audit-logs/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs/999999", + "variantName": "Audit log tamper invalid POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-014", + "variantId": "V251-AUD-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Permission denied event generated safely by blocked route", + "steps": "Execute GET /system-settings/rbac/roles as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "variantName": "Permission denied event generated safely by blocked route", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-015", + "variantId": "V251-AUD-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Login failure is handled without stack trace", + "steps": "Execute GET /login as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/login", + "variantName": "Login failure is handled without stack trace", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-016", + "variantId": "V251-AUD-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Document download audit invalid ID safe", + "steps": "Execute GET /documents/999999/download as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/download", + "variantName": "Document download audit invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-017", + "variantId": "V251-AUD-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Billing payment audit invalid ID safe", + "steps": "Execute GET /billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "variantName": "Billing payment audit invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-018", + "variantId": "V251-AUD-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Notice case audit invalid ID safe", + "steps": "Execute GET /notice-cases/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999", + "variantName": "Notice case audit invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-019", + "variantId": "V251-AUD-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log tenant scope query safe", + "steps": "Execute GET /system-settings/audit-logs?tenant_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs?tenant_id=999999", + "variantName": "Audit log tenant scope query safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "AUD-020", + "variantId": "V251-AUD-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log branch scope query safe", + "steps": "Execute GET /system-settings/audit-logs?branch_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/system-settings/audit-logs?branch_id=999999", + "variantName": "Audit log branch scope query safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-001", + "variantId": "V251-DSEC-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid document download returns safe response", + "steps": "Execute GET /documents/999999/download as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/download", + "variantName": "Invalid document download returns safe response", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-002", + "variantId": "V251-DSEC-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid document version download safe", + "steps": "Execute GET /documents/999999/versions/999999/download as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/versions/999999/download", + "variantName": "Invalid document version download safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-003", + "variantId": "V251-DSEC-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid permanent document download safe", + "steps": "Execute GET /documents/permanent/999999/download as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/permanent/999999/download", + "variantName": "Invalid permanent document download safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-004", + "variantId": "V251-DSEC-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Deleted document invalid download safe", + "steps": "Execute GET /documents/999999/deleted/download as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/deleted/download", + "variantName": "Deleted document invalid download safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-005", + "variantId": "V251-DSEC-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Client", + "scenario": "Client cannot download other document ID", + "steps": "Execute GET /documents/999999/download as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/download", + "variantName": "Client cannot download other document ID", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "DSEC-006", + "variantId": "V251-DSEC-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Consultant", + "scenario": "Consultant cannot download unshared document", + "steps": "Execute GET /documents/999999/download as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/download", + "variantName": "Consultant cannot download unshared document", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "DSEC-007", + "variantId": "V251-DSEC-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Staff", + "scenario": "Staff cannot access storage admin", + "steps": "Execute GET /documents/storage/nodes as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/storage/nodes", + "variantName": "Staff cannot access storage admin", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "DSEC-008", + "variantId": "V251-DSEC-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Storage agent jobs require auth", + "steps": "Execute GET /storage-agent/jobs as Public and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/storage-agent/jobs", + "variantName": "Storage agent jobs require auth", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-009", + "variantId": "V251-DSEC-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Storage agent heartbeat without secret rejected", + "steps": "Execute GET /storage-agent/heartbeat as Public and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/storage-agent/heartbeat", + "variantName": "Storage agent heartbeat without secret rejected", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-010", + "variantId": "V251-DSEC-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Download token invalid safe", + "steps": "Execute GET /documents/download-token/invalid-token as Public and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/download-token/invalid-token", + "variantName": "Download token invalid safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-011", + "variantId": "V251-DSEC-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Document search route does not leak across tenant", + "steps": "Execute GET /documents?tenant_id=999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents?tenant_id=999999", + "variantName": "Document search route does not leak across tenant", + "variantType": "security", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-012", + "variantId": "V251-DSEC-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Document repository loads for Firm Admin", + "steps": "Execute GET /documents as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/documents", + "variantName": "Document repository loads for Firm Admin", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "DSEC-013", + "variantId": "V251-DSEC-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload double extension rejected", + "steps": "Execute GET /documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/upload", + "variantName": "Document upload double extension rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-014", + "variantId": "V251-DSEC-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload MIME mismatch rejected", + "steps": "Execute GET /documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/upload", + "variantName": "Document upload MIME mismatch rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-015", + "variantId": "V251-DSEC-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload large file rejected or handled", + "steps": "Execute GET /documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/upload", + "variantName": "Document upload large file rejected or handled", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-016", + "variantId": "V251-DSEC-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload path traversal filename rejected", + "steps": "Execute GET /documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/upload", + "variantName": "Document upload path traversal filename rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-017", + "variantId": "V251-DSEC-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document delete CSRF-less POST rejected", + "steps": "Execute GET /documents/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/delete", + "variantName": "Document delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-018", + "variantId": "V251-DSEC-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document version delete CSRF-less POST rejected", + "steps": "Execute GET /documents/999999/versions/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/versions/999999/delete", + "variantName": "Document version delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-019", + "variantId": "V251-DSEC-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Permanent document delete CSRF-less POST rejected", + "steps": "Execute GET /documents/permanent/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/permanent/999999/delete", + "variantName": "Permanent document delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-020", + "variantId": "V251-DSEC-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document move cross-tenant CSRF-less POST rejected", + "steps": "Execute GET /documents/999999/move as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/move", + "variantName": "Document move cross-tenant CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-021", + "variantId": "V251-DSEC-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document share with client invalid ID rejected", + "steps": "Execute GET /documents/999999/share-client as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/share-client", + "variantName": "Document share with client invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-022", + "variantId": "V251-DSEC-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document share with consultant invalid ID rejected", + "steps": "Execute GET /documents/999999/share-consultant as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/share-consultant", + "variantName": "Document share with consultant invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-023", + "variantId": "V251-DSEC-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage node create CSRF-less POST rejected", + "steps": "Execute GET /documents/storage/nodes as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/storage/nodes", + "variantName": "Storage node create CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-024", + "variantId": "V251-DSEC-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage agent ack without secret rejected", + "steps": "Execute GET /storage-agent/jobs/999999/ack as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/storage-agent/jobs/999999/ack", + "variantName": "Storage agent ack without secret rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-025", + "variantId": "V251-DSEC-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage agent upload without secret rejected", + "steps": "Execute GET /storage-agent/jobs/999999/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/storage-agent/jobs/999999/upload", + "variantName": "Storage agent upload without secret rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-026", + "variantId": "V251-DSEC-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Download request expiry invalid safe", + "steps": "Execute GET /documents/download-requests/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/download-requests/999999", + "variantName": "Download request expiry invalid safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-027", + "variantId": "V251-DSEC-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document tag update CSRF-less POST rejected", + "steps": "Execute GET /documents/999999/tags as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/tags", + "variantName": "Document tag update CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "DSEC-028", + "variantId": "V251-DSEC-028", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document metadata update invalid ID rejected", + "steps": "Execute GET /documents/999999/edit as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/documents/999999/edit", + "variantName": "Document metadata update invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-001", + "variantId": "V251-WORK2-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Firm Admin", + "scenario": "Engagement create page/list loads", + "steps": "Execute GET /work/engagements as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/work/engagements", + "variantName": "Engagement create page/list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "WORK2-002", + "variantId": "V251-WORK2-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Firm Admin", + "scenario": "Existing engagement detail loads", + "steps": "Execute GET /work/engagements/1 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/1", + "variantName": "Existing engagement detail loads", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "WORK2-003", + "variantId": "V251-WORK2-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Staff", + "scenario": "Staff work board loads", + "steps": "Execute GET /work as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/work", + "variantName": "Staff work board loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "WORK2-004", + "variantId": "V251-WORK2-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Manager", + "scenario": "Manager team work board loads", + "steps": "Execute GET /manager/work as Manager and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/manager/work", + "variantName": "Manager team work board loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Manager" + }, + { + "sourceId": "WORK2-005", + "variantId": "V251-WORK2-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Partner", + "scenario": "Partner reviews page loads", + "steps": "Execute GET /partner/reviews as Partner and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/partner/reviews", + "variantName": "Partner reviews page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Partner" + }, + { + "sourceId": "WORK2-006", + "variantId": "V251-WORK2-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Client", + "scenario": "Client dashboard compliance item list loads", + "steps": "Execute GET /client/dashboard as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/client/dashboard", + "variantName": "Client dashboard compliance item list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "WORK2-007", + "variantId": "V251-WORK2-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Consultant", + "scenario": "Consultant assignment area loads", + "steps": "Execute GET /consultants as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/consultants", + "variantName": "Consultant assignment area loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "WORK2-008", + "variantId": "V251-WORK2-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Staff", + "scenario": "Invalid engagement ID is safe", + "steps": "Execute GET /work/engagements/999999 as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999", + "variantName": "Invalid engagement ID is safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "WORK2-009", + "variantId": "V251-WORK2-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Client", + "scenario": "Client cannot access internal engagement detail", + "steps": "Execute GET /work/engagements/999999 as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999", + "variantName": "Client cannot access internal engagement detail", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "WORK2-010", + "variantId": "V251-WORK2-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Consultant", + "scenario": "Consultant cannot access internal engagement detail", + "steps": "Execute GET /work/engagements/999999 as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999", + "variantName": "Consultant cannot access internal engagement detail", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "WORK2-011", + "variantId": "V251-WORK2-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Create engagement CSRF-less POST rejected", + "steps": "Execute GET /work/engagements as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements", + "variantName": "Create engagement CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-012", + "variantId": "V251-WORK2-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Generate tasks CSRF-less POST rejected", + "steps": "Execute GET /work/engagements/999999/generate-tasks as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/generate-tasks", + "variantName": "Generate tasks CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-013", + "variantId": "V251-WORK2-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Assign task CSRF-less POST rejected", + "steps": "Execute GET /work/tasks/999999/assign as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/assign", + "variantName": "Assign task CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-014", + "variantId": "V251-WORK2-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Staff start task invalid ID safe", + "steps": "Execute GET /work/tasks/999999/start as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/start", + "variantName": "Staff start task invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-015", + "variantId": "V251-WORK2-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Staff complete task invalid ID safe", + "steps": "Execute GET /work/tasks/999999/complete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/complete", + "variantName": "Staff complete task invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-016", + "variantId": "V251-WORK2-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task upload document invalid ID rejected", + "steps": "Execute GET /work/tasks/999999/documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/documents/upload", + "variantName": "Task upload document invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-017", + "variantId": "V251-WORK2-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Manager review invalid task rejected", + "steps": "Execute GET /work/tasks/999999/review as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/review", + "variantName": "Manager review invalid task rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-018", + "variantId": "V251-WORK2-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Partner approve invalid engagement rejected", + "steps": "Execute GET /work/engagements/999999/approve as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/approve", + "variantName": "Partner approve invalid engagement rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-019", + "variantId": "V251-WORK2-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Partner reject invalid engagement rejected", + "steps": "Execute GET /work/engagements/999999/reject as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/reject", + "variantName": "Partner reject invalid engagement rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-020", + "variantId": "V251-WORK2-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Add internal comment CSRF-less POST rejected", + "steps": "Execute GET /work/engagements/999999/comments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/comments", + "variantName": "Add internal comment CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-021", + "variantId": "V251-WORK2-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Add client-visible comment invalid route safe", + "steps": "Execute GET /work/engagements/999999/client-comments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/client-comments", + "variantName": "Add client-visible comment invalid route safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-022", + "variantId": "V251-WORK2-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Blocked task without reason rejected", + "steps": "Execute GET /work/tasks/999999/block as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/block", + "variantName": "Blocked task without reason rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-023", + "variantId": "V251-WORK2-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Completed task edit invalid ID safe", + "steps": "Execute GET /work/tasks/999999/edit as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/edit", + "variantName": "Completed task edit invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-024", + "variantId": "V251-WORK2-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Locked FY task change rejected or safe", + "steps": "Execute GET /work/tasks/999999/status as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/status", + "variantName": "Locked FY task change rejected or safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-025", + "variantId": "V251-WORK2-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task priority update CSRF-less POST rejected", + "steps": "Execute GET /work/tasks/999999/priority as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/priority", + "variantName": "Task priority update CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-026", + "variantId": "V251-WORK2-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task due date update CSRF-less POST rejected", + "steps": "Execute GET /work/tasks/999999/due-date as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/tasks/999999/due-date", + "variantName": "Task due date update CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-027", + "variantId": "V251-WORK2-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Work document download invalid ID safe", + "steps": "Execute GET /work/documents/999999/download as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/documents/999999/download", + "variantName": "Work document download invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-028", + "variantId": "V251-WORK2-028", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Work communication internal note hidden from client route safe", + "steps": "Execute GET /client/work/engagements/999999/comments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/client/work/engagements/999999/comments", + "variantName": "Work communication internal note hidden from client route safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-029", + "variantId": "V251-WORK2-029", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant engagement IDOR safe", + "steps": "Execute GET /work/engagements/999999?tenant_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999?tenant_id=999999", + "variantName": "Cross-tenant engagement IDOR safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "WORK2-030", + "variantId": "V251-WORK2-030", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Engagement delete CSRF-less POST rejected", + "steps": "Execute GET /work/engagements/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/work/engagements/999999/delete", + "variantName": "Engagement delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-001", + "variantId": "V251-BIZBILL-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Billing dashboard loads", + "steps": "Execute GET /billing as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/billing", + "variantName": "Billing dashboard loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "BIZBILL-002", + "variantId": "V251-BIZBILL-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invoices list loads", + "steps": "Execute GET /billing/invoices as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/billing/invoices", + "variantName": "Invoices list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "BIZBILL-003", + "variantId": "V251-BIZBILL-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invoice create page loads", + "steps": "Execute GET /billing/invoices/new as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/billing/invoices/new", + "variantName": "Invoice create page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "BIZBILL-004", + "variantId": "V251-BIZBILL-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Payments list loads", + "steps": "Execute GET /billing/payments as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/billing/payments", + "variantName": "Payments list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "BIZBILL-005", + "variantId": "V251-BIZBILL-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invalid invoice detail safe", + "steps": "Execute GET /billing/invoices/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999", + "variantName": "Invalid invoice detail safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "BIZBILL-006", + "variantId": "V251-BIZBILL-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Client", + "scenario": "Client cannot access all invoices", + "steps": "Execute GET /billing/invoices as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices", + "variantName": "Client cannot access all invoices", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "BIZBILL-007", + "variantId": "V251-BIZBILL-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Staff", + "scenario": "Staff cannot access billing", + "steps": "Execute GET /billing as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing", + "variantName": "Staff cannot access billing", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "BIZBILL-008", + "variantId": "V251-BIZBILL-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access billing", + "steps": "Execute GET /billing as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing", + "variantName": "Consultant cannot access billing", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "BIZBILL-009", + "variantId": "V251-BIZBILL-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Client", + "scenario": "Client own invoice invalid ID safe", + "steps": "Execute GET /client/invoices/999999 as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/client/invoices/999999", + "variantName": "Client own invoice invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "BIZBILL-010", + "variantId": "V251-BIZBILL-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice create without line items rejected", + "steps": "Execute GET /billing/invoices as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices", + "variantName": "Invoice create without line items rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-011", + "variantId": "V251-BIZBILL-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice duplicate number invalid POST safe", + "steps": "Execute GET /billing/invoices as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices", + "variantName": "Invoice duplicate number invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-012", + "variantId": "V251-BIZBILL-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice invalid GST/tax values rejected", + "steps": "Execute GET /billing/invoices as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices", + "variantName": "Invoice invalid GST/tax values rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-013", + "variantId": "V251-BIZBILL-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice issue CSRF-less POST rejected", + "steps": "Execute GET /billing/invoices/999999/issue as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/issue", + "variantName": "Invoice issue CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-014", + "variantId": "V251-BIZBILL-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice cancel CSRF-less POST rejected", + "steps": "Execute GET /billing/invoices/999999/cancel as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/cancel", + "variantName": "Invoice cancel CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-015", + "variantId": "V251-BIZBILL-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Void invoice cannot accept payment", + "steps": "Execute GET /billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "variantName": "Void invoice cannot accept payment", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-016", + "variantId": "V251-BIZBILL-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Partial payment invalid ID safe", + "steps": "Execute GET /billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "variantName": "Partial payment invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-017", + "variantId": "V251-BIZBILL-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Overpayment blocked", + "steps": "Execute GET /billing/invoices/999999/payments as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "variantName": "Overpayment blocked", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-018", + "variantId": "V251-BIZBILL-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment receipt invalid ID safe", + "steps": "Execute GET /billing/payments/999999/receipt as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/payments/999999/receipt", + "variantName": "Payment receipt invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-019", + "variantId": "V251-BIZBILL-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice PDF invalid ID safe", + "steps": "Execute GET /billing/invoices/999999/pdf as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/pdf", + "variantName": "Invoice PDF invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-020", + "variantId": "V251-BIZBILL-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice print invalid ID safe", + "steps": "Execute GET /billing/invoices/999999/print as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/print", + "variantName": "Invoice print invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-021", + "variantId": "V251-BIZBILL-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice internal notes hidden from client route safe", + "steps": "Execute GET /client/invoices/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/client/invoices/999999", + "variantName": "Invoice internal notes hidden from client route safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-022", + "variantId": "V251-BIZBILL-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment delete CSRF-less POST rejected", + "steps": "Execute GET /billing/payments/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/payments/999999/delete", + "variantName": "Payment delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-023", + "variantId": "V251-BIZBILL-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice line update invalid ID rejected", + "steps": "Execute GET /billing/invoices/999999/lines/999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999/lines/999999", + "variantName": "Invoice line update invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-024", + "variantId": "V251-BIZBILL-024", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice export requires authorization", + "steps": "Execute GET /billing/invoices/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/export", + "variantName": "Invoice export requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-025", + "variantId": "V251-BIZBILL-025", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment export requires authorization", + "steps": "Execute GET /billing/payments/export as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/payments/export", + "variantName": "Payment export requires authorization", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-026", + "variantId": "V251-BIZBILL-026", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-client invoice IDOR safe", + "steps": "Execute GET /billing/invoices/999999?client_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/999999?client_id=999999", + "variantName": "Cross-client invoice IDOR safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "BIZBILL-027", + "variantId": "V251-BIZBILL-027", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice number sequence endpoint safe", + "steps": "Execute GET /billing/invoices/next-number as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/billing/invoices/next-number", + "variantName": "Invoice number sequence endpoint safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-001", + "variantId": "V251-NCASE-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Notice cases list loads", + "steps": "Execute GET /notice-cases as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/notice-cases", + "variantName": "Notice cases list loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-002", + "variantId": "V251-NCASE-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Notice case new page loads", + "steps": "Execute GET /notice-cases/new as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/notice-cases/new", + "variantName": "Notice case new page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-003", + "variantId": "V251-NCASE-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case detail safe", + "steps": "Execute GET /notice-cases/999999 as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999", + "variantName": "Invalid case detail safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-004", + "variantId": "V251-NCASE-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Client", + "scenario": "Client cannot access internal case list", + "steps": "Execute GET /notice-cases as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases", + "variantName": "Client cannot access internal case list", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "NCASE-005", + "variantId": "V251-NCASE-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access internal case list", + "steps": "Execute GET /notice-cases as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases", + "variantName": "Consultant cannot access internal case list", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "NCASE-006", + "variantId": "V251-NCASE-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Staff", + "scenario": "Staff case list access safe", + "steps": "Execute GET /notice-cases as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/notice-cases", + "variantName": "Staff case list access safe", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "NCASE-007", + "variantId": "V251-NCASE-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case events safe", + "steps": "Execute GET /notice-cases/999999/events as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/events", + "variantName": "Invalid case events safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-008", + "variantId": "V251-NCASE-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case hearing safe", + "steps": "Execute GET /notice-cases/999999/hearings as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/hearings", + "variantName": "Invalid case hearing safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-009", + "variantId": "V251-NCASE-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case order safe", + "steps": "Execute GET /notice-cases/999999/orders as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/orders", + "variantName": "Invalid case order safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "NCASE-010", + "variantId": "V251-NCASE-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Create notice case CSRF-less POST rejected", + "steps": "Execute GET /notice-cases/new as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/new", + "variantName": "Create notice case CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-011", + "variantId": "V251-NCASE-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Notice date/hearing date invalid POST safe", + "steps": "Execute GET /notice-cases/999999/hearings as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/hearings", + "variantName": "Notice date/hearing date invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-012", + "variantId": "V251-NCASE-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Order date before hearing invalid POST safe", + "steps": "Execute GET /notice-cases/999999/orders as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/orders", + "variantName": "Order date before hearing invalid POST safe", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-013", + "variantId": "V251-NCASE-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invalid case status transition rejected", + "steps": "Execute GET /notice-cases/999999/status as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/status", + "variantName": "Invalid case status transition rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-014", + "variantId": "V251-NCASE-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case document upload invalid ID rejected", + "steps": "Execute GET /notice-cases/999999/documents/upload as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/documents/upload", + "variantName": "Case document upload invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-015", + "variantId": "V251-NCASE-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-case document IDOR safe", + "steps": "Execute GET /notice-cases/999999/documents/999999/download as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/documents/999999/download", + "variantName": "Cross-case document IDOR safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-016", + "variantId": "V251-NCASE-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Client-visible document invalid route safe", + "steps": "Execute GET /client/notice-cases/999999/documents/999999/download as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/client/notice-cases/999999/documents/999999/download", + "variantName": "Client-visible document invalid route safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-017", + "variantId": "V251-NCASE-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Internal note hidden from client route safe", + "steps": "Execute GET /client/notice-cases/999999/notes as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/client/notice-cases/999999/notes", + "variantName": "Internal note hidden from client route safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-018", + "variantId": "V251-NCASE-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case deadline reminder invalid ID safe", + "steps": "Execute GET /notice-cases/999999/reminders as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/reminders", + "variantName": "Case deadline reminder invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-019", + "variantId": "V251-NCASE-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case assignment CSRF-less POST rejected", + "steps": "Execute GET /notice-cases/999999/assign as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999/assign", + "variantName": "Case assignment CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-020", + "variantId": "V251-NCASE-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case event delete CSRF-less POST rejected", + "steps": "Execute GET /notice-cases/events/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/events/999999/delete", + "variantName": "Case event delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-021", + "variantId": "V251-NCASE-021", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case hearing delete CSRF-less POST rejected", + "steps": "Execute GET /notice-cases/hearings/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/hearings/999999/delete", + "variantName": "Case hearing delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-022", + "variantId": "V251-NCASE-022", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case order delete CSRF-less POST rejected", + "steps": "Execute GET /notice-cases/orders/999999/delete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/orders/999999/delete", + "variantName": "Case order delete CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "NCASE-023", + "variantId": "V251-NCASE-023", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant notice case IDOR safe", + "steps": "Execute GET /notice-cases/999999?tenant_id=999999 as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/notice-cases/999999?tenant_id=999999", + "variantName": "Cross-tenant notice case IDOR safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-001", + "variantId": "V251-HRRULE-001", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Staff", + "scenario": "Attendance page loads", + "steps": "Execute GET /employee/attendance as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employee/attendance", + "variantName": "Attendance page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "HRRULE-002", + "variantId": "V251-HRRULE-002", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Staff", + "scenario": "Leave page loads", + "steps": "Execute GET /employee/leaves as Staff and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employee/leaves", + "variantName": "Leave page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "sourceId": "HRRULE-003", + "variantId": "V251-HRRULE-003", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Manager", + "scenario": "Manager team attendance loads", + "steps": "Execute GET /employees/attendance as Manager and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employees/attendance", + "variantName": "Manager team attendance loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Manager" + }, + { + "sourceId": "HRRULE-004", + "variantId": "V251-HRRULE-004", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Manager", + "scenario": "Manager team leave loads", + "steps": "Execute GET /employees/leave as Manager and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employees/leave", + "variantName": "Manager team leave loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Manager" + }, + { + "sourceId": "HRRULE-005", + "variantId": "V251-HRRULE-005", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Firm Admin", + "scenario": "Payroll page loads for Firm Admin", + "steps": "Execute GET /employees/payroll as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employees/payroll", + "variantName": "Payroll page loads for Firm Admin", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "HRRULE-006", + "variantId": "V251-HRRULE-006", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Firm Admin", + "scenario": "Employee documents page loads", + "steps": "Execute GET /employees/documents as Firm Admin and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "UAT", + "route": "/employees/documents", + "variantName": "Employee documents page loads", + "variantType": "positive", + "automation": "automated", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "sourceId": "HRRULE-007", + "variantId": "V251-HRRULE-007", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Client", + "scenario": "Client cannot access attendance", + "steps": "Execute GET /employee/attendance as Client and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employee/attendance", + "variantName": "Client cannot access attendance", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Client" + }, + { + "sourceId": "HRRULE-008", + "variantId": "V251-HRRULE-008", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access payroll", + "steps": "Execute GET /employees/payroll as Consultant and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/payroll", + "variantName": "Consultant cannot access payroll", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "sourceId": "HRRULE-009", + "variantId": "V251-HRRULE-009", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Duplicate attendance same date rejected", + "steps": "Execute GET /employee/attendance as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employee/attendance", + "variantName": "Duplicate attendance same date rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-010", + "variantId": "V251-HRRULE-010", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Manual attendance CSRF-less POST rejected", + "steps": "Execute GET /employees/attendance/manual as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/attendance/manual", + "variantName": "Manual attendance CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-011", + "variantId": "V251-HRRULE-011", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave exceeding balance rejected", + "steps": "Execute GET /employee/leaves as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employee/leaves", + "variantName": "Leave exceeding balance rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-012", + "variantId": "V251-HRRULE-012", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave approve invalid ID rejected", + "steps": "Execute GET /employees/leave/999999/approve as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/leave/999999/approve", + "variantName": "Leave approve invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-013", + "variantId": "V251-HRRULE-013", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave reject invalid ID rejected", + "steps": "Execute GET /employees/leave/999999/reject as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/leave/999999/reject", + "variantName": "Leave reject invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-014", + "variantId": "V251-HRRULE-014", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll generate CSRF-less POST rejected", + "steps": "Execute GET /employees/payroll/generate as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/payroll/generate", + "variantName": "Payroll generate CSRF-less POST rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-015", + "variantId": "V251-HRRULE-015", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll approve before generate invalid safe", + "steps": "Execute GET /employees/payroll/999999/approve as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/payroll/999999/approve", + "variantName": "Payroll approve before generate invalid safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-016", + "variantId": "V251-HRRULE-016", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll paid before approval invalid safe", + "steps": "Execute GET /employees/payroll/999999/mark-paid as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/payroll/999999/mark-paid", + "variantName": "Payroll paid before approval invalid safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-017", + "variantId": "V251-HRRULE-017", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payslip invalid ID safe", + "steps": "Execute GET /employees/payroll/999999/payslip as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/payroll/999999/payslip", + "variantName": "Payslip invalid ID safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-018", + "variantId": "V251-HRRULE-018", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Offboarding complete invalid employee safe", + "steps": "Execute GET /employees/999999/offboarding/complete as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/999999/offboarding/complete", + "variantName": "Offboarding complete invalid employee safe", + "variantType": "security", + "automation": "automated", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-019", + "variantId": "V251-HRRULE-019", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Employee document approve invalid ID rejected", + "steps": "Execute GET /employees/documents/999999/approve as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/documents/999999/approve", + "variantName": "Employee document approve invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "sourceId": "HRRULE-020", + "variantId": "V251-HRRULE-020", + "sheet": "UAT_VAPT_v2_5_1_Additions", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Employee document reject invalid ID rejected", + "steps": "Execute GET /employees/documents/999999/reject as Anonymous/Attacker and verify expected security/business outcome.", + "expected": "Route/page responds without backend 500 and follows expected authorization/validation rules.", + "priority": "High", + "type": "VAPT", + "route": "/employees/documents/999999/reject", + "variantName": "Employee document reject invalid ID rejected", + "variantType": "security", + "automation": "automated", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + } +] \ No newline at end of file diff --git a/data/v2_5_test_cases.json b/data/v2_5_test_cases.json new file mode 100644 index 0000000..bc422d3 --- /dev/null +++ b/data/v2_5_test_cases.json @@ -0,0 +1,2416 @@ +[ + { + "sourceId": "CONS-001", + "variantId": "V25-CONS-001", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Firm Admin", + "scenario": "Consultant list loads for Firm Admin", + "steps": "Execute Playwright v2.5 module test: CONS-001 Consultant list loads for Firm Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-002", + "variantId": "V25-CONS-002", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Consultant", + "scenario": "Consultant detail page loads", + "steps": "Execute Playwright v2.5 module test: CONS-002 Consultant detail page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-003", + "variantId": "V25-CONS-003", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Consultant", + "scenario": "Consultant edit form loads", + "steps": "Execute Playwright v2.5 module test: CONS-003 Consultant edit form loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-004", + "variantId": "V25-CONS-004", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Consultant", + "scenario": "Consultant links page loads", + "steps": "Execute Playwright v2.5 module test: CONS-004 Consultant links page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-005", + "variantId": "V25-CONS-005", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Conversion requests list loads", + "steps": "Execute Playwright v2.5 module test: CONS-005 Conversion requests list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-006", + "variantId": "V25-CONS-006", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Conversion request detail loads", + "steps": "Execute Playwright v2.5 module test: CONS-006 Conversion request detail loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-007", + "variantId": "V25-CONS-007", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Conversion request review CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: CONS-007 Conversion request review CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-008", + "variantId": "V25-CONS-008", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Service requests list loads", + "steps": "Execute Playwright v2.5 module test: CONS-008 Service requests list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-009", + "variantId": "V25-CONS-009", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Service request detail loads", + "steps": "Execute Playwright v2.5 module test: CONS-009 Service request detail loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-010", + "variantId": "V25-CONS-010", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "All roles", + "scenario": "Service request status update CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: CONS-010 Service request status update CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-011", + "variantId": "V25-CONS-011", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Staff", + "scenario": "Staff cannot access consultant list", + "steps": "Execute Playwright v2.5 module test: CONS-011 Staff cannot access consultant list", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-012", + "variantId": "V25-CONS-012", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Client", + "scenario": "Client cannot access consultant detail", + "steps": "Execute Playwright v2.5 module test: CONS-012 Client cannot access consultant detail", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-013", + "variantId": "V25-CONS-013", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Consultant", + "scenario": "Non-existent consultant ID returns safe response", + "steps": "Execute Playwright v2.5 module test: CONS-013 Non-existent consultant ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CONS-014", + "variantId": "V25-CONS-014", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Consultants", + "role": "Consultant", + "scenario": "Consultant self-service dashboard loads", + "steps": "Execute Playwright v2.5 module test: CONS-014 Consultant self-service dashboard loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-001", + "variantId": "V25-DOC-STOR-001", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "Firm Admin", + "scenario": "Branch storage dashboard loads for Firm Admin", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-001 Branch storage dashboard loads for Firm Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-002", + "variantId": "V25-DOC-STOR-002", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Permanent document vault list loads", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-002 Permanent document vault list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-003", + "variantId": "V25-DOC-STOR-003", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "Client", + "scenario": "Permanent vault for specific client loads", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-003 Permanent vault for specific client loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-004", + "variantId": "V25-DOC-STOR-004", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Permanent vault upload CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-004 Permanent vault upload CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-005", + "variantId": "V25-DOC-STOR-005", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Permanent document download requires auth", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-005 Permanent document download requires auth", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-006", + "variantId": "V25-DOC-STOR-006", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "System Admin", + "scenario": "Storage jobs list loads for System Admin", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-006 Storage jobs list loads for System Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-007", + "variantId": "V25-DOC-STOR-007", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "System Admin", + "scenario": "Storage nodes list loads for System Admin", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-007 Storage nodes list loads for System Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-008", + "variantId": "V25-DOC-STOR-008", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Download requests list loads", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-008 Download requests list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-009", + "variantId": "V25-DOC-STOR-009", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Storage agent jobs endpoint requires auth", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-009 Storage agent jobs endpoint requires auth", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-010", + "variantId": "V25-DOC-STOR-010", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Storage agent download-requests endpoint requires auth", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-010 Storage agent download-requests endpoint requires auth", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-011", + "variantId": "V25-DOC-STOR-011", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Storage node toggle CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-011 Storage node toggle CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-012", + "variantId": "V25-DOC-STOR-012", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "Staff", + "scenario": "Staff cannot access branch storage dashboard", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-012 Staff cannot access branch storage dashboard", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-013", + "variantId": "V25-DOC-STOR-013", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "Client", + "scenario": "Client cannot access permanent vault admin", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-013 Client cannot access permanent vault admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-014", + "variantId": "V25-DOC-STOR-014", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Permanent document delete CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-014 Permanent document delete CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-015", + "variantId": "V25-DOC-STOR-015", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Non-existent permanent document download is handled safely", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-015 Non-existent permanent document download is handled safely", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "DOC-STOR-016", + "variantId": "V25-DOC-STOR-016", + "sheet": "UAT_Consultants_Documents_v2_5", + "module": "Documents", + "role": "All roles", + "scenario": "Storage agent agent-package download requires node auth", + "steps": "Execute Playwright v2.5 module test: DOC-STOR-016 Storage agent agent-package download requires node auth", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "UAT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-001", + "variantId": "V25-EMP-HR-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "Firm Admin", + "scenario": "HR dashboard loads without error for Firm Admin", + "steps": "Execute Playwright v2.5 module test: EMP-HR-001 HR dashboard loads without error for Firm Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-002", + "variantId": "V25-EMP-HR-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "Firm Admin", + "scenario": "Employee list loads for Firm Admin", + "steps": "Execute Playwright v2.5 module test: EMP-HR-002 Employee list loads for Firm Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-003", + "variantId": "V25-EMP-HR-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "All roles", + "scenario": "Employee list search does not crash", + "steps": "Execute Playwright v2.5 module test: EMP-HR-003 Employee list search does not crash", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-004", + "variantId": "V25-EMP-HR-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "All roles", + "scenario": "Employee create form loads", + "steps": "Execute Playwright v2.5 module test: EMP-HR-004 Employee create form loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-005", + "variantId": "V25-EMP-HR-005", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "All roles", + "scenario": "Employee create with blank form shows validation, not 500", + "steps": "Execute Playwright v2.5 module test: EMP-HR-005 Employee create with blank form shows validation, not 500", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "UAT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-006", + "variantId": "V25-EMP-HR-006", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "All roles", + "scenario": "Employee detail page loads", + "steps": "Execute Playwright v2.5 module test: EMP-HR-006 Employee detail page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-HR-007", + "variantId": "V25-EMP-HR-007", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - HR Admin", + "role": "All roles", + "scenario": "Employee edit form loads", + "steps": "Execute Playwright v2.5 module test: EMP-HR-007 Employee edit form loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ATT-001", + "variantId": "V25-EMP-ATT-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Attendance", + "role": "All roles", + "scenario": "HR attendance list loads", + "steps": "Execute Playwright v2.5 module test: EMP-ATT-001 HR attendance list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ATT-002", + "variantId": "V25-EMP-ATT-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Attendance", + "role": "All roles", + "scenario": "HR attendance list with filters does not crash", + "steps": "Execute Playwright v2.5 module test: EMP-ATT-002 HR attendance list with filters does not crash", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ATT-003", + "variantId": "V25-EMP-ATT-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Attendance", + "role": "All roles", + "scenario": "Manual attendance CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-ATT-003 Manual attendance CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ATT-004", + "variantId": "V25-EMP-ATT-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Attendance", + "role": "All roles", + "scenario": "Attendance review CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-ATT-004 Attendance review CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-LEAVE-001", + "variantId": "V25-EMP-LEAVE-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Leave", + "role": "All roles", + "scenario": "Leave types list loads", + "steps": "Execute Playwright v2.5 module test: EMP-LEAVE-001 Leave types list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-LEAVE-002", + "variantId": "V25-EMP-LEAVE-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Leave", + "role": "All roles", + "scenario": "Leave balances list loads", + "steps": "Execute Playwright v2.5 module test: EMP-LEAVE-002 Leave balances list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-LEAVE-003", + "variantId": "V25-EMP-LEAVE-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Leave", + "role": "All roles", + "scenario": "Leave requests list (pending) loads", + "steps": "Execute Playwright v2.5 module test: EMP-LEAVE-003 Leave requests list (pending) loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-LEAVE-004", + "variantId": "V25-EMP-LEAVE-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Leave", + "role": "All roles", + "scenario": "Leave requests list (all) loads", + "steps": "Execute Playwright v2.5 module test: EMP-LEAVE-004 Leave requests list (all) loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-LEAVE-005", + "variantId": "V25-EMP-LEAVE-005", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Leave", + "role": "All roles", + "scenario": "Leave review CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-LEAVE-005 Leave review CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-OB-001", + "variantId": "V25-EMP-OB-001", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-OB", + "role": "All roles", + "scenario": "Onboarding checklist template page loads", + "steps": "Execute Playwright v2.5 module test: EMP-OB-001 Onboarding checklist template page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-OB-002", + "variantId": "V25-EMP-OB-002", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-OB", + "role": "All roles", + "scenario": "Onboarding tasks list loads", + "steps": "Execute Playwright v2.5 module test: EMP-OB-002 Onboarding tasks list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-OB-003", + "variantId": "V25-EMP-OB-003", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-OB", + "role": "All roles", + "scenario": "Offboarding requests list loads", + "steps": "Execute Playwright v2.5 module test: EMP-OB-003 Offboarding requests list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-OB-004", + "variantId": "V25-EMP-OB-004", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-OB", + "role": "All roles", + "scenario": "Registration requests list loads", + "steps": "Execute Playwright v2.5 module test: EMP-OB-004 Registration requests list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-OB-005", + "variantId": "V25-EMP-OB-005", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-OB", + "role": "All roles", + "scenario": "Registration request detail loads", + "steps": "Execute Playwright v2.5 module test: EMP-OB-005 Registration request detail loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-DOC-001", + "variantId": "V25-EMP-DOC-001", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-DOC", + "role": "All roles", + "scenario": "Document types list loads", + "steps": "Execute Playwright v2.5 module test: EMP-DOC-001 Document types list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-DOC-002", + "variantId": "V25-EMP-DOC-002", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-DOC", + "role": "All roles", + "scenario": "Employee documents list loads", + "steps": "Execute Playwright v2.5 module test: EMP-DOC-002 Employee documents list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-DOC-003", + "variantId": "V25-EMP-DOC-003", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-DOC", + "role": "All roles", + "scenario": "Executable upload to employee documents is blocked", + "steps": "Execute Playwright v2.5 module test: EMP-DOC-003 Executable upload to employee documents is blocked", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-DOC-004", + "variantId": "V25-EMP-DOC-004", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-DOC", + "role": "All roles", + "scenario": "Document review CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-DOC-004 Document review CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-001", + "variantId": "V25-EMP-PAY-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Salary structures list loads", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-001 Salary structures list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-002", + "variantId": "V25-EMP-PAY-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Payroll runs list loads", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-002 Payroll runs list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-003", + "variantId": "V25-EMP-PAY-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Payslips list loads", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-003 Payslips list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-004", + "variantId": "V25-EMP-PAY-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Payslips filtered by run loads", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-004 Payslips filtered by run loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-005", + "variantId": "V25-EMP-PAY-005", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Payroll run generate CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-005 Payroll run generate CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-PAY-006", + "variantId": "V25-EMP-PAY-006", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Payroll", + "role": "All roles", + "scenario": "Payroll run approve CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-PAY-006 Payroll run approve CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-WORK-001", + "variantId": "V25-EMP-WORK-001", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-WORK", + "role": "All roles", + "scenario": "Work allocation dashboard loads", + "steps": "Execute Playwright v2.5 module test: EMP-WORK-001 Work allocation dashboard loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-WORK-002", + "variantId": "V25-EMP-WORK-002", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-WORK", + "role": "All roles", + "scenario": "Engagement progress dashboard loads", + "steps": "Execute Playwright v2.5 module test: EMP-WORK-002 Engagement progress dashboard loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-WORK-003", + "variantId": "V25-EMP-WORK-003", + "sheet": "UAT_Employees_v2_5", + "module": "EMP-WORK", + "role": "All roles", + "scenario": "HR Excel imports page loads", + "steps": "Execute Playwright v2.5 module test: EMP-WORK-003 HR Excel imports page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-001", + "variantId": "V25-EMP-ESS-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "Staff", + "scenario": "ESS dashboard loads for Staff", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-001 ESS dashboard loads for Staff", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-002", + "variantId": "V25-EMP-ESS-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My attendance page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-002 My attendance page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-003", + "variantId": "V25-EMP-ESS-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My leave page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-003 My leave page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-004", + "variantId": "V25-EMP-ESS-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My documents page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-004 My documents page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-005", + "variantId": "V25-EMP-ESS-005", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My payslips page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-005 My payslips page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-006", + "variantId": "V25-EMP-ESS-006", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My work kanban loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-006 My work kanban loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-007", + "variantId": "V25-EMP-ESS-007", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My offboarding page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-007 My offboarding page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-008", + "variantId": "V25-EMP-ESS-008", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "My profile page loads", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-008 My profile page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-009", + "variantId": "V25-EMP-ESS-009", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "Employee registration form loads for unlinked user", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-009 Employee registration form loads for unlinked user", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-ESS-010", + "variantId": "V25-EMP-ESS-010", + "sheet": "UAT_Employees_v2_5", + "module": "Employee Self Service", + "role": "All roles", + "scenario": "Leave application CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: EMP-ESS-010 Leave application CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-RBAC-001", + "variantId": "V25-EMP-RBAC-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - RBAC", + "role": "Staff", + "scenario": "Staff cannot access HR dashboard", + "steps": "Execute Playwright v2.5 module test: EMP-RBAC-001 Staff cannot access HR dashboard", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-RBAC-002", + "variantId": "V25-EMP-RBAC-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - RBAC", + "role": "Staff", + "scenario": "Staff cannot access employee list", + "steps": "Execute Playwright v2.5 module test: EMP-RBAC-002 Staff cannot access employee list", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-RBAC-003", + "variantId": "V25-EMP-RBAC-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - RBAC", + "role": "Staff", + "scenario": "Staff cannot access payroll runs", + "steps": "Execute Playwright v2.5 module test: EMP-RBAC-003 Staff cannot access payroll runs", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-RBAC-004", + "variantId": "V25-EMP-RBAC-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - RBAC", + "role": "Client", + "scenario": "Client cannot access employee portal", + "steps": "Execute Playwright v2.5 module test: EMP-RBAC-004 Client cannot access employee portal", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-RBAC-005", + "variantId": "V25-EMP-RBAC-005", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - RBAC", + "role": "Anonymous", + "scenario": "Anonymous cannot access any employee route", + "steps": "Execute Playwright v2.5 module test: EMP-RBAC-005 Anonymous cannot access any employee route", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-SEC-001", + "variantId": "V25-EMP-SEC-001", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Security", + "role": "All roles", + "scenario": "Non-existent employee ID returns safe response", + "steps": "Execute Playwright v2.5 module test: EMP-SEC-001 Non-existent employee ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-SEC-002", + "variantId": "V25-EMP-SEC-002", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Security", + "role": "All roles", + "scenario": "Non-existent payroll run ID returns safe response", + "steps": "Execute Playwright v2.5 module test: EMP-SEC-002 Non-existent payroll run ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-SEC-003", + "variantId": "V25-EMP-SEC-003", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Security", + "role": "All roles", + "scenario": "HR import commit without preview session is rejected safely", + "steps": "Execute Playwright v2.5 module test: EMP-SEC-003 HR import commit without preview session is rejected safely", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "UAT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "EMP-SEC-004", + "variantId": "V25-EMP-SEC-004", + "sheet": "UAT_Employees_v2_5", + "module": "Employees - Security", + "role": "Staff", + "scenario": "Payroll run generate by Staff is blocked", + "steps": "Execute Playwright v2.5 module test: EMP-SEC-004 Payroll run generate by Staff is blocked", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-001", + "variantId": "V25-CASE-001", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case list loads", + "steps": "Execute Playwright v2.5 module test: CASE-001 Notice case list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-002", + "variantId": "V25-CASE-002", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case detail page loads", + "steps": "Execute Playwright v2.5 module test: CASE-002 Notice case detail page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-003", + "variantId": "V25-CASE-003", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case edit page loads", + "steps": "Execute Playwright v2.5 module test: CASE-003 Notice case edit page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-004", + "variantId": "V25-CASE-004", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case events sub-page loads", + "steps": "Execute Playwright v2.5 module test: CASE-004 Notice case events sub-page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-005", + "variantId": "V25-CASE-005", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case hearings sub-page loads", + "steps": "Execute Playwright v2.5 module test: CASE-005 Notice case hearings sub-page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-006", + "variantId": "V25-CASE-006", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case orders sub-page loads", + "steps": "Execute Playwright v2.5 module test: CASE-006 Notice case orders sub-page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-007", + "variantId": "V25-CASE-007", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case document download requires auth", + "steps": "Execute Playwright v2.5 module test: CASE-007 Notice case document download requires auth", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-008", + "variantId": "V25-CASE-008", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case document delete CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: CASE-008 Notice case document delete CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-009", + "variantId": "V25-CASE-009", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Notice case upload CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: CASE-009 Notice case upload CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-010", + "variantId": "V25-CASE-010", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "Client", + "scenario": "Client cannot access notice cases", + "steps": "Execute Playwright v2.5 module test: CASE-010 Client cannot access notice cases", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-011", + "variantId": "V25-CASE-011", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "IDOR: cross-case document download blocked", + "steps": "Execute Playwright v2.5 module test: CASE-011 IDOR: cross-case document download blocked", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "CASE-012", + "variantId": "V25-CASE-012", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Notice Cases", + "role": "All roles", + "scenario": "Non-existent case detail returns safe response", + "steps": "Execute Playwright v2.5 module test: CASE-012 Non-existent case detail returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-001", + "variantId": "V25-SVC-001", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk imports page loads", + "steps": "Execute Playwright v2.5 module test: SVC-001 Bulk imports page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-002", + "variantId": "V25-SVC-002", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk imports service-master template downloads", + "steps": "Execute Playwright v2.5 module test: SVC-002 Bulk imports service-master template downloads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-003", + "variantId": "V25-SVC-003", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk imports engagement-assignments template downloads", + "steps": "Execute Playwright v2.5 module test: SVC-003 Bulk imports engagement-assignments template downloads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-004", + "variantId": "V25-SVC-004", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk imports firm-task-templates template downloads", + "steps": "Execute Playwright v2.5 module test: SVC-004 Bulk imports firm-task-templates template downloads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-005", + "variantId": "V25-SVC-005", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk import preview CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: SVC-005 Bulk import preview CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-006", + "variantId": "V25-SVC-006", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Subscription detail loads", + "steps": "Execute Playwright v2.5 module test: SVC-006 Subscription detail loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-007", + "variantId": "V25-SVC-007", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Subscription edit page loads", + "steps": "Execute Playwright v2.5 module test: SVC-007 Subscription edit page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-008", + "variantId": "V25-SVC-008", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Subscription lock CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: SVC-008 Subscription lock CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-009", + "variantId": "V25-SVC-009", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Bulk lock CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: SVC-009 Bulk lock CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-010", + "variantId": "V25-SVC-010", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Subscription generate CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: SVC-010 Subscription generate CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-011", + "variantId": "V25-SVC-011", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "Client", + "scenario": "Client cannot access services admin", + "steps": "Execute Playwright v2.5 module test: SVC-011 Client cannot access services admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-012", + "variantId": "V25-SVC-012", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Non-existent subscription ID returns safe response", + "steps": "Execute Playwright v2.5 module test: SVC-012 Non-existent subscription ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "SVC-013", + "variantId": "V25-SVC-013", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Services", + "role": "All roles", + "scenario": "Task bulk-update CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: SVC-013 Task bulk-update CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-001", + "variantId": "V25-WORK-001", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Engagement work detail page loads", + "steps": "Execute Playwright v2.5 module test: WORK-001 Engagement work detail page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-002", + "variantId": "V25-WORK-002", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Task status update CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: WORK-002 Task status update CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-003", + "variantId": "V25-WORK-003", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Task comment POST CSRF-less is rejected", + "steps": "Execute Playwright v2.5 module test: WORK-003 Task comment POST CSRF-less is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-004", + "variantId": "V25-WORK-004", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Task comments list page loads", + "steps": "Execute Playwright v2.5 module test: WORK-004 Task comments list page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-005", + "variantId": "V25-WORK-005", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Task edit page loads", + "steps": "Execute Playwright v2.5 module test: WORK-005 Task edit page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-006", + "variantId": "V25-WORK-006", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Task upload CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: WORK-006 Task upload CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-007", + "variantId": "V25-WORK-007", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "All roles", + "scenario": "Non-existent task returns safe response", + "steps": "Execute Playwright v2.5 module test: WORK-007 Non-existent task returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-008", + "variantId": "V25-WORK-008", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "Staff", + "scenario": "IDOR: Staff cannot update task assigned to another user via direct POST", + "steps": "Execute Playwright v2.5 module test: WORK-008 IDOR: Staff cannot update task assigned to another user via direct POST", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "WORK-009", + "variantId": "V25-WORK-009", + "sheet": "UAT_NoticeCases_Services_Work_v2_5", + "module": "Workspaces", + "role": "Client", + "scenario": "Client cannot access work detail", + "steps": "Execute Playwright v2.5 module test: WORK-009 Client cannot access work detail", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-001", + "variantId": "V25-PART-001", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Partner dashboard loads", + "steps": "Execute Playwright v2.5 module test: PART-001 Partner dashboard loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-002", + "variantId": "V25-PART-002", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Partner client list loads", + "steps": "Execute Playwright v2.5 module test: PART-002 Partner client list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-003", + "variantId": "V25-PART-003", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Partner reviews list loads", + "steps": "Execute Playwright v2.5 module test: PART-003 Partner reviews list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-004", + "variantId": "V25-PART-004", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Partner task review page loads", + "steps": "Execute Playwright v2.5 module test: PART-004 Partner task review page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-005", + "variantId": "V25-PART-005", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Partner task review CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: PART-005 Partner task review CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-006", + "variantId": "V25-PART-006", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Staff cannot access partner dashboard", + "steps": "Execute Playwright v2.5 module test: PART-006 Staff cannot access partner dashboard", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-007", + "variantId": "V25-PART-007", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Client cannot access partner reviews", + "steps": "Execute Playwright v2.5 module test: PART-007 Client cannot access partner reviews", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-008", + "variantId": "V25-PART-008", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "Partner", + "scenario": "Anonymous access to partner dashboard is blocked", + "steps": "Execute Playwright v2.5 module test: PART-008 Anonymous access to partner dashboard is blocked", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "PART-009", + "variantId": "V25-PART-009", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Partners", + "role": "All roles", + "scenario": "Non-existent task review ID returns safe response", + "steps": "Execute Playwright v2.5 module test: PART-009 Non-existent task review ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-001", + "variantId": "V25-BILL-001", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "Firm Admin", + "scenario": "Invoice list loads for Firm Admin", + "steps": "Execute Playwright v2.5 module test: BILL-001 Invoice list loads for Firm Admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-002", + "variantId": "V25-BILL-002", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice create form loads", + "steps": "Execute Playwright v2.5 module test: BILL-002 Invoice create form loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-003", + "variantId": "V25-BILL-003", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice detail page loads", + "steps": "Execute Playwright v2.5 module test: BILL-003 Invoice detail page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-004", + "variantId": "V25-BILL-004", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice print page loads", + "steps": "Execute Playwright v2.5 module test: BILL-004 Invoice print page loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-005", + "variantId": "V25-BILL-005", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice payments subpage loads", + "steps": "Execute Playwright v2.5 module test: BILL-005 Invoice payments subpage loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-006", + "variantId": "V25-BILL-006", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Payment receipt loads", + "steps": "Execute Playwright v2.5 module test: BILL-006 Payment receipt loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-007", + "variantId": "V25-BILL-007", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Payments list loads", + "steps": "Execute Playwright v2.5 module test: BILL-007 Payments list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-008", + "variantId": "V25-BILL-008", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Fee structures list loads", + "steps": "Execute Playwright v2.5 module test: BILL-008 Fee structures list loads", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Medium", + "type": "Regression", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-009", + "variantId": "V25-BILL-009", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice post CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: BILL-009 Invoice post CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-010", + "variantId": "V25-BILL-010", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "New payment CSRF-less POST is rejected", + "steps": "Execute Playwright v2.5 module test: BILL-010 New payment CSRF-less POST is rejected", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-011", + "variantId": "V25-BILL-011", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "Staff", + "scenario": "Staff cannot access invoice list", + "steps": "Execute Playwright v2.5 module test: BILL-011 Staff cannot access invoice list", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-012", + "variantId": "V25-BILL-012", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "Client", + "scenario": "Client cannot access billing admin", + "steps": "Execute Playwright v2.5 module test: BILL-012 Client cannot access billing admin", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-013", + "variantId": "V25-BILL-013", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "Anonymous", + "scenario": "Anonymous access to billing is blocked", + "steps": "Execute Playwright v2.5 module test: BILL-013 Anonymous access to billing is blocked", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-014", + "variantId": "V25-BILL-014", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Non-existent invoice ID returns safe response", + "steps": "Execute Playwright v2.5 module test: BILL-014 Non-existent invoice ID returns safe response", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "Critical", + "type": "VAPT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-015", + "variantId": "V25-BILL-015", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Invoice create with blank form shows validation, not 500", + "steps": "Execute Playwright v2.5 module test: BILL-015 Invoice create with blank form shows validation, not 500", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "UAT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + }, + { + "sourceId": "BILL-016", + "variantId": "V25-BILL-016", + "sheet": "UAT_Partners_Billing_v2_5", + "module": "Billing", + "role": "All roles", + "scenario": "Fee structure import template download works", + "steps": "Execute Playwright v2.5 module test: BILL-016 Fee structure import template download works", + "expected": "Route/action completes safely according to ERP permission and validation rules; no server 500 or unauthorized data exposure.", + "priority": "High", + "type": "UAT", + "route": "", + "variantName": "v2.5 new module automated check", + "variantType": "v2_5", + "automation": "automated", + "manualReason": "" + } +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3fed7ad..094736f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "playwright-full-erp-uat-vapt-suite-v2-3", - "version": "2.4.1", + "version": "2.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-full-erp-uat-vapt-suite-v2-3", - "version": "2.4.1", + "version": "2.5.1", "dependencies": { "@playwright/test": "^1.48.2", "dotenv": "^16.4.5", "imapflow": "^1.4.2", "xlsx": "^0.18.5" - } + }, + "description": "SQLite-first full ERP UAT/VAPT suite v2.5 with v2.4.2 matrix coverage plus employees, partners, billing, consultants, documents, notice cases, services and work depth tests." }, "node_modules/@pinojs/redact": { "version": "0.4.0", @@ -450,4 +451,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 549dcaa..61cd27b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "playwright-full-erp-uat-vapt-suite-v2-3", - "version": "2.4.1", + "version": "2.5.1", "private": true, - "description": "Full Excel-mapped UAT + VAPT suite for Audit Firm ERP with v2.0.4 hardening, v2.4 FY-lock scaffolding, matrix-reconciled checklist, and IMAP OTP support.", + "description": "SQLite-first full ERP UAT/VAPT suite v2.5.1 with v2.4.2 matrix, v2.5 modules, and additional deep ERP gap tests.", "scripts": { "test": "playwright test", "test:excel": "playwright test tests/full-excel-variants.spec.js", @@ -14,7 +14,30 @@ "show-report": "playwright show-report", "test:v204": "playwright test tests/v204-security-additions.spec.js", "test:all": "playwright test", - "test:fy": "playwright test tests/fy-lock-backup.spec.js" + "test:fy": "playwright test tests/fy-lock-backup.spec.js", + "db:init": "python3 scripts/sqlite_db.py init", + "db:import-cases": "python3 scripts/sqlite_db.py import-cases", + "db:summary": "python3 scripts/sqlite_db.py summary", + "db:export-json": "python3 scripts/sqlite_db.py export-json --output results/merged-results.json", + "db:export-excel": "python3 scripts/sqlite_db.py export-xlsx --output results/UAT_VAPT_SQLite_Export.xlsx", + "test:sqlite": "npm run db:import-cases; npx playwright test --workers=1; npm run db:export-json; npm run db:export-excel", + "test:employees": "playwright test tests/employees.spec.js", + "test:partners-billing": "playwright test tests/partners-billing.spec.js", + "test:consultants-docs": "playwright test tests/consultants-documents.spec.js", + "test:cases-services-work": "playwright test tests/noticecases-services-work.spec.js", + "test:new-modules": "playwright test tests/employees.spec.js tests/partners-billing.spec.js tests/consultants-documents.spec.js tests/noticecases-services-work.spec.js", + "test:v251-additions": "playwright test tests/api-auth-rbac.spec.js tests/audit-logs.spec.js tests/billing-business-rules.spec.js tests/document-security-deep.spec.js tests/email-integration.spec.js tests/employee-hr-business-rules.spec.js tests/marketplace-leads.spec.js tests/notice-case-business-rules.spec.js tests/platform-billing.spec.js tests/system-settings-tenancy.spec.js tests/work-lifecycle-e2e.spec.js", + "test:email": "playwright test tests/email-integration.spec.js", + "test:marketplace": "playwright test tests/marketplace-leads.spec.js", + "test:platform-billing": "playwright test tests/platform-billing.spec.js", + "test:system-settings": "playwright test tests/system-settings-tenancy.spec.js", + "test:api-auth-rbac": "playwright test tests/api-auth-rbac.spec.js", + "test:audit-logs": "playwright test tests/audit-logs.spec.js", + "test:document-security-deep": "playwright test tests/document-security-deep.spec.js", + "test:work-lifecycle": "playwright test tests/work-lifecycle-e2e.spec.js", + "test:billing-rules": "playwright test tests/billing-business-rules.spec.js", + "test:notice-rules": "playwright test tests/notice-case-business-rules.spec.js", + "test:hr-rules": "playwright test tests/employee-hr-business-rules.spec.js" }, "dependencies": { "@playwright/test": "^1.48.2", @@ -22,4 +45,4 @@ "imapflow": "^1.4.2", "xlsx": "^0.18.5" } -} +} \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index 9b64c86..f1ca42c 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -11,6 +11,7 @@ module.exports = defineConfig({ reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], ['json', { outputFile: 'results/playwright-results.json' }], + ['./reporters/sqlite-reporter.js', { dbPath: 'results/uat_vapt_results.sqlite' }], ['list'] ], use: { diff --git a/reporters/sqlite-reporter.js b/reporters/sqlite-reporter.js new file mode 100644 index 0000000..3e6e7e9 --- /dev/null +++ b/reporters/sqlite-reporter.js @@ -0,0 +1,94 @@ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +function runPython(args, inputObj) { + const result = spawnSync('python3', ['scripts/sqlite_db.py', ...args], { + cwd: process.cwd(), + input: inputObj ? JSON.stringify(inputObj) : undefined, + encoding: 'utf8', + env: process.env, + maxBuffer: 1024 * 1024 * 10, + }); + if (result.status !== 0) { + const msg = result.stderr || result.stdout || 'Unknown sqlite_db.py error'; + throw new Error(msg); + } + const out = (result.stdout || '').trim(); + return out ? JSON.parse(out) : {}; +} + +class SqliteReporter { + constructor(options = {}) { + this.dbPath = options.dbPath || process.env.SQLITE_RESULTS_DB || 'results/uat_vapt_results.sqlite'; + this.runName = options.runName || process.env.TEST_RUN_NAME || `Run ${new Date().toISOString()}`; + this.runId = null; + } + + onBegin(config, suite) { + fs.mkdirSync(path.dirname(this.dbPath), { recursive: true }); + runPython(['--db', this.dbPath, 'init']); + const matrixPath = process.env.TEST_MATRIX_JSON || 'data/generated-test-matrix.json'; + if (fs.existsSync(matrixPath)) { + runPython(['--db', this.dbPath, '--matrix', matrixPath, 'import-cases']); + } + const started = runPython(['--db', this.dbPath, '--run-name', this.runName, '--base-url', process.env.BASE_URL || '', 'begin-run']); + this.runId = started.run_id; + console.log(`SQLite reporter writing to ${this.dbPath}; run_id=${this.runId}`); + } + + extractVariantId(test) { + const titlePath = test.titlePath ? test.titlePath() : [test.title]; + const fullTitle = titlePath.join(' '); + const bracket = fullTitle.match(/\[([^\]]+)\]/); + if (bracket) return bracket[1]; + const fallback = fullTitle.match(/((?:V25-)?[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/); + return fallback ? fallback[1] : ''; + } + + onTestEnd(test, result) { + const location = test.location || {}; + const attachments = (result.attachments || []).map(a => ({ + name: a.name || '', + contentType: a.contentType || '', + path: a.path || '', + })); + const record = { + run_id: this.runId, + variant_id: this.extractVariantId(test), + title: test.title, + title_path: test.titlePath ? test.titlePath() : [test.title], + project_name: test.parent && test.parent.project ? (test.parent.project()?.name || '') : '', + location, + status: result.status, + expected_status: test.expectedStatus, + duration_ms: result.duration || 0, + retry: result.retry || 0, + error: result.error ? { message: result.error.message || '', stack: result.error.stack || '' } : null, + stdout: (result.stdout || []).map(x => Buffer.isBuffer(x) ? x.toString('utf8') : String(x)).join('\n'), + stderr: (result.stderr || []).map(x => Buffer.isBuffer(x) ? x.toString('utf8') : String(x)).join('\n'), + attachments, + started_at: result.startTime ? result.startTime.toISOString() : null, + finished_at: new Date().toISOString(), + }; + try { + runPython(['--db', this.dbPath, 'record-result'], record); + } catch (e) { + console.error('Failed to record SQLite test result:', e.message); + } + } + + onEnd(result) { + if (!this.runId) return; + try { + runPython(['--db', this.dbPath, '--run-id', String(this.runId), '--status', result.status || 'completed', 'finish-run']); + runPython(['--db', this.dbPath, '--run-id', String(this.runId), '--output', 'results/merged-results.json', 'export-json']); + runPython(['--db', this.dbPath, '--run-id', String(this.runId), '--output', 'results/UAT_VAPT_SQLite_Export.xlsx', 'export-xlsx']); + console.log(`SQLite run ${this.runId} finished and exported.`); + } catch (e) { + console.error('Failed to finish/export SQLite run:', e.message); + } + } +} + +module.exports = SqliteReporter; diff --git a/scripts/merge-results.js b/scripts/merge-results.js index 3b2acc3..5942f52 100644 --- a/scripts/merge-results.js +++ b/scripts/merge-results.js @@ -1,8 +1,30 @@ const fs = require('fs'); const path = require('path'); +const { spawnSync } = require('child_process'); + +const sqliteDb = process.env.SQLITE_RESULTS_DB || 'results/uat_vapt_results.sqlite'; +const outPath = 'results/merged-results.json'; + +function exportFromSqliteIfAvailable() { + if (!fs.existsSync(sqliteDb)) return false; + const result = spawnSync('python3', ['scripts/sqlite_db.py', '--db', sqliteDb, '--output', outPath, 'export-json'], { + cwd: process.cwd(), + encoding: 'utf8', + env: process.env, + }); + if (result.status === 0) { + console.log((result.stdout || '').trim()); + console.log(`Wrote ${outPath} from SQLite database ${sqliteDb}`); + return true; + } + console.warn('SQLite export failed, falling back to Playwright JSON:', result.stderr || result.stdout); + return false; +} + +if (exportFromSqliteIfAvailable()) process.exit(0); + const matrix = JSON.parse(fs.readFileSync('data/generated-test-matrix.json','utf8')); const resultPath = 'results/playwright-results.json'; -const outPath = 'results/merged-results.json'; let results = {}; if (fs.existsSync(resultPath)) { const raw = JSON.parse(fs.readFileSync(resultPath,'utf8')); @@ -21,4 +43,4 @@ if (fs.existsSync(resultPath)) { const merged = matrix.map(v => ({...v, result: results[v.variantId] || (v.automation === 'manual' ? 'manual' : 'not_run')})); fs.mkdirSync(path.dirname(outPath), {recursive:true}); fs.writeFileSync(outPath, JSON.stringify(merged,null,2)); -console.log(`Wrote ${outPath}`); +console.log(`Wrote ${outPath} from Playwright JSON fallback`); diff --git a/scripts/result-server.js b/scripts/result-server.js index a76e22c..b168d49 100644 --- a/scripts/result-server.js +++ b/scripts/result-server.js @@ -1,38 +1,23 @@ -const http = require("http"); -const fs = require("fs"); -const path = require("path"); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); const PORT = Number(process.env.RESULT_SERVER_PORT || 3000); -const ROOT = process.env.RESULT_ROOT || "/tests"; +const ROOT = process.env.RESULT_ROOT || process.cwd(); const FILES = [ - { - title: "Updated Excel Result", - path: "results/Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2_4_Results.xlsx", - type: "Excel", - }, - { - title: "Merged Results JSON", - path: "results/merged-results.json", - type: "JSON", - }, - { - title: "API Check Results JSON", - path: "results/api-check-results.json", - type: "JSON", - }, - { - title: "Playwright Report Index", - path: "playwright-report/index.html", - type: "HTML", - }, + { title: 'SQLite Database', path: 'results/uat_vapt_results.sqlite', type: 'SQLite' }, + { title: 'SQLite Excel Export', path: 'results/UAT_VAPT_SQLite_Export.xlsx', type: 'Excel' }, + { title: 'Updated Master Excel Result', path: 'results/Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2_4_Results.xlsx', type: 'Excel' }, + { title: 'Merged Results JSON', path: 'results/merged-results.json', type: 'JSON' }, + { title: 'Playwright Results JSON', path: 'results/playwright-results.json', type: 'JSON' }, + { title: 'API Check Results JSON', path: 'results/api-check-results.json', type: 'JSON' }, + { title: 'Playwright Report Index', path: 'playwright-report/index.html', type: 'HTML' }, ]; function safeJoin(relativePath) { const fullPath = path.resolve(ROOT, relativePath); - if (!fullPath.startsWith(path.resolve(ROOT))) { - throw new Error("Invalid path"); - } + if (!fullPath.startsWith(path.resolve(ROOT))) throw new Error('Invalid path'); return fullPath; } @@ -40,218 +25,50 @@ function fileInfo(relativePath) { try { const full = safeJoin(relativePath); const stat = fs.statSync(full); - return { - exists: true, - size: stat.size, - mtime: stat.mtime, - }; - } catch { - return { - exists: false, - size: 0, - mtime: null, - }; - } + return { exists: true, size: stat.size, mtime: stat.mtime }; + } catch { return { exists: false, size: 0, mtime: null }; } } function formatBytes(bytes) { - if (!bytes) return "-"; - const units = ["B", "KB", "MB", "GB"]; - let size = bytes; - let unit = 0; - while (size >= 1024 && unit < units.length - 1) { - size = size / 1024; - unit++; - } - return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`; + if (!bytes) return '-'; + const units = ['B','KB','MB','GB']; let size = bytes; let u = 0; + while (size >= 1024 && u < units.length - 1) { size /= 1024; u++; } + return `${size.toFixed(u === 0 ? 0 : 2)} ${units[u]}`; } function contentType(filePath) { - if (filePath.endsWith(".xlsx")) { - return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - } - if (filePath.endsWith(".json")) return "application/json"; - if (filePath.endsWith(".html")) return "text/html; charset=utf-8"; - if (filePath.endsWith(".png")) return "image/png"; - if (filePath.endsWith(".webm")) return "video/webm"; - if (filePath.endsWith(".zip")) return "application/zip"; - return "application/octet-stream"; + if (filePath.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + if (filePath.endsWith('.sqlite') || filePath.endsWith('.db')) return 'application/vnd.sqlite3'; + if (filePath.endsWith('.json')) return 'application/json'; + if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'; + if (filePath.endsWith('.png')) return 'image/png'; + if (filePath.endsWith('.webm')) return 'video/webm'; + if (filePath.endsWith('.zip')) return 'application/zip'; + return 'application/octet-stream'; } function renderHome() { - const rows = FILES.map((item) => { + const rows = FILES.map(item => { const info = fileInfo(item.path); - const status = info.exists ? "Available" : "Missing"; - const downloadLink = info.exists - ? `Download` - : `Not available`; - - return ` - - ${item.title} - ${item.type} - ${status} - ${formatBytes(info.size)} - ${info.mtime ? info.mtime.toLocaleString() : "-"} - ${downloadLink} - - `; - }).join(""); - - return ` - - - - Playwright UAT/VAPT Results - - - -
-

Playwright UAT/VAPT Results

-
Last refreshed: ${new Date().toLocaleString()}
- - - - - - - - - - - - - ${rows} -
FileTypeStatusSizeModifiedAction
- -
- Keep this page private. Test reports may contain screenshots, URLs, user emails, and error details. - Recommended Coolify port: 3000. -
-
- -`; + const action = info.exists ? `Download` : 'Missing'; + return `${item.title}${item.type}${info.exists ? 'Available':'Missing'}${formatBytes(info.size)}${info.mtime ? info.mtime.toLocaleString() : '-'}${action}`; + }).join(''); + return `Playwright UAT/VAPT Results

Playwright UAT/VAPT Results

Last refreshed: ${new Date().toLocaleString()}
${rows}
FileTypeStatusSizeModifiedAction
Keep this page private. Reports may contain screenshots, URLs, user emails, and error details. Recommended port: 3000.
`; } function sendFile(res, relativePath) { let fullPath; - try { - fullPath = safeJoin(relativePath); - } catch { - res.writeHead(400); - res.end("Invalid file path"); - return; - } - - if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) { - res.writeHead(404); - res.end("File not found"); - return; - } - - res.writeHead(200, { - "Content-Type": contentType(fullPath), - "Content-Disposition": `attachment; filename="${path.basename(fullPath)}"`, - }); - + try { fullPath = safeJoin(relativePath); } catch { res.writeHead(400); res.end('Invalid file path'); return; } + if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) { res.writeHead(404); res.end('File not found'); return; } + res.writeHead(200, { 'Content-Type': contentType(fullPath), 'Content-Disposition': `attachment; filename="${path.basename(fullPath)}"` }); fs.createReadStream(fullPath).pipe(res); } -const server = http.createServer((req, res) => { +http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); - - if (url.pathname === "/") { - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); - res.end(renderHome()); - return; - } - - if (url.pathname === "/download") { - const file = url.searchParams.get("file"); - if (!file) { - res.writeHead(400); - res.end("Missing file parameter"); - return; - } - sendFile(res, file); - return; - } - - res.writeHead(404); - res.end("Not found"); -}); - -server.listen(PORT, "0.0.0.0", () => { - console.log(`Playwright result server running on http://0.0.0.0:${PORT}`); -}); + if (url.pathname === '/') { res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); res.end(renderHome()); return; } + if (url.pathname === '/download') { const file = url.searchParams.get('file'); if (!file) { res.writeHead(400); res.end('Missing file parameter'); return; } sendFile(res, file); return; } + res.writeHead(404); res.end('Not found'); +}).listen(PORT, '0.0.0.0', () => console.log(`Result server running on http://0.0.0.0:${PORT}`)); diff --git a/scripts/sqlite_db.py b/scripts/sqlite_db.py new file mode 100644 index 0000000..a70d0df --- /dev/null +++ b/scripts/sqlite_db.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +"""SQLite-first UAT/VAPT result database helper. + +No third-party Python packages are required. It can: + - create schema + - import generated Playwright matrix as test_cases + - create a test run + - record every Playwright test result immediately + - finish a run and update counts + - export latest run to JSON and a simple XLSX workbook +""" +import argparse +import csv +import datetime as dt +import html +import json +import os +import re +import sqlite3 +import sys +import zipfile +from pathlib import Path + +DB_DEFAULT = os.environ.get("SQLITE_RESULTS_DB", "results/uat_vapt_results.sqlite") +MATRIX_DEFAULT = os.environ.get("TEST_MATRIX_JSON", "data/generated-test-matrix.json") +EXTRA_CASES_DEFAULT = os.environ.get("EXTRA_TEST_CASES_JSON", "data/v2_5_test_cases.json,data/v2_5_1_test_cases.json") + + +def now_iso(): + return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def connect(db_path: str): + Path(db_path).parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA busy_timeout=30000") + return conn + + +def init_db(conn): + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS test_cases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id TEXT, + variant_id TEXT UNIQUE, + sheet TEXT, + module TEXT, + role TEXT, + scenario TEXT, + steps TEXT, + expected TEXT, + priority TEXT, + type TEXT, + route TEXT, + variant_name TEXT, + variant_type TEXT, + automation TEXT, + manual_reason TEXT, + is_active INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT + ); + + CREATE TABLE IF NOT EXISTS test_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_name TEXT, + base_url TEXT, + status TEXT, + started_at TEXT DEFAULT CURRENT_TIMESTAMP, + finished_at TEXT, + total_cases INTEGER DEFAULT 0, + executed_count INTEGER DEFAULT 0, + passed_count INTEGER DEFAULT 0, + failed_count INTEGER DEFAULT 0, + skipped_count INTEGER DEFAULT 0, + timedout_count INTEGER DEFAULT 0, + interrupted_count INTEGER DEFAULT 0, + not_run_count INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS test_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id INTEGER NOT NULL, + variant_id TEXT, + test_title TEXT, + title_path TEXT, + project_name TEXT, + file TEXT, + line INTEGER, + column INTEGER, + status TEXT, + expected_status TEXT, + duration_ms INTEGER, + retry INTEGER DEFAULT 0, + error_message TEXT, + error_stack TEXT, + stdout TEXT, + stderr TEXT, + started_at TEXT, + finished_at TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(run_id) REFERENCES test_runs(id), + FOREIGN KEY(variant_id) REFERENCES test_cases(variant_id) + ); + + CREATE TABLE IF NOT EXISTS test_attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + result_id INTEGER NOT NULL, + run_id INTEGER NOT NULL, + variant_id TEXT, + attachment_name TEXT, + content_type TEXT, + file_path TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(result_id) REFERENCES test_results(id), + FOREIGN KEY(run_id) REFERENCES test_runs(id) + ); + + CREATE INDEX IF NOT EXISTS idx_test_cases_variant_id ON test_cases(variant_id); + CREATE INDEX IF NOT EXISTS idx_test_results_run_id ON test_results(run_id); + CREATE INDEX IF NOT EXISTS idx_test_results_variant_id ON test_results(variant_id); + CREATE INDEX IF NOT EXISTS idx_test_results_status ON test_results(status); + CREATE INDEX IF NOT EXISTS idx_test_attachments_run_id ON test_attachments(run_id); + """ + ) + conn.commit() + + +def norm(v): + return "" if v is None else str(v) + + +def _load_case_rows(path: str): + if not path or not Path(path).exists(): + return [] + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def import_cases(conn, matrix_path: str): + """Import primary generated matrix plus optional v2.5/additional test-case JSON. + + matrix_path may be a comma-separated list. In addition, EXTRA_TEST_CASES_JSON + defaults to data/v2_5_test_cases.json when present. + """ + init_db(conn) + paths = [] + for part in str(matrix_path or "").split(','): + part = part.strip() + if part: + paths.append(part) + extra = EXTRA_CASES_DEFAULT + for part in str(extra or "").split(','): + part = part.strip() + if part and part not in paths: + paths.append(part) + + sql = """ + INSERT INTO test_cases ( + source_id, variant_id, sheet, module, role, scenario, steps, expected, + priority, type, route, variant_name, variant_type, automation, manual_reason, + is_active, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?) + ON CONFLICT(variant_id) DO UPDATE SET + source_id=excluded.source_id, + sheet=excluded.sheet, + module=excluded.module, + role=excluded.role, + scenario=excluded.scenario, + steps=excluded.steps, + expected=excluded.expected, + priority=excluded.priority, + type=excluded.type, + route=excluded.route, + variant_name=excluded.variant_name, + variant_type=excluded.variant_type, + automation=excluded.automation, + manual_reason=excluded.manual_reason, + is_active=1, + updated_at=excluded.updated_at + """ + ts = now_iso() + count = 0 + for path in paths: + rows = _load_case_rows(path) + for r in rows: + variant_id = r.get("variantId") or r.get("variant_id") or "" + if not variant_id: + continue + conn.execute(sql, ( + norm(r.get("sourceId") or r.get("source_id")), + norm(variant_id), + norm(r.get("sheet")), + norm(r.get("module")), + norm(r.get("role")), + norm(r.get("scenario")), + norm(r.get("steps")), + norm(r.get("expected")), + norm(r.get("priority")), + norm(r.get("type")), + norm(r.get("route")), + norm(r.get("variantName") or r.get("variant_name")), + norm(r.get("variantType") or r.get("variant_type")), + norm(r.get("automation")), + norm(r.get("manualReason") or r.get("manual_reason")), + ts, + )) + count += 1 + conn.commit() + return count + +def begin_run(conn, run_name: str, base_url: str): + init_db(conn) + total_cases = conn.execute("SELECT COUNT(*) AS c FROM test_cases WHERE is_active=1").fetchone()["c"] + cur = conn.execute( + "INSERT INTO test_runs (run_name, base_url, status, started_at, total_cases) VALUES (?, ?, ?, ?, ?)", + (run_name, base_url, "running", now_iso(), total_cases), + ) + conn.commit() + return cur.lastrowid + + +def record_result(conn, record: dict): + init_db(conn) + run_id = int(record.get("run_id") or record.get("runId") or 0) + if not run_id: + raise ValueError("run_id is required") + variant_id = record.get("variant_id") or record.get("variantId") or extract_variant_id(" ".join(record.get("title_path") or record.get("titlePath") or [])) + if isinstance(record.get("title_path"), list): + title_path = json.dumps(record.get("title_path"), ensure_ascii=False) + elif isinstance(record.get("titlePath"), list): + title_path = json.dumps(record.get("titlePath"), ensure_ascii=False) + else: + title_path = norm(record.get("title_path") or record.get("titlePath")) + + err = record.get("error") or {} + if isinstance(err, str): + error_message, error_stack = err, "" + else: + error_message = norm(err.get("message")) + error_stack = norm(err.get("stack")) + + loc = record.get("location") or {} + cur = conn.execute( + """ + INSERT INTO test_results ( + run_id, variant_id, test_title, title_path, project_name, file, line, column, + status, expected_status, duration_ms, retry, error_message, error_stack, + stdout, stderr, started_at, finished_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + run_id, + norm(variant_id), + norm(record.get("title") or record.get("test_title")), + title_path, + norm(record.get("project_name") or record.get("projectName")), + norm(loc.get("file") or record.get("file")), + int(loc.get("line") or record.get("line") or 0), + int(loc.get("column") or record.get("column") or 0), + norm(record.get("status")), + norm(record.get("expected_status") or record.get("expectedStatus")), + int(record.get("duration_ms") or record.get("duration") or 0), + int(record.get("retry") or 0), + error_message, + error_stack, + norm(record.get("stdout")), + norm(record.get("stderr")), + norm(record.get("started_at") or record.get("startedAt")), + norm(record.get("finished_at") or record.get("finishedAt") or now_iso()), + ), + ) + result_id = cur.lastrowid + for a in record.get("attachments") or []: + conn.execute( + """ + INSERT INTO test_attachments (result_id, run_id, variant_id, attachment_name, content_type, file_path) + VALUES (?, ?, ?, ?, ?, ?) + """, + (result_id, run_id, norm(variant_id), norm(a.get("name")), norm(a.get("contentType") or a.get("content_type")), norm(a.get("path") or a.get("file_path"))), + ) + conn.commit() + return result_id + + +def finish_run(conn, run_id: int, status: str): + init_db(conn) + rows = conn.execute("SELECT status, COUNT(*) AS c FROM test_results WHERE run_id=? GROUP BY status", (run_id,)).fetchall() + counts = {r["status"]: r["c"] for r in rows} + executed = sum(counts.values()) + total_cases = conn.execute("SELECT COUNT(*) AS c FROM test_cases WHERE is_active=1").fetchone()["c"] + not_run = max(total_cases - executed, 0) + conn.execute( + """ + UPDATE test_runs SET + status=?, finished_at=?, total_cases=?, executed_count=?, passed_count=?, failed_count=?, + skipped_count=?, timedout_count=?, interrupted_count=?, not_run_count=? + WHERE id=? + """, + ( + status, + now_iso(), + total_cases, + executed, + counts.get("passed", 0), + counts.get("failed", 0), + counts.get("skipped", 0), + counts.get("timedOut", 0) + counts.get("timedout", 0), + counts.get("interrupted", 0), + not_run, + run_id, + ), + ) + conn.commit() + return counts + + +def latest_run_id(conn): + row = conn.execute("SELECT id FROM test_runs ORDER BY id DESC LIMIT 1").fetchone() + return row["id"] if row else None + + +def merged_rows(conn, run_id: int): + # Use the latest result per variant_id within the run. Retries become later rows; last row wins. + sql = """ + WITH latest AS ( + SELECT tr.* + FROM test_results tr + JOIN ( + SELECT variant_id, MAX(id) AS max_id + FROM test_results + WHERE run_id=? + GROUP BY variant_id + ) x ON x.max_id = tr.id + ) + SELECT + tc.source_id AS sourceId, + tc.sheet, + tc.module, + tc.role, + tc.scenario, + tc.steps, + tc.expected, + tc.priority, + tc.type, + tc.route, + tc.variant_id AS variantId, + tc.variant_name AS variantName, + tc.variant_type AS variantType, + tc.automation, + COALESCE(latest.status, CASE WHEN tc.automation='manual' THEN 'manual' ELSE 'not_run' END) AS result, + latest.duration_ms AS durationMs, + latest.retry, + latest.error_message AS errorMessage, + latest.started_at AS startedAt, + latest.finished_at AS finishedAt + FROM test_cases tc + LEFT JOIN latest ON latest.variant_id = tc.variant_id + WHERE tc.is_active=1 + ORDER BY tc.sheet, tc.source_id, tc.variant_id + """ + return [dict(r) for r in conn.execute(sql, (run_id,)).fetchall()] + + +def export_merged_json(conn, run_id: int, output_path: str): + rows = merged_rows(conn, run_id) + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(rows, f, indent=2, ensure_ascii=False) + return len(rows) + + +def export_csv(conn, run_id: int, output_path: str): + rows = merged_rows(conn, run_id) + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", newline="", encoding="utf-8") as f: + if rows: + w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) + w.writeheader() + w.writerows(rows) + return len(rows) + + +def xlsx_col(n): + s = "" + while n: + n, r = divmod(n - 1, 26) + s = chr(65 + r) + s + return s + + +def sheet_xml(rows): + out = ['', ''] + for r_idx, row in enumerate(rows, start=1): + out.append(f'') + for c_idx, val in enumerate(row, start=1): + ref = f"{xlsx_col(c_idx)}{r_idx}" + if val is None: + val = "" + # Keep everything as inline string for maximum compatibility with no sharedStrings table. + txt = html.escape(str(val), quote=False) + out.append(f'{txt}') + out.append('') + out.append('') + return "".join(out) + + +def write_xlsx(path_out: str, sheets: dict): + Path(path_out).parent.mkdir(parents=True, exist_ok=True) + names = list(sheets.keys()) + with zipfile.ZipFile(path_out, "w", zipfile.ZIP_DEFLATED) as z: + z.writestr("[Content_Types].xml", """ + + + + +""" + "".join(f'' for i in range(1, len(names)+1)) + "") + z.writestr("_rels/.rels", """ + + +""") + z.writestr("xl/workbook.xml", """ +""" + "".join(f'' for i, name in enumerate(names, start=1)) + "") + z.writestr("xl/_rels/workbook.xml.rels", """ +""" + "".join(f'' for i in range(1, len(names)+1)) + "") + for i, name in enumerate(names, start=1): + z.writestr(f"xl/worksheets/sheet{i}.xml", sheet_xml(sheets[name])) + + +def rows_to_aoa(rows): + if not rows: + return [] + headers = list(rows[0].keys()) + return [headers] + [[r.get(h, "") for h in headers] for r in rows] + + +def export_xlsx(conn, run_id: int, output_path: str): + run = conn.execute("SELECT * FROM test_runs WHERE id=?", (run_id,)).fetchone() + summary = conn.execute("SELECT status, COUNT(*) AS count FROM test_results WHERE run_id=? GROUP BY status", (run_id,)).fetchall() + results = merged_rows(conn, run_id) + attachments = conn.execute( + """ + SELECT tr.variant_id, tr.test_title, ta.attachment_name, ta.content_type, ta.file_path + FROM test_attachments ta + JOIN test_results tr ON tr.id = ta.result_id + WHERE ta.run_id=? + ORDER BY tr.variant_id, ta.attachment_name + """, + (run_id,), + ).fetchall() + failures = [r for r in results if r.get("result") not in ("passed", "manual", "skipped")] + sheets = { + "Run": rows_to_aoa([dict(run)] if run else []), + "Summary": rows_to_aoa([dict(r) for r in summary]), + "Failures": rows_to_aoa(failures), + "Results": rows_to_aoa(results), + "Attachments": rows_to_aoa([dict(r) for r in attachments]), + } + write_xlsx(output_path, sheets) + return len(results) + + +def extract_variant_id(text: str): + text = text or "" + m = re.search(r"\[([^\]]+)\]", text) + if m: + return m.group(1) + # Fallback for legacy/custom tests whose title starts with an ID. + m = re.search(r"\b((?:V25-)?[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\b", text) + return m.group(1) if m else "" + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("command", choices=["init", "import-cases", "begin-run", "record-result", "finish-run", "export-json", "export-csv", "export-xlsx", "summary"]) + p.add_argument("--db", default=DB_DEFAULT) + p.add_argument("--matrix", default=MATRIX_DEFAULT) + p.add_argument("--run-id", type=int) + p.add_argument("--run-name", default=os.environ.get("TEST_RUN_NAME") or f"Run {now_iso()}") + p.add_argument("--base-url", default=os.environ.get("BASE_URL", "")) + p.add_argument("--status", default="completed") + p.add_argument("--output", default="") + args = p.parse_args() + + conn = connect(args.db) + try: + if args.command == "init": + init_db(conn) + print(json.dumps({"ok": True, "db": args.db})) + elif args.command == "import-cases": + count = import_cases(conn, args.matrix) + print(json.dumps({"ok": True, "imported": count, "db": args.db})) + elif args.command == "begin-run": + run_id = begin_run(conn, args.run_name, args.base_url) + print(json.dumps({"ok": True, "run_id": run_id})) + elif args.command == "record-result": + record = json.load(sys.stdin) + result_id = record_result(conn, record) + print(json.dumps({"ok": True, "result_id": result_id})) + elif args.command == "finish-run": + run_id = args.run_id or latest_run_id(conn) + counts = finish_run(conn, run_id, args.status) + print(json.dumps({"ok": True, "run_id": run_id, "counts": counts})) + elif args.command == "export-json": + run_id = args.run_id or latest_run_id(conn) + output = args.output or "results/merged-results.json" + count = export_merged_json(conn, run_id, output) + print(json.dumps({"ok": True, "run_id": run_id, "rows": count, "output": output})) + elif args.command == "export-csv": + run_id = args.run_id or latest_run_id(conn) + output = args.output or "results/UAT_VAPT_SQLite_Export.csv" + count = export_csv(conn, run_id, output) + print(json.dumps({"ok": True, "run_id": run_id, "rows": count, "output": output})) + elif args.command == "export-xlsx": + run_id = args.run_id or latest_run_id(conn) + output = args.output or "results/UAT_VAPT_SQLite_Export.xlsx" + count = export_xlsx(conn, run_id, output) + print(json.dumps({"ok": True, "run_id": run_id, "rows": count, "output": output})) + elif args.command == "summary": + run_id = args.run_id or latest_run_id(conn) + rows = conn.execute("SELECT status, COUNT(*) AS count FROM test_results WHERE run_id=? GROUP BY status", (run_id,)).fetchall() + print(json.dumps({r["status"]: r["count"] for r in rows}, indent=2)) + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/seed/seed_uat_data_v2_5_1.py b/seed/seed_uat_data_v2_5_1.py new file mode 100644 index 0000000..cf031ce --- /dev/null +++ b/seed/seed_uat_data_v2_5_1.py @@ -0,0 +1,514 @@ +""" +Audit Firm ERP -- UAT seed extension for Playwright v2.5.1 Deep SQLite suite. + +Purpose: + Extend the existing core UAT seed with supporting records for the new v2.5.1 + deep tests: email integration, marketplace leads, platform billing, RBAC, + audit logs, document security, HR, billing rules, notice-case rules and + financial-year locking. + +Run inside ERP container, AFTER the core seed: + + cd /app + PYTHONPATH=/app python scripts/seed_uat_data.py + PYTHONPATH=/app python scripts/seed_uat_data_v2_5_1.py + +Safe/idempotent for UAT: it inserts or updates only UAT-coded records where the +matching tables/columns exist. If a module table is not present in this ERP build, +the script skips that area and prints a warning instead of failing. + +Do NOT run against live production data. +""" +from __future__ import annotations + +import hashlib +import json +import os +import sys +from datetime import date, datetime, timedelta, timezone + +UAT_DOMAIN = os.getenv("UAT_EMAIL_DOMAIN", "vavalam.com") +ACTIVE_FY = os.getenv("ACTIVE_FY", "2025-26") +PREVIOUS_FY = os.getenv("PREVIOUS_FY", "2024-25") +ASSESSMENT_YEAR = os.getenv("ASSESSMENT_YEAR", "2026-27") +LSA_SECRET = os.getenv("UAT_LSA_SECRET", "uat-local-storage-agent-secret") +SMTP_PASSWORD = os.getenv("UAT_SMTP_PASSWORD", "Pass@123##") + + +def fail(msg: str) -> None: + print(f"\n[v2.5.1 seed] ERROR: {msg}", file=sys.stderr) + sys.exit(1) + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +def today() -> date: + return date.today() + + +def main() -> None: + try: + from sqlalchemy import MetaData, and_, inspect, select, update + from app.core.db.common import CommonSessionLocal + except Exception as exc: + fail( + "Could not import ERP DB modules. Run this from ERP project root inside the app container. " + f"Underlying import error: {exc!r}" + ) + + db = CommonSessionLocal() + engine = db.get_bind() + meta = MetaData() + meta.reflect(bind=engine) + inspector = inspect(engine) + table_names = set(inspector.get_table_names()) + + report: list[str] = [] + ids: dict[str, object] = {} + + def norm(name: str) -> str: + return name.lower().replace("_", "").replace("-", "") + + def find_table(*candidates: str): + # exact match first + for name in candidates: + if name in meta.tables: + return meta.tables[name] + # normalized exact match + normalized = {norm(t): t for t in table_names} + for name in candidates: + hit = normalized.get(norm(name)) + if hit: + return meta.tables[hit] + # token contains fallback + for name in candidates: + tokens = [tok for tok in name.lower().replace("-", "_").split("_") if tok] + for t in table_names: + low = t.lower() + if all(tok in low for tok in tokens): + return meta.tables[t] + return None + + def valid(table, data: dict) -> dict: + if table is None: + return {} + cols = set(table.c.keys()) + return {k: v for k, v in data.items() if k in cols} + + def one(table, lookup: dict): + if table is None: + return None + data = valid(table, lookup) + if not data: + return None + stmt = select(table).where(and_(*[table.c[k] == v for k, v in data.items()])) + return db.execute(stmt).mappings().first() + + def first(table, **lookup): + return one(table, lookup) + + def first_by_any(table, lookups: list[dict]): + for lookup in lookups: + row = one(table, lookup) + if row: + return row + return None + + def upsert(table, lookup: dict, defaults: dict | None = None, label: str = "record"): + if table is None: + report.append(f"SKIP {label}: table missing") + return None + lookup_v = valid(table, lookup) + if not lookup_v: + report.append(f"SKIP {label}: no matching lookup columns in {table.name}") + return None + defaults_v = valid(table, defaults or {}) + row = one(table, lookup_v) + if row: + if defaults_v: + db.execute(update(table).where(and_(*[table.c[k] == v for k, v in lookup_v.items()])).values(**defaults_v)) + db.flush() + return one(table, lookup_v) + data = {**lookup_v, **defaults_v} + result = db.execute(table.insert().values(**data)) + db.flush() + pk = result.inserted_primary_key[0] if result.inserted_primary_key else None + if pk and "id" in table.c: + return one(table, {"id": pk}) + return one(table, lookup_v) + + def update_existing(table, lookup: dict, values: dict, label: str): + if table is None: + report.append(f"SKIP {label}: table missing") + return None + lookup_v = valid(table, lookup) + values_v = valid(table, values) + if not lookup_v or not values_v: + report.append(f"SKIP {label}: no matching columns in {table.name}") + return None + db.execute(update(table).where(and_(*[table.c[k] == v for k, v in lookup_v.items()])).values(**values_v)) + db.flush() + return one(table, lookup_v) + + def val(row, key: str, default=None): + return row[key] if row and key in row else default + + # Core table references from earlier seed + tenants = find_table("tenants", "tenant") + branches = find_table("branches", "branch") + users = find_table("users", "user") + clients = find_table("clients", "client") + employees = find_table("employees", "employee") + financial_years = find_table("financial_years", "financial_year") + documents = find_table("engagement_documents", "documents", "client_documents") + document_versions = find_table("engagement_document_versions", "document_versions") + storage_nodes = find_table("branch_storage_nodes", "storage_nodes") + storage_jobs = find_table("document_storage_jobs", "storage_jobs") + notice_cases = find_table("notice_cases", "cases") + subscriptions = find_table("client_service_subscriptions", "subscriptions", "engagements") + task_instances = find_table("client_service_task_instances", "task_instances", "tasks") + services = find_table("service_catalogue", "service_catalogues", "services") + roles = find_table("roles", "rbac_roles") + permissions = find_table("permissions", "rbac_permissions") + role_permissions = find_table("role_permissions", "rbac_role_permissions") + + tenant_a = first_by_any(tenants, [{"code": "UAT-A"}, {"tenant_code": "UAT-A"}, {"name": "UAT Tenant A"}]) + tenant_b = first_by_any(tenants, [{"code": "UAT-B"}, {"tenant_code": "UAT-B"}, {"name": "UAT Tenant B"}]) + branch_a = first_by_any(branches, [{"code": "UAT-BA"}, {"branch_code": "UAT-BA"}, {"name": "UAT Branch A"}]) + branch_b = first_by_any(branches, [{"code": "UAT-BB"}, {"branch_code": "UAT-BB"}, {"name": "UAT Branch B"}]) + firm_admin = first(users, email=f"uat.firmadmin@{UAT_DOMAIN}") + system_admin = first(users, email=f"uat.admin@{UAT_DOMAIN}") + partner = first(users, email=f"uat.partner@{UAT_DOMAIN}") + manager = first(users, email=f"uat.manager@{UAT_DOMAIN}") + staff = first(users, email=f"uat.staff@{UAT_DOMAIN}") + staff2 = first(users, email=f"uat.staff2@{UAT_DOMAIN}") + client_user = first(users, email=f"uat.client@{UAT_DOMAIN}") + consultant = first(users, email=f"uat.consultant@{UAT_DOMAIN}") + client_a = first_by_any(clients, [{"client_code": "UAT-CL-A"}, {"code": "UAT-CL-A"}, {"client_name": "UAT Client A Pvt Ltd"}]) + client_b = first_by_any(clients, [{"client_code": "UAT-CL-B"}, {"code": "UAT-CL-B"}, {"client_name": "UAT Client B Pvt Ltd"}]) + service_a = first_by_any(services, [{"service_code": "GST-GSTR3B-M"}, {"code": "GST-GSTR3B-M"}, {"service_name": "GSTR-3B Monthly Filing"}]) + sub_a = first_by_any(subscriptions, [{"financial_year": ACTIVE_FY, "client_id": val(client_a, "id")}, {"client_id": val(client_a, "id")}]) + task_a = first_by_any(task_instances, [{"client_id": val(client_a, "id")}, {"assigned_to_user_id": val(staff, "id")}]) + + if not tenant_a or not branch_a or not firm_admin: + fail("Core UAT seed appears missing. Run seed_uat_data.py first.") + + tenant_a_id = val(tenant_a, "id") + tenant_b_id = val(tenant_b, "id") + branch_a_id = val(branch_a, "id") + branch_b_id = val(branch_b, "id") + firm_admin_id = val(firm_admin, "id") + system_admin_id = val(system_admin, "id", firm_admin_id) + partner_id = val(partner, "id", firm_admin_id) + manager_id = val(manager, "id", firm_admin_id) + staff_id = val(staff, "id", firm_admin_id) + staff2_id = val(staff2, "id", staff_id) + client_user_id = val(client_user, "id", firm_admin_id) + consultant_id = val(consultant, "id", firm_admin_id) + client_a_id = val(client_a, "id") + client_b_id = val(client_b, "id") + service_a_id = val(service_a, "id") + sub_a_id = val(sub_a, "id") + task_a_id = val(task_a, "id") + + common_scope = { + "tenant_id": tenant_a_id, + "branch_id": branch_a_id, + "created_by_user_id": firm_admin_id, + "updated_by_user_id": firm_admin_id, + "created_by_id": firm_admin_id, + "updated_by_id": firm_admin_id, + "created_at": utcnow(), + "updated_at": utcnow(), + "is_active": True, + } + + # 1. Email integration seed + email_settings = find_table("email_settings", "firm_email_settings", "smtp_settings", "mail_settings") + smtp = upsert(email_settings, {"tenant_id": tenant_a_id, "setting_code": "UAT-SMTP-A"}, { + **common_scope, + "name": "UAT SMTP Settings", + "smtp_host": "mail.vavalam.com", + "smtp_port": 587, + "smtp_username": "no-reply@vavalam.com", + "smtp_password": SMTP_PASSWORD, + "from_email": "no-reply@vavalam.com", + "from_name": "ARRR ERP UAT", + "use_tls": True, + "imap_host": "mail.vavalam.com", + "imap_port": 993, + "imap_username": f"uat.firmadmin@{UAT_DOMAIN}", + "imap_password": SMTP_PASSWORD, + "status": "active", + "is_default": True, + }, "email settings") + ids["EMAIL_SETTINGS_ID"] = val(smtp, "id") + + email_templates = find_table("email_templates", "mail_templates") + for code, subject, body in [ + ("UAT_OTP", "Your ERP OTP", "Your OTP is {{ otp }}"), + ("UAT_INVITE", "ERP Invitation", "Please accept invitation: {{ invite_url }}"), + ("UAT_NOTICE", "Notice update", "Notice {{ reference_no }} requires attention"), + ]: + upsert(email_templates, {"tenant_id": tenant_a_id, "template_code": code}, { + **common_scope, "name": code.replace("_", " "), "subject": subject, "body": body, "body_html": body, + "template_type": "system", "is_active": True, + }, f"email template {code}") + + email_queue = find_table("email_queue", "queued_emails", "mail_queue") + upsert(email_queue, {"tenant_id": tenant_a_id, "queue_code": "UAT-EMAIL-Q-001"}, { + **common_scope, "to_email": f"uat.client@{UAT_DOMAIN}", "from_email": "no-reply@vavalam.com", + "subject": "UAT queued email", "body": "Queued email for UAT", "status": "pending", + "priority": 5, "scheduled_at": utcnow(), "attempts": 0, + }, "email queue") + + email_logs = find_table("email_logs", "mail_logs", "email_delivery_logs") + upsert(email_logs, {"tenant_id": tenant_a_id, "message_id": "UAT-EMAIL-LOG-001"}, { + **common_scope, "to_email": f"uat.client@{UAT_DOMAIN}", "from_email": "no-reply@vavalam.com", + "subject": "UAT delivered email", "status": "sent", "sent_at": utcnow(), "provider_response": "UAT seeded", + }, "email log") + + inbox = find_table("email_inbox_messages", "email_inbox", "inbound_emails", "mail_inbox") + upsert(inbox, {"tenant_id": tenant_a_id, "message_id": "UAT-INBOX-001"}, { + **common_scope, "from_email": f"uat.client@{UAT_DOMAIN}", "to_email": "support@vavalam.com", + "subject": "UAT client document submission", "body": "Please map this email to the UAT client.", + "received_at": utcnow(), "status": "unmapped", "client_id": client_a_id, + }, "email inbox") + + # 2. Marketplace / public lead seed + leads = find_table("marketplace_leads", "leads", "public_leads") + lead1 = upsert(leads, {"tenant_id": tenant_a_id, "lead_code": "UAT-LEAD-001"}, { + **common_scope, "source": "public_form", "lead_type": "compliance", "name": "UAT Marketplace Lead", + "company_name": "UAT Lead Pvt Ltd", "contact_name": "UAT Lead Contact", "email": "lead@example.com", + "mobile": "9000000101", "service_interest": "GST", "status": "new", "assigned_to_user_id": manager_id, + "notes": "Seeded marketplace lead for v2.5.1 tests", + }, "marketplace lead new") + lead2 = upsert(leads, {"tenant_id": tenant_a_id, "lead_code": "UAT-LEAD-CONVERTED"}, { + **common_scope, "source": "referral", "name": "UAT Converted Lead", "email": "converted@example.com", + "mobile": "9000000102", "status": "converted", "assigned_to_user_id": partner_id, "converted_client_id": client_a_id, + }, "marketplace lead converted") + ids["MARKETPLACE_LEAD_ID"] = val(lead1, "id") + + # 3. Platform billing seed + plans = find_table("platform_billing_plans", "billing_plans", "plans") + plan = upsert(plans, {"plan_code": "UAT-PLAN-PRO"}, { + "name": "UAT Professional Plan", "description": "Seed plan for platform billing tests", + "monthly_price": 9999, "annual_price": 99990, "currency": "INR", "max_users": 25, + "max_clients": 500, "is_active": True, "created_at": utcnow(), "updated_at": utcnow(), + }, "platform plan") + accounts = find_table("platform_billing_accounts", "billing_accounts", "accounts") + account = upsert(accounts, {"tenant_id": tenant_a_id, "account_code": "UAT-PLAT-ACC-A"}, { + **common_scope, "account_name": "UAT Tenant A Billing Account", "billing_email": "support@vavalam.com", + "gstin": "33AABCU1111A1Z5", "status": "active", + }, "platform billing account") + plat_subs = find_table("platform_subscriptions", "platform_billing_subscriptions", "audit_firm_subscriptions", "subscriptions") + plat_sub = upsert(plat_subs, {"tenant_id": tenant_a_id, "subscription_code": "UAT-PLAT-SUB-A"}, { + **common_scope, "account_id": val(account, "id"), "plan_id": val(plan, "id"), "status": "active", + "start_date": today(), "end_date": today() + timedelta(days=365), "billing_cycle": "monthly", + }, "platform subscription") + plat_invoices = find_table("platform_invoices", "platform_billing_invoices", "invoices") + p_inv = upsert(plat_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-PLAT-INV-001"}, { + **common_scope, "account_id": val(account, "id"), "subscription_id": val(plat_sub, "id"), + "invoice_date": today(), "due_date": today() + timedelta(days=15), "status": "issued", + "subtotal": 9999, "tax_amount": 1799.82, "total_amount": 11798.82, "balance_amount": 11798.82, + }, "platform invoice") + payments = find_table("platform_payments", "invoice_payments", "payments") + upsert(payments, {"tenant_id": tenant_a_id, "payment_ref": "UAT-PLAT-PAY-001"}, { + **common_scope, "invoice_id": val(p_inv, "id"), "payment_date": today(), "amount": 5000, + "payment_mode": "bank_transfer", "status": "posted", "remarks": "Seed partial platform payment", + }, "platform payment") + ids["PLATFORM_INVOICE_ID"] = val(p_inv, "id") + + # 4. RBAC seed + perm_codes = [ + "uat.clients.read", "uat.documents.read", "uat.billing.read", "uat.email.manage", "uat.marketplace.manage", + ] + for code in perm_codes: + upsert(permissions, {"code": code}, { + "name": code, "description": f"UAT permission {code}", "module": "UAT", "is_active": True, + "created_at": utcnow(), "updated_at": utcnow(), + }, f"permission {code}") + role = upsert(roles, {"name": "UAT Limited Tester"}, { + "description": "Seeded limited role for RBAC tests", "is_system": False, "is_active": True, + "tenant_id": tenant_a_id, "created_at": utcnow(), "updated_at": utcnow(), + }, "rbac role") + if role_permissions is not None and role is not None: + for code in perm_codes[:2]: + perm = first(permissions, code=code) if permissions is not None else None + upsert(role_permissions, {"role_id": val(role, "id"), "permission_id": val(perm, "id")}, { + "created_at": utcnow(), "created_by_user_id": firm_admin_id, + }, f"role permission {code}") + + # 5. Audit logs seed + audit_logs = find_table("audit_logs", "activity_logs", "system_audit_logs") + for code, action, entity in [ + ("UAT-AUD-LOGIN-FAIL", "login_failed", "User"), + ("UAT-AUD-DOC-DOWNLOAD", "document_download", "EngagementDocument"), + ("UAT-AUD-PERM-DENIED", "permission_denied", "RBAC"), + ]: + upsert(audit_logs, {"tenant_id": tenant_a_id, "event_code": code}, { + **common_scope, "user_id": firm_admin_id, "action": action, "event_type": action, + "entity_type": entity, "entity_id": client_a_id or 1, "ip_address": "127.0.0.1", + "user_agent": "UAT seed", "details": json.dumps({"seed": True, "no_secret": True}), + "message": f"Seed audit event {action}", + }, f"audit log {code}") + + # 6. Financial-year states + update_existing(financial_years, {"tenant_id": tenant_a_id, "year_code": PREVIOUS_FY}, { + "is_locked": True, "locked_at": utcnow(), "locked_by_user_id": firm_admin_id, + "lock_reason": "UAT locked FY for v2.5.1 tests", + }, "previous FY lock") + update_existing(financial_years, {"tenant_id": tenant_a_id, "year_code": ACTIVE_FY}, { + "is_locked": False, "is_current": True, + }, "active FY current/unlocked") + + backups = find_table("financial_year_backups", "fy_backups", "backup_exports") + fy = first(financial_years, tenant_id=tenant_a_id, year_code=PREVIOUS_FY) + backup = upsert(backups, {"tenant_id": tenant_a_id, "backup_code": "UAT-FY-BACKUP-001"}, { + **common_scope, "financial_year_id": val(fy, "id"), "year_code": PREVIOUS_FY, + "status": "completed", "file_name": "uat-fy-backup-001.zip", "file_size_bytes": 1024, + "created_by_user_id": firm_admin_id, + }, "FY backup") + ids["YEAR_BACKUP_EXPORT_ID"] = val(backup, "id") + ids["LOCKED_FY"] = PREVIOUS_FY + + # 7. Document security/deep seed + secret_hash = hashlib.sha256(LSA_SECRET.encode()).hexdigest() + node = upsert(storage_nodes, {"tenant_id": tenant_a_id, "branch_id": branch_a_id, "node_code": "UAT-LSA-SEC-A"}, { + **common_scope, "node_name": "UAT Security Storage Agent", "secret_key_hash": secret_hash, + "storage_root_path": "D:/UAT/Security", "storage_mode": "pull_jobs", "status": "active", + "quota_limit_bytes": 1024 * 1024 * 1024, "used_storage_bytes": 0, + }, "security storage node") + ids["STORAGE_NODE_SECURITY_ID"] = val(node, "id") + + doc_sec = upsert(documents, {"tenant_id": tenant_a_id, "document_code": "UAT-DOC-SEC-001"}, { + **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "engagement_id": sub_a_id, + "task_instance_id": task_a_id, "financial_year": ACTIVE_FY, "assessment_year": ASSESSMENT_YEAR, + "document_type": "SECURITY", "title": "UAT Client Visible Document", "description": "Client visible UAT doc", + "current_version_no": 2, "status": "active", "is_deleted": False, "is_client_visible": True, + }, "client visible document") + ids["CLIENT_VISIBLE_DOCUMENT_ID"] = val(doc_sec, "id") + for version, filename in [(1, "uat-sec-v1.txt"), (2, "uat-sec-v2.txt")]: + content = f"UAT secure version {version}\n".encode() + upsert(document_versions, {"document_id": val(doc_sec, "id"), "version_no": version}, { + **common_scope, "tenant_id": tenant_a_id, "branch_id": branch_a_id, "client_id": client_a_id, + "engagement_id": sub_a_id, "original_filename": filename, "stored_filename": filename, + "content_type": "text/plain", "file_size_bytes": len(content), + "file_hash_sha256": hashlib.sha256(content).hexdigest(), "storage_backend": "LOCAL_YEAR_WISE", + "local_relative_path": f"UAT-A/UAT-CL-A/{ACTIVE_FY}/{filename}", "storage_status": "stored", + "uploaded_by_user_id": staff_id, + }, f"document version {version}") + doc_deleted = upsert(documents, {"tenant_id": tenant_a_id, "document_code": "UAT-DOC-DELETED-001"}, { + **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "engagement_id": sub_a_id, + "financial_year": ACTIVE_FY, "document_type": "SECURITY", "title": "UAT Deleted Document", + "status": "deleted", "is_deleted": True, "deleted_at": utcnow(), "deleted_by_user_id": firm_admin_id, + }, "deleted document") + ids["DELETED_DOCUMENT_ID"] = val(doc_deleted, "id") + + storage_job = upsert(storage_jobs, {"tenant_id": tenant_a_id, "job_code": "UAT-STORAGE-JOB-SEC-001"}, { + **common_scope, "storage_node_id": val(node, "id"), "document_id": val(doc_sec, "id"), + "job_type": "store_version", "status": "pending", "priority": 1, + "staging_relative_path": "staging/uat-sec-v2.txt", "target_relative_path": f"UAT-A/UAT-CL-A/{ACTIVE_FY}/uat-sec-v2.txt", + "file_size_bytes": 21, "expected_hash_sha256": hashlib.sha256(b"UAT secure version 2\n").hexdigest(), + "attempts": 0, + }, "storage job security") + ids["STORAGE_JOB_SECURITY_ID"] = val(storage_job, "id") + + # 8. HR seed + attendance = find_table("employee_attendance", "attendance", "employee_attendances") + upsert(attendance, {"tenant_id": tenant_a_id, "employee_id": val(first(employees, user_id=staff_id), "id"), "attendance_date": today()}, { + **common_scope, "user_id": staff_id, "status": "present", "check_in_time": utcnow(), "source": "seed", + "remarks": "Seed attendance for duplicate/manager tests", + }, "attendance") + leave_balances = find_table("employee_leave_balances", "leave_balances") + leave_balance = upsert(leave_balances, {"tenant_id": tenant_a_id, "user_id": staff_id, "leave_type": "casual", "financial_year": ACTIVE_FY}, { + **common_scope, "employee_id": val(first(employees, user_id=staff_id), "id"), "opening_balance": 12, + "availed": 2, "balance": 10, "is_active": True, + }, "leave balance") + leave_requests = find_table("employee_leave_requests", "leave_requests", "leaves") + leave_req = upsert(leave_requests, {"tenant_id": tenant_a_id, "leave_code": "UAT-LEAVE-PENDING-001"}, { + **common_scope, "user_id": staff_id, "employee_id": val(first(employees, user_id=staff_id), "id"), + "leave_type": "casual", "from_date": today() + timedelta(days=5), "to_date": today() + timedelta(days=6), + "days": 2, "status": "pending", "reason": "UAT seeded pending leave", "approver_user_id": manager_id, + "balance_id": val(leave_balance, "id"), + }, "leave request pending") + ids["PENDING_LEAVE_ID"] = val(leave_req, "id") + payroll_runs = find_table("payroll_runs", "employee_payroll_runs", "payroll") + payroll = upsert(payroll_runs, {"tenant_id": tenant_a_id, "run_code": "UAT-PAYROLL-001"}, { + **common_scope, "financial_year": ACTIVE_FY, "period_month": today().month, "period_year": today().year, + "status": "generated", "gross_amount": 50000, "deduction_amount": 5000, "net_amount": 45000, + }, "payroll run") + payslips = find_table("payslips", "employee_payslips") + upsert(payslips, {"tenant_id": tenant_a_id, "payslip_code": "UAT-PAYSLIP-001"}, { + **common_scope, "payroll_run_id": val(payroll, "id"), "employee_id": val(first(employees, user_id=staff_id), "id"), + "user_id": staff_id, "gross_amount": 50000, "deduction_amount": 5000, "net_amount": 45000, + "status": "generated", "file_name": "uat-payslip-001.pdf", + }, "payslip") + + # 9. Firm billing business seed + firm_invoices = find_table("billing_invoices", "firm_invoices", "client_invoices", "invoices") + draft_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-DRAFT-001"}, { + **common_scope, "client_id": client_a_id, "invoice_date": today(), "due_date": today() + timedelta(days=15), + "status": "draft", "subtotal": 10000, "tax_amount": 1800, "total_amount": 11800, "balance_amount": 11800, + "financial_year": ACTIVE_FY, + }, "billing draft invoice") + issued_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-ISSUED-001"}, { + **common_scope, "client_id": client_a_id, "invoice_date": today(), "due_date": today() + timedelta(days=15), + "status": "issued", "subtotal": 20000, "tax_amount": 3600, "total_amount": 23600, "balance_amount": 18600, + "financial_year": ACTIVE_FY, + }, "billing issued invoice") + cancel_inv = upsert(firm_invoices, {"tenant_id": tenant_a_id, "invoice_no": "UAT-BILL-CANCELLED-001"}, { + **common_scope, "client_id": client_a_id, "invoice_date": today(), "status": "cancelled", + "subtotal": 1000, "tax_amount": 180, "total_amount": 1180, "balance_amount": 0, + "financial_year": ACTIVE_FY, + }, "billing cancelled invoice") + firm_payments = find_table("billing_payments", "client_payments", "invoice_payments", "payments") + upsert(firm_payments, {"tenant_id": tenant_a_id, "payment_ref": "UAT-BILL-PAY-001"}, { + **common_scope, "invoice_id": val(issued_inv, "id"), "client_id": client_a_id, + "payment_date": today(), "amount": 5000, "payment_mode": "upi", "status": "posted", + }, "billing partial payment") + ids["BILLING_DRAFT_INVOICE_ID"] = val(draft_inv, "id") + ids["BILLING_ISSUED_INVOICE_ID"] = val(issued_inv, "id") + ids["BILLING_CANCELLED_INVOICE_ID"] = val(cancel_inv, "id") + + # 10. Notice case business seed + if notice_cases is not None: + nc = upsert(notice_cases, {"tenant_id": tenant_a_id, "case_code": "UAT-NC-DEEP-001"}, { + **common_scope, "branch_id": branch_a_id, "client_id": client_a_id, "reference_no": "UAT-NOTICE-DEEP-001", + "department": "GST", "case_type": "Notice", "title": "UAT Deep GST Notice", "status": "Hearing Scheduled", + "notice_date": today() - timedelta(days=10), "hearing_date": today() + timedelta(days=10), + "order_date": None, "due_date": today() + timedelta(days=20), "assigned_to_user_id": staff_id, + "partner_user_id": partner_id, "internal_notes": "Internal note must not be client-visible", + "client_visible_notes": "Client visible notice update", + "issue_summary": "Seeded v2.5.1 notice case for business-rule testing", + }, "deep notice case") + ids["NOTICE_CASE_DEEP_ID"] = val(nc, "id") + nc_events = find_table("notice_case_events", "case_events", "notice_case_activities") + upsert(nc_events, {"tenant_id": tenant_a_id, "event_code": "UAT-NC-EVT-001"}, { + **common_scope, "case_id": val(nc, "id"), "event_type": "hearing_scheduled", "event_date": today(), + "notes": "Seeded hearing event", "is_client_visible": True, "created_by_user_id": staff_id, + }, "notice event") + + db.commit() + + print("\n[v2.5.1 seed] Completed seed extension.") + print("[v2.5.1 seed] Created/updated data for email, marketplace, platform billing, RBAC, audit, FY, documents, HR, billing and notice-case tests.") + print("\nUseful IDs for Playwright .env if needed:\n") + for k, v in ids.items(): + if v is not None: + print(f"{k}={v}") + print("\nSkipped/missing-module notes:") + for line in report: + print(f"- {line}") + print("\nIf many rows were skipped, that means the corresponding ERP module/table is not present yet; tests for those modules should then fail/skip as ERP gaps, not seed failures.") + + db.close() + + +if __name__ == "__main__": + try: + main() + except Exception: + raise diff --git a/tests/api-auth-rbac.spec.js b/tests/api-auth-rbac.spec.js new file mode 100644 index 0000000..85c022e --- /dev/null +++ b/tests/api-auth-rbac.spec.js @@ -0,0 +1,529 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-API-001", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API auth token rejects blank credentials", + "type": "VAPT", + "route": "/api/auth/token", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-002", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API refresh rejects missing refresh token", + "type": "VAPT", + "route": "/api/auth/refresh", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-003", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API logout without token handled safely", + "type": "VAPT", + "route": "/api/auth/logout", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-004", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API me requires authentication", + "type": "VAPT", + "route": "/api/auth/me", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-005", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API forgot-password rejects invalid email safely", + "type": "VAPT", + "route": "/api/auth/forgot-password", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-006", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API reset-password rejects bogus token", + "type": "VAPT", + "route": "/api/auth/reset-password", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-007", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API invite accept rejects bogus token", + "type": "VAPT", + "route": "/api/auth/invite/accept", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-008", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users requires authorization", + "type": "VAPT", + "route": "/api/users", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-009", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users create CSRF/auth rejected", + "type": "VAPT", + "route": "/api/users", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-010", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC roles requires authorization", + "type": "VAPT", + "route": "/api/rbac/roles", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-011", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC roles create auth rejected", + "type": "VAPT", + "route": "/api/rbac/roles", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-012", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API RBAC permissions requires authorization", + "type": "VAPT", + "route": "/api/rbac/permissions", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-013", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenancy tenants requires authorization", + "type": "VAPT", + "route": "/api/tenancy/tenants", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-014", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenancy branches requires authorization", + "type": "VAPT", + "route": "/api/tenancy/branches", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-015", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API clients requires authorization", + "type": "VAPT", + "route": "/api/v1/clients", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-016", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API clients create auth rejected", + "type": "VAPT", + "route": "/api/v1/clients", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-017", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client invalid ID safe", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-018", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client update invalid ID auth rejected", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "_kind": "api", + "_method": "PUT", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-019", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API client delete invalid ID auth rejected", + "type": "VAPT", + "route": "/api/v1/clients/999999", + "_kind": "api", + "_method": "DELETE", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-020", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API cross-tenant query does not leak", + "type": "VAPT", + "route": "/api/v1/clients?tenant_id=999999", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-021", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API roles duplicate invalid POST safe", + "type": "VAPT", + "route": "/api/rbac/roles", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-022", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API permission elevation attempt rejected", + "type": "VAPT", + "route": "/api/rbac/roles/999999/permissions", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-023", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API tenant create auth rejected", + "type": "VAPT", + "route": "/api/tenancy/tenants", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-024", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API branch create auth rejected", + "type": "VAPT", + "route": "/api/tenancy/branches", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-025", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API malformed JSON safe", + "type": "VAPT", + "route": "/api/auth/token", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-026", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API token response does not leak password", + "type": "VAPT", + "route": "/api/auth/token", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-027", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API forgot password does not disclose account existence", + "type": "VAPT", + "route": "/api/auth/forgot-password", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-028", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API reset token replay rejected", + "type": "VAPT", + "route": "/api/auth/reset-password", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-029", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API rate-limit/bruteforce endpoint safe", + "type": "VAPT", + "route": "/api/auth/token", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-030", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API CORS preflight handled safely", + "type": "VAPT", + "route": "/api/auth/me", + "_kind": "api", + "_method": "OPTIONS", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-031", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API export clients requires authorization", + "type": "VAPT", + "route": "/api/v1/clients/export", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-032", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users invalid ID safe", + "type": "VAPT", + "route": "/api/users/999999", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-033", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API users role update auth rejected", + "type": "VAPT", + "route": "/api/users/999999/roles", + "_kind": "api", + "_method": "POST", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-034", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API permission denied response does not include stack trace", + "type": "VAPT", + "route": "/api/users", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + }, + { + "variantId": "V251-API-035", + "module": "API Auth / RBAC", + "role": "API/Anonymous", + "scenario": "API unknown endpoint returns safe 404", + "type": "VAPT", + "route": "/api/does-not-exist-uat-vapt", + "_kind": "api", + "_method": "GET", + "_file": "api-auth-rbac.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - API Auth / RBAC", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/audit-logs.spec.js b/tests/audit-logs.spec.js new file mode 100644 index 0000000..5697f94 --- /dev/null +++ b/tests/audit-logs.spec.js @@ -0,0 +1,349 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-AUD-001", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs page loads for System Admin", + "type": "UAT", + "route": "/system-settings/audit-logs", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-002", + "module": "Audit Logs", + "role": "Firm Admin", + "scenario": "Firm Admin audit logs page loads or safely restricted", + "type": "UAT", + "route": "/system-settings/audit-logs", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-AUD-003", + "module": "Audit Logs", + "role": "Staff", + "scenario": "Staff cannot access audit logs", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-AUD-004", + "module": "Audit Logs", + "role": "Client", + "scenario": "Client cannot access audit logs", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-AUD-005", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit log invalid detail safe", + "type": "UAT", + "route": "/system-settings/audit-logs/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-006", + "module": "Audit Logs", + "role": "Staff", + "scenario": "Audit log export requires permission", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-AUD-007", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit log filters do not crash", + "type": "UAT", + "route": "/system-settings/audit-logs?module=clients&action=create", + "_kind": "page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-008", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose password", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-009", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose OTP", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-010", + "module": "Audit Logs", + "role": "System Admin", + "scenario": "Audit logs do not expose reset token", + "type": "VAPT", + "route": "/system-settings/audit-logs", + "_kind": "no-secret", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-AUD-011", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log export CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-012", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log delete invalid ID rejected", + "type": "VAPT", + "route": "/system-settings/audit-logs/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-013", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log tamper invalid POST rejected", + "type": "VAPT", + "route": "/system-settings/audit-logs/999999", + "_kind": "post", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-014", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Permission denied event generated safely by blocked route", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-015", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Login failure is handled without stack trace", + "type": "VAPT", + "route": "/login", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-016", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Document download audit invalid ID safe", + "type": "VAPT", + "route": "/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-017", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Billing payment audit invalid ID safe", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-018", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Notice case audit invalid ID safe", + "type": "VAPT", + "route": "/notice-cases/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-019", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log tenant scope query safe", + "type": "VAPT", + "route": "/system-settings/audit-logs?tenant_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + }, + { + "variantId": "V251-AUD-020", + "module": "Audit Logs", + "role": "Anonymous/Attacker", + "scenario": "Audit log branch scope query safe", + "type": "VAPT", + "route": "/system-settings/audit-logs?branch_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "audit-logs.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Audit Logs", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/billing-business-rules.spec.js b/tests/billing-business-rules.spec.js new file mode 100644 index 0000000..e3d5b1e --- /dev/null +++ b/tests/billing-business-rules.spec.js @@ -0,0 +1,433 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-BIZBILL-001", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Billing dashboard loads", + "type": "UAT", + "route": "/billing", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-BIZBILL-002", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invoices list loads", + "type": "UAT", + "route": "/billing/invoices", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-BIZBILL-003", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invoice create page loads", + "type": "UAT", + "route": "/billing/invoices/new", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-BIZBILL-004", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Payments list loads", + "type": "UAT", + "route": "/billing/payments", + "_kind": "page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-BIZBILL-005", + "module": "Billing Business Rules", + "role": "Firm Admin", + "scenario": "Invalid invoice detail safe", + "type": "VAPT", + "route": "/billing/invoices/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-BIZBILL-006", + "module": "Billing Business Rules", + "role": "Client", + "scenario": "Client cannot access all invoices", + "type": "VAPT", + "route": "/billing/invoices", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-BIZBILL-007", + "module": "Billing Business Rules", + "role": "Staff", + "scenario": "Staff cannot access billing", + "type": "VAPT", + "route": "/billing", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-BIZBILL-008", + "module": "Billing Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access billing", + "type": "VAPT", + "route": "/billing", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-BIZBILL-009", + "module": "Billing Business Rules", + "role": "Client", + "scenario": "Client own invoice invalid ID safe", + "type": "VAPT", + "route": "/client/invoices/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-BIZBILL-010", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice create without line items rejected", + "type": "VAPT", + "route": "/billing/invoices", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-011", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice duplicate number invalid POST safe", + "type": "VAPT", + "route": "/billing/invoices", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-012", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice invalid GST/tax values rejected", + "type": "VAPT", + "route": "/billing/invoices", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-013", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice issue CSRF-less POST rejected", + "type": "VAPT", + "route": "/billing/invoices/999999/issue", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-014", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice cancel CSRF-less POST rejected", + "type": "VAPT", + "route": "/billing/invoices/999999/cancel", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-015", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Void invoice cannot accept payment", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-016", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Partial payment invalid ID safe", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-017", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Overpayment blocked", + "type": "VAPT", + "route": "/billing/invoices/999999/payments", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-018", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment receipt invalid ID safe", + "type": "VAPT", + "route": "/billing/payments/999999/receipt", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-019", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice PDF invalid ID safe", + "type": "VAPT", + "route": "/billing/invoices/999999/pdf", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-020", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice print invalid ID safe", + "type": "VAPT", + "route": "/billing/invoices/999999/print", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-021", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice internal notes hidden from client route safe", + "type": "VAPT", + "route": "/client/invoices/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-022", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/billing/payments/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-023", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice line update invalid ID rejected", + "type": "VAPT", + "route": "/billing/invoices/999999/lines/999999", + "_kind": "post", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-024", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice export requires authorization", + "type": "VAPT", + "route": "/billing/invoices/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-025", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payment export requires authorization", + "type": "VAPT", + "route": "/billing/payments/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-026", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-client invoice IDOR safe", + "type": "VAPT", + "route": "/billing/invoices/999999?client_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-BIZBILL-027", + "module": "Billing Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invoice number sequence endpoint safe", + "type": "VAPT", + "route": "/billing/invoices/next-number", + "_kind": "safe-page", + "_method": "GET", + "_file": "billing-business-rules.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Billing Business Rules", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/consultants-documents.spec.js b/tests/consultants-documents.spec.js new file mode 100644 index 0000000..c9b6743 --- /dev/null +++ b/tests/consultants-documents.spec.js @@ -0,0 +1,280 @@ +/** + * ============================================================================= + * UAT_Consultants_Documents -- Consultants portal + document storage module + * ============================================================================= + * + * Covers: + * CONS-* : Consultant portal — list, detail, links, conversion requests, + * service requests + * DOC-STOR-*: Document storage — permanent vault, storage nodes, branch + * storage dashboard, download requests + * SEC-* : CSRF rejection, anonymous probes, IDOR on storage endpoints + * + * Required .env additions: + * CONSULTANT_A_ID= # a seeded consultant id + * CONVERSION_REQUEST_ID= # a seeded managed-client conversion request id + * SERVICE_REQUEST_ID= # a seeded consultant service request id + * CLIENT_A_ID= # already used in main suite; reused here + * STORAGE_NODE_ID= # a seeded storage node id + * DOWNLOAD_REQUEST_ID= # a seeded download request id + * PERM_DOCUMENT_ID= # a seeded permanent document id + * PERM_VERSION_ID= # a seeded permanent document version id + * + * ============================================================================= + */ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +const { login } = require('../fixtures/auth'); +const { expectNoBackendError, readBody, blockedOrNotFound, uploadPath } = 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`); +} + +// --------------------------------------------------------------------------- +// Consultants +// --------------------------------------------------------------------------- + +test.describe('CONS: Consultant portal', () => { + + test('[V25-CONS-001] CONS-001 Consultant list loads for Firm Admin', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/consultants'); + await expectNoBackendError(page); + }); + + test('[V25-CONS-002] CONS-002 Consultant detail page loads', async ({ page }) => { + skipIfMissing('CONSULTANT_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/consultants/${idOr('CONSULTANT_A_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-CONS-003] CONS-003 Consultant edit form loads', async ({ page }) => { + skipIfMissing('CONSULTANT_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/consultants/${idOr('CONSULTANT_A_ID')}/edit`); + await expectNoBackendError(page); + }); + + test('[V25-CONS-004] CONS-004 Consultant links page loads', async ({ page }) => { + skipIfMissing('CONSULTANT_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/consultants/${idOr('CONSULTANT_A_ID')}/links`); + await expectNoBackendError(page); + }); + + test('[V25-CONS-005] CONS-005 Conversion requests list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/consultants/conversion-requests'); + await expectNoBackendError(page); + }); + + test('[V25-CONS-006] CONS-006 Conversion request detail loads', async ({ page }) => { + skipIfMissing('CONVERSION_REQUEST_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/consultants/conversion-requests/${idOr('CONVERSION_REQUEST_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-CONS-007] CONS-007 Conversion request review CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('CONVERSION_REQUEST_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/consultants/conversion-requests/${idOr('CONVERSION_REQUEST_ID')}/review`, + { form: { decision: 'approve', 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-CONS-008] CONS-008 Service requests list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/consultants/service-requests'); + await expectNoBackendError(page); + }); + + test('[V25-CONS-009] CONS-009 Service request detail loads', async ({ page }) => { + skipIfMissing('SERVICE_REQUEST_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/consultants/service-requests/${idOr('SERVICE_REQUEST_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-CONS-010] CONS-010 Service request status update CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('SERVICE_REQUEST_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/consultants/service-requests/${idOr('SERVICE_REQUEST_ID')}/status`, + { form: { status: 'approved', 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-CONS-011] CONS-011 Staff cannot access consultant list', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/consultants'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-CONS-012] CONS-012 Client cannot access consultant detail', async ({ page }) => { + skipIfMissing('CONSULTANT_A_ID'); + await login(page, 'Client'); + const resp = await safeGoto(page, `/consultants/${idOr('CONSULTANT_A_ID')}`); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-CONS-013] CONS-013 Non-existent consultant ID returns safe response', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/consultants/999999999'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-CONS-014] CONS-014 Consultant self-service dashboard loads', async ({ page }) => { + await login(page, 'Consultant'); + const resp = await safeGoto(page, '/consultant/dashboard'); + // may 404 if no consultant portal route — acceptable; must not 500 + expect(resp.status()).toBeLessThan(500); + await expectNoBackendError(page); + }); + +}); + +// --------------------------------------------------------------------------- +// Document storage module +// --------------------------------------------------------------------------- + +test.describe('DOC-STOR: Document storage and permanent vault', () => { + + test('[V25-DOC-STOR-001] DOC-STOR-001 Branch storage dashboard loads for Firm Admin', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/documents/branch-storage-dashboard'); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-002] DOC-STOR-002 Permanent document vault list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/documents/permanent'); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-003] DOC-STOR-003 Permanent vault for specific client loads', async ({ page }) => { + skipIfMissing('CLIENT_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/documents/permanent/clients/${idOr('CLIENT_A_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-004] DOC-STOR-004 Permanent vault upload CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('CLIENT_A_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/documents/permanent/clients/${idOr('CLIENT_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-DOC-STOR-005] DOC-STOR-005 Permanent document download requires auth', async ({ page }) => { + skipIfMissing('PERM_DOCUMENT_ID'); + // anonymous access + const resp = await safeGoto(page, `/documents/permanent/${idOr('PERM_DOCUMENT_ID')}/download`); + const body = await readBody(page); + await blockedOrNotFound(resp, body); + }); + + test('[V25-DOC-STOR-006] DOC-STOR-006 Storage jobs list loads for System Admin', async ({ page }) => { + await login(page, 'System Admin'); + await safeGoto(page, '/documents/storage-jobs'); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-007] DOC-STOR-007 Storage nodes list loads for System Admin', async ({ page }) => { + await login(page, 'System Admin'); + await safeGoto(page, '/documents/storage-nodes'); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-008] DOC-STOR-008 Download requests list loads', async ({ page }) => { + await login(page, 'System Admin'); + await safeGoto(page, '/documents/download-requests'); + await expectNoBackendError(page); + }); + + test('[V25-DOC-STOR-009] DOC-STOR-009 Storage agent jobs endpoint requires auth', async ({ request }) => { + const resp = await request.get(`${process.env.BASE_URL}/documents/storage-agent/jobs/pending`).catch(() => null); + if (!resp) test.skip(true, 'Request failed at network level'); + expect(resp.status()).toBeLessThan(500); + expect([400, 401, 403, 404, 405, 422].includes(resp.status())).toBeTruthy(); + }); + + test('[V25-DOC-STOR-010] DOC-STOR-010 Storage agent download-requests endpoint requires auth', async ({ request }) => { + const resp = await request.get(`${process.env.BASE_URL}/documents/storage-agent/download-requests/pending`).catch(() => null); + if (!resp) test.skip(true, 'Request failed at network level'); + expect(resp.status()).toBeLessThan(500); + expect([400, 401, 403, 404, 405, 422].includes(resp.status())).toBeTruthy(); + }); + + test('[V25-DOC-STOR-011] DOC-STOR-011 Storage node toggle CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('STORAGE_NODE_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/documents/storage-nodes/${idOr('STORAGE_NODE_ID')}/toggle`, + { 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-DOC-STOR-012] DOC-STOR-012 Staff cannot access branch storage dashboard', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/documents/branch-storage-dashboard'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-DOC-STOR-013] DOC-STOR-013 Client cannot access permanent vault admin', async ({ page }) => { + await login(page, 'Client'); + const resp = await safeGoto(page, '/documents/permanent'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-DOC-STOR-014] DOC-STOR-014 Permanent document delete CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('PERM_DOCUMENT_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/documents/permanent/${idOr('PERM_DOCUMENT_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-DOC-STOR-015] DOC-STOR-015 Non-existent permanent document download is handled safely', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/documents/permanent/999999999/download'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-DOC-STOR-016] DOC-STOR-016 Storage agent agent-package download requires node auth', async ({ request }) => { + const resp = await request.get(`${process.env.BASE_URL}/documents/storage-nodes/download-agent-package`).catch(() => null); + if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); + expect(resp.status()).toBeLessThan(500); + expect([400, 401, 403, 404, 422].includes(resp.status())).toBeTruthy(); + }); + +}); diff --git a/tests/document-security-deep.spec.js b/tests/document-security-deep.spec.js new file mode 100644 index 0000000..cb08631 --- /dev/null +++ b/tests/document-security-deep.spec.js @@ -0,0 +1,445 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-DSEC-001", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid document download returns safe response", + "type": "VAPT", + "route": "/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-002", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid document version download safe", + "type": "VAPT", + "route": "/documents/999999/versions/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-003", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Invalid permanent document download safe", + "type": "VAPT", + "route": "/documents/permanent/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-004", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Deleted document invalid download safe", + "type": "VAPT", + "route": "/documents/999999/deleted/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-005", + "module": "Document Security Deep", + "role": "Client", + "scenario": "Client cannot download other document ID", + "type": "VAPT", + "route": "/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-DSEC-006", + "module": "Document Security Deep", + "role": "Consultant", + "scenario": "Consultant cannot download unshared document", + "type": "VAPT", + "route": "/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-DSEC-007", + "module": "Document Security Deep", + "role": "Staff", + "scenario": "Staff cannot access storage admin", + "type": "VAPT", + "route": "/documents/storage/nodes", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-DSEC-008", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Storage agent jobs require auth", + "type": "VAPT", + "route": "/storage-agent/jobs", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-009", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Storage agent heartbeat without secret rejected", + "type": "VAPT", + "route": "/storage-agent/heartbeat", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-010", + "module": "Document Security Deep", + "role": "Public", + "scenario": "Download token invalid safe", + "type": "VAPT", + "route": "/documents/download-token/invalid-token", + "_kind": "safe-page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-011", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Document search route does not leak across tenant", + "type": "VAPT", + "route": "/documents?tenant_id=999999", + "_kind": "page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-012", + "module": "Document Security Deep", + "role": "Firm Admin", + "scenario": "Document repository loads for Firm Admin", + "type": "UAT", + "route": "/documents", + "_kind": "page", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-DSEC-013", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload double extension rejected", + "type": "VAPT", + "route": "/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-014", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload MIME mismatch rejected", + "type": "VAPT", + "route": "/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-015", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload large file rejected or handled", + "type": "VAPT", + "route": "/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-016", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document upload path traversal filename rejected", + "type": "VAPT", + "route": "/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-017", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-018", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document version delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/999999/versions/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-019", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Permanent document delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/permanent/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-020", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document move cross-tenant CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/999999/move", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-021", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document share with client invalid ID rejected", + "type": "VAPT", + "route": "/documents/999999/share-client", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-022", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document share with consultant invalid ID rejected", + "type": "VAPT", + "route": "/documents/999999/share-consultant", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-023", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage node create CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/storage/nodes", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-024", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage agent ack without secret rejected", + "type": "VAPT", + "route": "/storage-agent/jobs/999999/ack", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-025", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Storage agent upload without secret rejected", + "type": "VAPT", + "route": "/storage-agent/jobs/999999/upload", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-026", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Download request expiry invalid safe", + "type": "VAPT", + "route": "/documents/download-requests/999999", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-027", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document tag update CSRF-less POST rejected", + "type": "VAPT", + "route": "/documents/999999/tags", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + }, + { + "variantId": "V251-DSEC-028", + "module": "Document Security Deep", + "role": "Anonymous/Attacker", + "scenario": "Document metadata update invalid ID rejected", + "type": "VAPT", + "route": "/documents/999999/edit", + "_kind": "post", + "_method": "GET", + "_file": "document-security-deep.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Document Security Deep", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/email-integration.spec.js b/tests/email-integration.spec.js new file mode 100644 index 0000000..1c8720d --- /dev/null +++ b/tests/email-integration.spec.js @@ -0,0 +1,421 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-EMAIL-001", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email settings page loads", + "type": "UAT", + "route": "/email/settings", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-002", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email test settings endpoint visible/safe", + "type": "UAT", + "route": "/email/settings/test", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-003", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email logs page loads", + "type": "UAT", + "route": "/email/logs", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-004", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue page loads", + "type": "UAT", + "route": "/email/queue", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-005", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email inbox page loads", + "type": "UAT", + "route": "/email/inbox", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-006", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email templates page loads", + "type": "UAT", + "route": "/email/templates", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-007", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email template detail invalid ID safe", + "type": "UAT", + "route": "/email/templates/999999", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-008", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email inbox invalid message detail safe", + "type": "UAT", + "route": "/email/inbox/999999", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-009", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "SMTP settings CSRF-less save rejected", + "type": "VAPT", + "route": "/email/settings", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-010", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "SMTP test CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/settings/test", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-011", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email queue process CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/queue/process", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-012", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox fetch CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/inbox/fetch", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-013", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox map-all CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/inbox/map-all", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-014", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Inbox message map CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/inbox/999999/map", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-015", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email template save CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/templates", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-016", + "module": "Email Integration", + "role": "Anonymous/Attacker", + "scenario": "Email template update invalid ID CSRF-less POST rejected", + "type": "VAPT", + "route": "/email/templates/999999", + "_kind": "post", + "_method": "POST", + "_file": "email-integration.spec.js", + "_login_role": null + }, + { + "variantId": "V251-EMAIL-017", + "module": "Email Integration", + "role": "Staff", + "scenario": "Staff cannot access email settings", + "type": "VAPT", + "route": "/email/settings", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-EMAIL-018", + "module": "Email Integration", + "role": "Client", + "scenario": "Client cannot access email logs", + "type": "VAPT", + "route": "/email/logs", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-EMAIL-019", + "module": "Email Integration", + "role": "Consultant", + "scenario": "Consultant cannot access email inbox", + "type": "VAPT", + "route": "/email/inbox", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-EMAIL-020", + "module": "Email Integration", + "role": "Partner", + "scenario": "Partner email queue access is blocked or safe", + "type": "VAPT", + "route": "/email/queue", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Partner" + }, + { + "variantId": "V251-EMAIL-021", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Invalid email attachment download does not crash", + "type": "UAT", + "route": "/email/inbox/999999/attachments/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-022", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email logs do not expose SMTP password", + "type": "VAPT", + "route": "/email/logs", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-023", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email settings page does not render SMTP password in clear text", + "type": "VAPT", + "route": "/email/settings", + "_kind": "page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-024", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue invalid retry action safe", + "type": "UAT", + "route": "/email/queue/999999/retry", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-025", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email queue invalid delete action safe", + "type": "UAT", + "route": "/email/queue/999999/delete", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-EMAIL-026", + "module": "Email Integration", + "role": "Firm Admin", + "scenario": "Email template preview invalid ID safe", + "type": "UAT", + "route": "/email/templates/999999/preview", + "_kind": "safe-page", + "_method": "GET", + "_file": "email-integration.spec.js", + "_login_role": "Firm Admin" + } +]; + +test.describe("v2.5.1 Additions - Email Integration", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/employee-hr-business-rules.spec.js b/tests/employee-hr-business-rules.spec.js new file mode 100644 index 0000000..b20043a --- /dev/null +++ b/tests/employee-hr-business-rules.spec.js @@ -0,0 +1,349 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-HRRULE-001", + "module": "Employee HR Business Rules", + "role": "Staff", + "scenario": "Attendance page loads", + "type": "UAT", + "route": "/employee/attendance", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-HRRULE-002", + "module": "Employee HR Business Rules", + "role": "Staff", + "scenario": "Leave page loads", + "type": "UAT", + "route": "/employee/leaves", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-HRRULE-003", + "module": "Employee HR Business Rules", + "role": "Manager", + "scenario": "Manager team attendance loads", + "type": "UAT", + "route": "/employees/attendance", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Manager" + }, + { + "variantId": "V251-HRRULE-004", + "module": "Employee HR Business Rules", + "role": "Manager", + "scenario": "Manager team leave loads", + "type": "UAT", + "route": "/employees/leave", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Manager" + }, + { + "variantId": "V251-HRRULE-005", + "module": "Employee HR Business Rules", + "role": "Firm Admin", + "scenario": "Payroll page loads for Firm Admin", + "type": "UAT", + "route": "/employees/payroll", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-HRRULE-006", + "module": "Employee HR Business Rules", + "role": "Firm Admin", + "scenario": "Employee documents page loads", + "type": "UAT", + "route": "/employees/documents", + "_kind": "page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-HRRULE-007", + "module": "Employee HR Business Rules", + "role": "Client", + "scenario": "Client cannot access attendance", + "type": "VAPT", + "route": "/employee/attendance", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-HRRULE-008", + "module": "Employee HR Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access payroll", + "type": "VAPT", + "route": "/employees/payroll", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-HRRULE-009", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Duplicate attendance same date rejected", + "type": "VAPT", + "route": "/employee/attendance", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-010", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Manual attendance CSRF-less POST rejected", + "type": "VAPT", + "route": "/employees/attendance/manual", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-011", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave exceeding balance rejected", + "type": "VAPT", + "route": "/employee/leaves", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-012", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave approve invalid ID rejected", + "type": "VAPT", + "route": "/employees/leave/999999/approve", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-013", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Leave reject invalid ID rejected", + "type": "VAPT", + "route": "/employees/leave/999999/reject", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-014", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll generate CSRF-less POST rejected", + "type": "VAPT", + "route": "/employees/payroll/generate", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-015", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll approve before generate invalid safe", + "type": "VAPT", + "route": "/employees/payroll/999999/approve", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-016", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payroll paid before approval invalid safe", + "type": "VAPT", + "route": "/employees/payroll/999999/mark-paid", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-017", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Payslip invalid ID safe", + "type": "VAPT", + "route": "/employees/payroll/999999/payslip", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-018", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Offboarding complete invalid employee safe", + "type": "VAPT", + "route": "/employees/999999/offboarding/complete", + "_kind": "safe-page", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-019", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Employee document approve invalid ID rejected", + "type": "VAPT", + "route": "/employees/documents/999999/approve", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-HRRULE-020", + "module": "Employee HR Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Employee document reject invalid ID rejected", + "type": "VAPT", + "route": "/employees/documents/999999/reject", + "_kind": "post", + "_method": "GET", + "_file": "employee-hr-business-rules.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Employee HR Business Rules", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/employees.spec.js b/tests/employees.spec.js new file mode 100644 index 0000000..81a6889 --- /dev/null +++ b/tests/employees.spec.js @@ -0,0 +1,519 @@ +/** + * ============================================================================= + * UAT_Employees -- HR / Employee Self-Service module + * ============================================================================= + * + * Covers: + * EMP-HR-* : HR admin routes (/employees/...) + * EMP-ESS-* : Employee self-service portal (/employee/...) + * EMP-RBAC-* : Access control — staff should not reach HR admin pages + * EMP-SEC-* : CSRF, upload security, IDOR probes + * + * Required .env additions (run seed/seed_uat_data_production_gitea.py first): + * EMPLOYEE_A_ID= # a seeded active employee id + * EMPLOYEE_B_ID= # a second employee in the same tenant (for IDOR) + * PAYROLL_RUN_ID= # a seeded payroll run id (status: draft) + * SALARY_STRUCTURE_ID= # a seeded salary structure id + * LEAVE_TYPE_ID= # a seeded leave type id + * LEAVE_REQUEST_ID= # a seeded pending leave request id + * ATTENDANCE_ID= # a seeded manual attendance record id + * REG_REQUEST_ID= # a seeded pending employee registration request id + * ONBOARDING_TASK_ID= # a seeded onboarding task id + * OFFBOARDING_REQ_ID= # a seeded offboarding request id + * EMP_DOCUMENT_ID= # a seeded employee document id + * + * Tests that require specific IDs skip gracefully when the env var is absent. + * ============================================================================= + */ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +const { login } = require('../fixtures/auth'); +const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); +const { expectBlockedOrSafe } = require('../fixtures/assertions'); + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +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`); +} + +// --------------------------------------------------------------------------- +// HR Dashboard +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: HR dashboard and employee list', () => { + + test('[V25-EMP-HR-001] EMP-HR-001 HR dashboard loads without error for Firm Admin', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/employees/dashboard'); + await expectNoBackendError(page); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-EMP-HR-002] EMP-HR-002 Employee list loads for Firm Admin', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees'); + await expectNoBackendError(page); + const body = await readBody(page); + // should see a list or empty state — no crash + expect(body.length).toBeGreaterThan(0); + }); + + test('[V25-EMP-HR-003] EMP-HR-003 Employee list search does not crash', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees?q=test&link_status=linked'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-HR-004] EMP-HR-004 Employee create form loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/new'); + await expectNoBackendError(page); + const body = await readBody(page); + expect(/full.?name|employee.?code|create|add/i.test(body)).toBeTruthy(); + }); + + test('[V25-EMP-HR-005] EMP-HR-005 Employee create with blank form shows validation, not 500', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/new'); + const submit = page.locator('button[type="submit"], input[type="submit"]').first(); + if (await submit.count()) { + await submit.click().catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + } + await expectNoBackendError(page); + }); + + test('[V25-EMP-HR-006] EMP-HR-006 Employee detail page loads', async ({ page }) => { + skipIfMissing('EMPLOYEE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/employees/${idOr('EMPLOYEE_A_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-EMP-HR-007] EMP-HR-007 Employee edit form loads', async ({ page }) => { + skipIfMissing('EMPLOYEE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/employees/${idOr('EMPLOYEE_A_ID')}/edit`); + await expectNoBackendError(page); + }); + +}); + +// --------------------------------------------------------------------------- +// Attendance (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Attendance management', () => { + + test('[V25-EMP-ATT-001] EMP-ATT-001 HR attendance list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/attendance'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ATT-002] EMP-ATT-002 HR attendance list with filters does not crash', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/attendance?status=present&approval_status=pending'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ATT-003] EMP-ATT-003 Manual attendance CSRF-less POST is rejected', async ({ request }) => { + const resp = await request.post(`${process.env.BASE_URL}/employees/attendance/manual`, { + form: { employee_id: idOr('EMPLOYEE_A_ID'), attendance_date: '2025-01-01', status: 'present', 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-EMP-ATT-004] EMP-ATT-004 Attendance review CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('ATTENDANCE_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/employees/attendance/${idOr('ATTENDANCE_ID')}/review`, + { form: { approval_status: 'approved', 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(); + }); + +}); + +// --------------------------------------------------------------------------- +// Leave types & balances (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Leave management', () => { + + test('[V25-EMP-LEAVE-001] EMP-LEAVE-001 Leave types list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/leave-types'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-LEAVE-002] EMP-LEAVE-002 Leave balances list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/leave-balances'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-LEAVE-003] EMP-LEAVE-003 Leave requests list (pending) loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/leave?status=pending'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-LEAVE-004] EMP-LEAVE-004 Leave requests list (all) loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/leave?status=all'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-LEAVE-005] EMP-LEAVE-005 Leave review CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('LEAVE_REQUEST_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/employees/leave/${idOr('LEAVE_REQUEST_ID')}/review`, + { form: { status: 'approved', 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(); + }); + +}); + +// --------------------------------------------------------------------------- +// Onboarding / Offboarding (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Onboarding and offboarding', () => { + + test('[V25-EMP-OB-001] EMP-OB-001 Onboarding checklist template page loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/onboarding-checklist'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-OB-002] EMP-OB-002 Onboarding tasks list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/onboarding'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-OB-003] EMP-OB-003 Offboarding requests list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/offboarding'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-OB-004] EMP-OB-004 Registration requests list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/registration-requests'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-OB-005] EMP-OB-005 Registration request detail loads', async ({ page }) => { + skipIfMissing('REG_REQUEST_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/employees/registration-requests/${idOr('REG_REQUEST_ID')}`); + await expectNoBackendError(page); + }); + +}); + +// --------------------------------------------------------------------------- +// Documents (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Employee documents', () => { + + test('[V25-EMP-DOC-001] EMP-DOC-001 Document types list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/document-types'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-DOC-002] EMP-DOC-002 Employee documents list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/documents'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-DOC-003] EMP-DOC-003 Executable upload to employee documents is blocked', async ({ page }) => { + skipIfMissing('EMPLOYEE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/employees/${idOr('EMPLOYEE_A_ID')}`); + await expectNoBackendError(page); + const fileInput = page.locator('input[type="file"]').first(); + if (!(await fileInput.count())) test.skip(true, 'No file input on employee detail page'); + const { uploadPath } = require('../fixtures/v204-helpers'); + await fileInput.setInputFiles(uploadPath('not-a-pdf.exe')); + const submit = page.locator('form:has(input[type="file"]) button[type="submit"]').first(); + if (await submit.count()) await submit.click().catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + await expectNoBackendError(page); + const body = await readBody(page); + expect(/not allowed|invalid|blocked|file type|extension|upload failed|dangerous|forbidden/i.test(body)).toBeTruthy(); + }); + + test('[V25-EMP-DOC-004] EMP-DOC-004 Document review CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('EMP_DOCUMENT_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/employees/documents/${idOr('EMP_DOCUMENT_ID')}/review`, + { form: { status: 'verified', 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(); + }); + +}); + +// --------------------------------------------------------------------------- +// Payroll (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Payroll', () => { + + test('[V25-EMP-PAY-001] EMP-PAY-001 Salary structures list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/payroll/structures'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-PAY-002] EMP-PAY-002 Payroll runs list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/payroll/runs'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-PAY-003] EMP-PAY-003 Payslips list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/payroll/payslips'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-PAY-004] EMP-PAY-004 Payslips filtered by run loads', async ({ page }) => { + skipIfMissing('PAYROLL_RUN_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/employees/payroll/payslips?payroll_run_id=${idOr('PAYROLL_RUN_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-EMP-PAY-005] EMP-PAY-005 Payroll run generate CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('PAYROLL_RUN_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/employees/payroll/runs/${idOr('PAYROLL_RUN_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-EMP-PAY-006] EMP-PAY-006 Payroll run approve CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('PAYROLL_RUN_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/employees/payroll/runs/${idOr('PAYROLL_RUN_ID')}/approve`, + { 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 assignment dashboard (HR admin) +// --------------------------------------------------------------------------- + +test.describe('EMP-HR: Work allocation', () => { + + test('[V25-EMP-WORK-001] EMP-WORK-001 Work allocation dashboard loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/work'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-WORK-002] EMP-WORK-002 Engagement progress dashboard loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/progress'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-WORK-003] EMP-WORK-003 HR Excel imports page loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/imports'); + await expectNoBackendError(page); + }); + +}); + +// --------------------------------------------------------------------------- +// Employee Self-Service portal (/employee/...) +// --------------------------------------------------------------------------- + +test.describe('EMP-ESS: Employee self-service portal', () => { + + test('[V25-EMP-ESS-001] EMP-ESS-001 ESS dashboard loads for Staff', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/dashboard'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-002] EMP-ESS-002 My attendance page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/attendance'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-003] EMP-ESS-003 My leave page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/leave'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-004] EMP-ESS-004 My documents page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/documents'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-005] EMP-ESS-005 My payslips page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/payslips'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-006] EMP-ESS-006 My work kanban loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/work'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-007] EMP-ESS-007 My offboarding page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/offboarding'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-008] EMP-ESS-008 My profile page loads', async ({ page }) => { + await login(page, 'Staff'); + await safeGoto(page, '/employee/profile'); + await expectNoBackendError(page); + }); + + test('[V25-EMP-ESS-009] EMP-ESS-009 Employee registration form loads for unlinked user', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employee/register'); + await expectNoBackendError(page); + // may redirect to dashboard if already registered — both are fine + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-EMP-ESS-010] EMP-ESS-010 Leave application CSRF-less POST is rejected', async ({ request }) => { + const resp = await request.post(`${process.env.BASE_URL}/employee/leave/apply`, { + form: { leave_type_id: '1', from_date: '2025-06-01', to_date: '2025-06-01', 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(); + }); + +}); + +// --------------------------------------------------------------------------- +// RBAC: Staff must not reach HR admin pages +// --------------------------------------------------------------------------- + +test.describe('EMP-RBAC: Access control', () => { + + test('[V25-EMP-RBAC-001] EMP-RBAC-001 Staff cannot access HR dashboard', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employees/dashboard'); + await expectNoBackendError(page); + const body = await readBody(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-EMP-RBAC-002] EMP-RBAC-002 Staff cannot access employee list', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employees'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-EMP-RBAC-003] EMP-RBAC-003 Staff cannot access payroll runs', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employees/payroll/runs'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-EMP-RBAC-004] EMP-RBAC-004 Client cannot access employee portal', async ({ page }) => { + await login(page, 'Client'); + const resp = await safeGoto(page, '/employee/dashboard'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-EMP-RBAC-005] EMP-RBAC-005 Anonymous cannot access any employee route', async ({ page }) => { + for (const route of ['/employees', '/employees/dashboard', '/employee/dashboard']) { + const resp = await safeGoto(page, route); + await expectBlockedOrSafe(page, resp); + } + }); + +}); + +// --------------------------------------------------------------------------- +// IDOR / Security probes +// --------------------------------------------------------------------------- + +test.describe('EMP-SEC: Security probes', () => { + + test('[V25-EMP-SEC-001] EMP-SEC-001 Non-existent employee ID returns safe response', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employees/999999999'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-EMP-SEC-002] EMP-SEC-002 Non-existent payroll run ID returns safe response', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/employees/payroll/payslips?payroll_run_id=999999999'); + await expectNoBackendError(page); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-EMP-SEC-003] EMP-SEC-003 HR import commit without preview session is rejected safely', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/employees/imports'); + // Attempt to POST commit without having a session preview + const resp = await page.request.post(`${process.env.BASE_URL}/employees/imports/commit`, { + form: { csrf_token: 'invalid' }, + }).catch(() => null); + if (!resp) test.skip(true, 'Request failed at network level'); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-EMP-SEC-004] EMP-SEC-004 Payroll run generate by Staff is blocked', async ({ request }) => { + skipIfMissing('PAYROLL_RUN_ID'); + // Staff should not be able to trigger payroll — attempt raw API call + const resp = await request.post( + `${process.env.BASE_URL}/employees/payroll/runs/${idOr('PAYROLL_RUN_ID')}/generate`, + { form: { 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(); + }); + +}); diff --git a/tests/marketplace-leads.spec.js b/tests/marketplace-leads.spec.js new file mode 100644 index 0000000..e9e33bc --- /dev/null +++ b/tests/marketplace-leads.spec.js @@ -0,0 +1,373 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-MKT-001", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Marketplace dashboard loads", + "type": "UAT", + "route": "/marketplace", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-002", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Leads list loads", + "type": "UAT", + "route": "/marketplace/leads", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-003", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "New lead page loads", + "type": "UAT", + "route": "/marketplace/leads/new", + "_kind": "page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-004", + "module": "Marketplace / Leads", + "role": "Public", + "scenario": "Public lead page loads without auth", + "type": "UAT", + "route": "/marketplace/public-lead", + "_kind": "public", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-005", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Invalid lead detail safe", + "type": "VAPT", + "route": "/marketplace/leads/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-006", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead assign invalid ID safe", + "type": "VAPT", + "route": "/marketplace/leads/999999/assign", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-007", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead status invalid ID safe", + "type": "VAPT", + "route": "/marketplace/leads/999999/status", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-008", + "module": "Marketplace / Leads", + "role": "Firm Admin", + "scenario": "Lead convert invalid ID safe", + "type": "VAPT", + "route": "/marketplace/leads/999999/convert-client", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-MKT-009", + "module": "Marketplace / Leads", + "role": "Staff", + "scenario": "Staff lead list access blocked or safe", + "type": "VAPT", + "route": "/marketplace/leads", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-MKT-010", + "module": "Marketplace / Leads", + "role": "Client", + "scenario": "Client lead list access blocked", + "type": "VAPT", + "route": "/marketplace/leads", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-MKT-011", + "module": "Marketplace / Leads", + "role": "Consultant", + "scenario": "Consultant internal leads access blocked", + "type": "VAPT", + "route": "/marketplace/leads", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-MKT-012", + "module": "Marketplace / Leads", + "role": "Public", + "scenario": "Public route does not expose internal list", + "type": "VAPT", + "route": "/marketplace/public-lead", + "_kind": "public-no-sensitive", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-013", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Public lead blank CSRF-less POST handled safely", + "type": "VAPT", + "route": "/marketplace/public-lead", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-014", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Internal lead create CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/new", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-015", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead assign CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/999999/assign", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-016", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead status CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/999999/status", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-017", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead convert-client CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/999999/convert-client", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-018", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead duplicate check invalid POST safe", + "type": "VAPT", + "route": "/marketplace/leads/check-duplicate", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-019", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead note add CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/999999/notes", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-020", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead attachment upload CSRF-less POST rejected", + "type": "VAPT", + "route": "/marketplace/leads/999999/upload", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-021", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant lead probe safe", + "type": "VAPT", + "route": "/marketplace/leads/999999?tenant_id=999999", + "_kind": "post", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + }, + { + "variantId": "V251-MKT-022", + "module": "Marketplace / Leads", + "role": "Anonymous/Attacker", + "scenario": "Lead export requires authorization", + "type": "VAPT", + "route": "/marketplace/leads/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "marketplace-leads.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Marketplace / Leads", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/notice-case-business-rules.spec.js b/tests/notice-case-business-rules.spec.js new file mode 100644 index 0000000..31cfb5d --- /dev/null +++ b/tests/notice-case-business-rules.spec.js @@ -0,0 +1,385 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-NCASE-001", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Notice cases list loads", + "type": "UAT", + "route": "/notice-cases", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-002", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Notice case new page loads", + "type": "UAT", + "route": "/notice-cases/new", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-003", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case detail safe", + "type": "VAPT", + "route": "/notice-cases/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-004", + "module": "Notice Case Business Rules", + "role": "Client", + "scenario": "Client cannot access internal case list", + "type": "VAPT", + "route": "/notice-cases", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-NCASE-005", + "module": "Notice Case Business Rules", + "role": "Consultant", + "scenario": "Consultant cannot access internal case list", + "type": "VAPT", + "route": "/notice-cases", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-NCASE-006", + "module": "Notice Case Business Rules", + "role": "Staff", + "scenario": "Staff case list access safe", + "type": "UAT", + "route": "/notice-cases", + "_kind": "page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-NCASE-007", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case events safe", + "type": "VAPT", + "route": "/notice-cases/999999/events", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-008", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case hearing safe", + "type": "VAPT", + "route": "/notice-cases/999999/hearings", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-009", + "module": "Notice Case Business Rules", + "role": "Firm Admin", + "scenario": "Invalid case order safe", + "type": "VAPT", + "route": "/notice-cases/999999/orders", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-NCASE-010", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Create notice case CSRF-less POST rejected", + "type": "VAPT", + "route": "/notice-cases/new", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-011", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Notice date/hearing date invalid POST safe", + "type": "VAPT", + "route": "/notice-cases/999999/hearings", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-012", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Order date before hearing invalid POST safe", + "type": "VAPT", + "route": "/notice-cases/999999/orders", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-013", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Invalid case status transition rejected", + "type": "VAPT", + "route": "/notice-cases/999999/status", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-014", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case document upload invalid ID rejected", + "type": "VAPT", + "route": "/notice-cases/999999/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-015", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-case document IDOR safe", + "type": "VAPT", + "route": "/notice-cases/999999/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-016", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Client-visible document invalid route safe", + "type": "VAPT", + "route": "/client/notice-cases/999999/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-017", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Internal note hidden from client route safe", + "type": "VAPT", + "route": "/client/notice-cases/999999/notes", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-018", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case deadline reminder invalid ID safe", + "type": "VAPT", + "route": "/notice-cases/999999/reminders", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-019", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case assignment CSRF-less POST rejected", + "type": "VAPT", + "route": "/notice-cases/999999/assign", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-020", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case event delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/notice-cases/events/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-021", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case hearing delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/notice-cases/hearings/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-022", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Case order delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/notice-cases/orders/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + }, + { + "variantId": "V251-NCASE-023", + "module": "Notice Case Business Rules", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant notice case IDOR safe", + "type": "VAPT", + "route": "/notice-cases/999999?tenant_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "notice-case-business-rules.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Notice Case Business Rules", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/noticecases-services-work.spec.js b/tests/noticecases-services-work.spec.js new file mode 100644 index 0000000..73c8961 --- /dev/null +++ b/tests/noticecases-services-work.spec.js @@ -0,0 +1,331 @@ +/** + * ============================================================================= + * 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 { 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( + `${process.env.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( + `${process.env.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 resp = await safeGoto(page, '/services/bulk-imports/templates/service-master.xlsx'); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-SVC-003] SVC-003 Bulk imports engagement-assignments template downloads', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/services/bulk-imports/templates/engagement-assignments.xlsx'); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-SVC-004] SVC-004 Bulk imports firm-task-templates template downloads', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/services/bulk-imports/templates/firm-task-templates.xlsx'); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-SVC-005] SVC-005 Bulk import preview CSRF-less POST is rejected', async ({ request }) => { + const resp = await request.post( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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( + `${process.env.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); + }); + +}); diff --git a/tests/partners-billing.spec.js b/tests/partners-billing.spec.js new file mode 100644 index 0000000..20dc531 --- /dev/null +++ b/tests/partners-billing.spec.js @@ -0,0 +1,233 @@ +/** + * ============================================================================= + * UAT_Partners_Billing -- Partners portal + firm-level billing + * ============================================================================= + * + * Covers: + * PART-* : Partner portal (/partner/...) and partner-facing review routes + * BILL-* : Firm billing — invoices, payments, receipts, fee structures + * RBAC-* : Access control checks across both modules + * SEC-* : CSRF rejection and anonymous access probes + * + * Required .env additions: + * INVOICE_A_ID= # a seeded firm invoice id (status: draft) + * PAYMENT_A_ID= # a seeded payment id against INVOICE_A_ID + * PARTNER_TASK_A_ID= # a seeded service task assigned for partner review + * + * Tests skip gracefully when env vars are absent. + * ============================================================================= + */ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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`); +} + +// --------------------------------------------------------------------------- +// Partners portal +// --------------------------------------------------------------------------- + +test.describe('PART: Partner portal', () => { + + test('[V25-PART-001] PART-001 Partner dashboard loads', async ({ page }) => { + await login(page, 'Partner'); + const resp = await safeGoto(page, '/partner/dashboard'); + await expectNoBackendError(page); + expect(resp.status()).toBeLessThan(500); + }); + + test('[V25-PART-002] PART-002 Partner client list loads', async ({ page }) => { + await login(page, 'Partner'); + await safeGoto(page, '/partner/clients'); + await expectNoBackendError(page); + }); + + test('[V25-PART-003] PART-003 Partner reviews list loads', async ({ page }) => { + await login(page, 'Partner'); + await safeGoto(page, '/partner/reviews'); + await expectNoBackendError(page); + }); + + test('[V25-PART-004] PART-004 Partner task review page loads', async ({ page }) => { + skipIfMissing('PARTNER_TASK_A_ID'); + await login(page, 'Partner'); + await safeGoto(page, `/partner/tasks/${idOr('PARTNER_TASK_A_ID')}/review`); + await expectNoBackendError(page); + }); + + test('[V25-PART-005] PART-005 Partner task review CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('PARTNER_TASK_A_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/partner/tasks/${idOr('PARTNER_TASK_A_ID')}/review`, + { form: { status: 'approved', 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-PART-006] PART-006 Staff cannot access partner dashboard', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/partner/dashboard'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-PART-007] PART-007 Client cannot access partner reviews', async ({ page }) => { + await login(page, 'Client'); + const resp = await safeGoto(page, '/partner/reviews'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-PART-008] PART-008 Anonymous access to partner dashboard is blocked', async ({ page }) => { + const resp = await safeGoto(page, '/partner/dashboard'); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-PART-009] PART-009 Non-existent task review ID returns safe response', async ({ page }) => { + await login(page, 'Partner'); + const resp = await safeGoto(page, '/partner/tasks/999999999/review'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + +}); + +// --------------------------------------------------------------------------- +// Firm billing — invoices +// --------------------------------------------------------------------------- + +test.describe('BILL: Invoices and payments', () => { + + test('[V25-BILL-001] BILL-001 Invoice list loads for Firm Admin', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/billing'); + await expectNoBackendError(page); + }); + + test('[V25-BILL-002] BILL-002 Invoice create form loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/billing/invoices/new'); + await expectNoBackendError(page); + }); + + test('[V25-BILL-003] BILL-003 Invoice detail page loads', async ({ page }) => { + skipIfMissing('INVOICE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/billing/invoices/${idOr('INVOICE_A_ID')}`); + await expectNoBackendError(page); + }); + + test('[V25-BILL-004] BILL-004 Invoice print page loads', async ({ page }) => { + skipIfMissing('INVOICE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/billing/invoices/${idOr('INVOICE_A_ID')}/print`); + await expectNoBackendError(page); + }); + + test('[V25-BILL-005] BILL-005 Invoice payments subpage loads', async ({ page }) => { + skipIfMissing('INVOICE_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/billing/invoices/${idOr('INVOICE_A_ID')}/payments`); + await expectNoBackendError(page); + }); + + test('[V25-BILL-006] BILL-006 Payment receipt loads', async ({ page }) => { + skipIfMissing('PAYMENT_A_ID'); + await login(page, 'Firm Admin'); + await safeGoto(page, `/billing/payments/${idOr('PAYMENT_A_ID')}/receipt`); + await expectNoBackendError(page); + }); + + test('[V25-BILL-007] BILL-007 Payments list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/billing/payments'); + await expectNoBackendError(page); + }); + + test('[V25-BILL-008] BILL-008 Fee structures list loads', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/billing/fee-structures/list'); + await expectNoBackendError(page); + }); + + test('[V25-BILL-009] BILL-009 Invoice post CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('INVOICE_A_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/billing/invoices/${idOr('INVOICE_A_ID')}/issue`, + { 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-BILL-010] BILL-010 New payment CSRF-less POST is rejected', async ({ request }) => { + skipIfMissing('INVOICE_A_ID'); + const resp = await request.post( + `${process.env.BASE_URL}/billing/invoices/${idOr('INVOICE_A_ID')}/payments/new`, + { form: { amount: '1000', 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-BILL-011] BILL-011 Staff cannot access invoice list', async ({ page }) => { + await login(page, 'Staff'); + const resp = await safeGoto(page, '/billing'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-BILL-012] BILL-012 Client cannot access billing admin', async ({ page }) => { + await login(page, 'Client'); + const resp = await safeGoto(page, '/billing/invoices/new'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-BILL-013] BILL-013 Anonymous access to billing is blocked', async ({ page }) => { + const resp = await safeGoto(page, '/billing'); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-BILL-014] BILL-014 Non-existent invoice ID returns safe response', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/billing/invoices/999999999'); + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + + test('[V25-BILL-015] BILL-015 Invoice create with blank form shows validation, not 500', async ({ page }) => { + await login(page, 'Firm Admin'); + await safeGoto(page, '/billing/invoices/new'); + const submit = page.locator('button[type="submit"], input[type="submit"]').first(); + if (await submit.count()) { + await submit.click().catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + } + await expectNoBackendError(page); + }); + + test('[V25-BILL-016] BILL-016 Fee structure import template download works', async ({ page }) => { + await login(page, 'Firm Admin'); + const resp = await safeGoto(page, '/billing/fee-structures/template'); + // should download or redirect, not crash + expect(resp.status()).toBeLessThan(500); + }); + +}); diff --git a/tests/platform-billing.spec.js b/tests/platform-billing.spec.js new file mode 100644 index 0000000..ca53abf --- /dev/null +++ b/tests/platform-billing.spec.js @@ -0,0 +1,529 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-PBILL-001", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Plans page loads", + "type": "UAT", + "route": "/platform-billing/plans", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-002", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Accounts page loads", + "type": "UAT", + "route": "/platform-billing/accounts", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-003", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Audit firm subscriptions page loads", + "type": "UAT", + "route": "/platform-billing/audit-firm-subscriptions", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-004", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Client dashboard subscriptions page loads", + "type": "UAT", + "route": "/platform-billing/client-dashboard-subscriptions", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-005", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Consultant subscriptions page loads", + "type": "UAT", + "route": "/platform-billing/consultant-subscriptions", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-006", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Subscriptions page loads", + "type": "UAT", + "route": "/platform-billing/subscriptions", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-007", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invoices page loads", + "type": "UAT", + "route": "/platform-billing/invoices", + "_kind": "page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-008", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid invoice detail/post safe", + "type": "UAT", + "route": "/platform-billing/invoices/999999/post", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-009", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid invoice payments safe", + "type": "UAT", + "route": "/platform-billing/invoices/999999/payments", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-010", + "module": "Platform Billing", + "role": "System Admin", + "scenario": "Invalid plan detail safe", + "type": "UAT", + "route": "/platform-billing/plans/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-PBILL-011", + "module": "Platform Billing", + "role": "Firm", + "scenario": "Firm Admin cannot manage platform plans", + "type": "VAPT", + "route": "/platform-billing/plans", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-PBILL-012", + "module": "Platform Billing", + "role": "Staff", + "scenario": "Staff cannot access platform accounts", + "type": "VAPT", + "route": "/platform-billing/accounts", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-PBILL-013", + "module": "Platform Billing", + "role": "Client", + "scenario": "Client cannot access platform invoices list", + "type": "VAPT", + "route": "/platform-billing/invoices", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-PBILL-014", + "module": "Platform Billing", + "role": "Consultant", + "scenario": "Consultant cannot access platform invoice list", + "type": "VAPT", + "route": "/platform-billing/invoices", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-PBILL-015", + "module": "Platform Billing", + "role": "Partner", + "scenario": "Partner cannot post platform invoice", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/post", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": "Partner" + }, + { + "variantId": "V251-PBILL-016", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/plans", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-017", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan update invalid ID CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/plans/999999", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-018", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Account create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/accounts", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-019", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Audit firm subscription create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/audit-firm-subscriptions", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-020", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Client dashboard subscription create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/client-dashboard-subscriptions", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-021", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Consultant subscription create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/consultant-subscriptions", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-022", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Platform subscription create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/subscriptions", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-023", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice create CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/invoices", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-024", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice post CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/post", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-025", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice payment CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/payments", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-026", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice cancel CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/cancel", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-027", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Payment over-amount invalid POST safe", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/payments", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-028", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Duplicate plan code invalid POST safe", + "type": "VAPT", + "route": "/platform-billing/plans", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-029", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Duplicate invoice number invalid POST safe", + "type": "VAPT", + "route": "/platform-billing/invoices", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-030", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice export requires auth", + "type": "VAPT", + "route": "/platform-billing/invoices/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-031", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Invoice PDF invalid ID safe", + "type": "VAPT", + "route": "/platform-billing/invoices/999999/pdf", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-032", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Cross-account invoice IDOR blocked", + "type": "VAPT", + "route": "/platform-billing/invoices/999999?account_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-033", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Payment receipt invalid ID safe", + "type": "VAPT", + "route": "/platform-billing/payments/999999/receipt", + "_kind": "safe-page", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-034", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Billing account delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/accounts/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + }, + { + "variantId": "V251-PBILL-035", + "module": "Platform Billing", + "role": "Anonymous/Attacker", + "scenario": "Plan delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/platform-billing/plans/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "platform-billing.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Platform Billing", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/system-settings-tenancy.spec.js b/tests/system-settings-tenancy.spec.js new file mode 100644 index 0000000..42fee02 --- /dev/null +++ b/tests/system-settings-tenancy.spec.js @@ -0,0 +1,541 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-SYS-001", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Tenants page loads", + "type": "UAT", + "route": "/system-settings/tenants", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-002", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Branches page loads", + "type": "UAT", + "route": "/system-settings/branches", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-003", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Branding page loads", + "type": "UAT", + "route": "/system-settings/branding", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-004", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Financial years page loads", + "type": "UAT", + "route": "/system-settings/financial-years", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-005", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "RBAC page loads", + "type": "UAT", + "route": "/system-settings/rbac", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-006", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Roles page loads", + "type": "UAT", + "route": "/system-settings/rbac/roles", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-007", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Permissions page loads", + "type": "UAT", + "route": "/system-settings/rbac/permissions", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-008", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Audit logs page loads", + "type": "UAT", + "route": "/system-settings/audit-logs", + "_kind": "page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-009", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid tenant context switch safe", + "type": "UAT", + "route": "/system-settings/context/tenant/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-010", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid branch context switch safe", + "type": "UAT", + "route": "/system-settings/context/branch/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-011", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "Invalid financial year context switch safe", + "type": "UAT", + "route": "/system-settings/context/financial-year/INVALID", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-012", + "module": "System Settings / Tenancy / FY", + "role": "System Admin", + "scenario": "FY backup export page/action safe", + "type": "UAT", + "route": "/system-settings/financial-years/backup-export", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "System Admin" + }, + { + "variantId": "V251-SYS-013", + "module": "System Settings / Tenancy / FY", + "role": "Staff", + "scenario": "Staff cannot access tenants", + "type": "VAPT", + "route": "/system-settings/tenants", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-SYS-014", + "module": "System Settings / Tenancy / FY", + "role": "Client", + "scenario": "Client cannot access branches", + "type": "VAPT", + "route": "/system-settings/branches", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-SYS-015", + "module": "System Settings / Tenancy / FY", + "role": "Consultant", + "scenario": "Consultant cannot access financial years", + "type": "VAPT", + "route": "/system-settings/financial-years", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-SYS-016", + "module": "System Settings / Tenancy / FY", + "role": "Partner", + "scenario": "Partner cannot manage RBAC roles unless permitted", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Partner" + }, + { + "variantId": "V251-SYS-017", + "module": "System Settings / Tenancy / FY", + "role": "Manager", + "scenario": "Manager cannot switch unauthorized tenant", + "type": "VAPT", + "route": "/system-settings/context/tenant/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Manager" + }, + { + "variantId": "V251-SYS-018", + "module": "System Settings / Tenancy / FY", + "role": "Firm Admin", + "scenario": "Firm Admin cannot switch to unauthorized branch", + "type": "VAPT", + "route": "/system-settings/context/branch/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-SYS-019", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Tenant create CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/tenants", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-020", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Tenant update invalid ID CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/tenants/999999", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-021", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branch create CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/branches", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-022", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branch update invalid ID CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/branches/999999", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-023", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Branding save CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/branding", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-024", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Financial year create CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/financial-years", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-025", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Make current FY CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/make-current", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-026", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Lock FY CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/lock", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-027", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Unlock FY CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/unlock", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-028", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "FY backup export CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/financial-years/999999/backup-export", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-029", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "FY backup download invalid ID safe", + "type": "VAPT", + "route": "/system-settings/financial-years/backups/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-030", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Role create CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-031", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Role update invalid ID CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-032", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Permission assign CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999/permissions", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-033", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Permission remove CSRF-less POST rejected", + "type": "VAPT", + "route": "/system-settings/rbac/roles/999999/permissions/999999/remove", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-034", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Duplicate role invalid POST safe", + "type": "VAPT", + "route": "/system-settings/rbac/roles", + "_kind": "post", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-035", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Unauthorized branch context query safe", + "type": "VAPT", + "route": "/system-settings/context/branch/999999?tenant_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + }, + { + "variantId": "V251-SYS-036", + "module": "System Settings / Tenancy / FY", + "role": "Anonymous/Attacker", + "scenario": "Audit log export requires authorization", + "type": "VAPT", + "route": "/system-settings/audit-logs/export", + "_kind": "safe-page", + "_method": "GET", + "_file": "system-settings-tenancy.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - System Settings / Tenancy / FY", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +}); diff --git a/tests/work-lifecycle-e2e.spec.js b/tests/work-lifecycle-e2e.spec.js new file mode 100644 index 0000000..6606bae --- /dev/null +++ b/tests/work-lifecycle-e2e.spec.js @@ -0,0 +1,469 @@ + +const { test, expect } = require('@playwright/test'); +require('dotenv').config(); +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; +} + +async function expectRouteAvailable(page, resp) { + await expectNoBackendError(page); + if (resp) { + expect(resp.status(), `Expected route to exist but got ${resp.status()} at ${page.url()}`).not.toBe(404); + expect(resp.status()).toBeLessThan(500); + } +} + +async function expectNoSecrets(page) { + const body = await readBody(page); + expect(body).not.toMatch(/Password@123|Pass@123|smtp_password|IMAP_PASSWORD|SMTP_PASSWORD|otp\s*[:=]\s*\d{4,8}|reset_token|access_token|refresh_token/i); +} + +async function expectPublicNoSensitive(page) { + await expectNoBackendError(page); + const body = await readBody(page); + expect(body).not.toMatch(/tenant_id|branch_id|internal note|audit log|smtp|password|secret|token/i); +} + +async function apiCall(request, method, route) { + const url = `${process.env.BASE_URL}${route}`; + const opts = { data: { csrf_token: '', test_payload: 'uat-vapt' }, headers: { 'Content-Type': 'application/json' } }; + if (method === 'GET') return await request.get(url).catch(() => null); + if (method === 'POST') return await request.post(url, opts).catch(() => null); + if (method === 'PUT') return await request.put(url, opts).catch(() => null); + if (method === 'DELETE') return await request.delete(url).catch(() => null); + if (method === 'OPTIONS') return await request.fetch(url, { method: 'OPTIONS' }).catch(() => null); + return await request.fetch(url, { method }).catch(() => null); +} + +async function expectApiSafe(resp) { + expect(resp, 'API response should be available').toBeTruthy(); + expect(resp.status()).toBeLessThan(500); + const text = await resp.text().catch(() => ''); + expect(text).not.toMatch(/Traceback|Exception in ASGI application|OperationalError|ProgrammingError|AttributeError|UndefinedError|Password@123|Pass@123/i); +} + + +const cases = [ + { + "variantId": "V251-WORK2-001", + "module": "Work Lifecycle E2E", + "role": "Firm Admin", + "scenario": "Engagement create page/list loads", + "type": "UAT", + "route": "/work/engagements", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-WORK2-002", + "module": "Work Lifecycle E2E", + "role": "Firm Admin", + "scenario": "Existing engagement detail loads", + "type": "VAPT", + "route": "/work/engagements/1", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Firm Admin" + }, + { + "variantId": "V251-WORK2-003", + "module": "Work Lifecycle E2E", + "role": "Staff", + "scenario": "Staff work board loads", + "type": "UAT", + "route": "/work", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-WORK2-004", + "module": "Work Lifecycle E2E", + "role": "Manager", + "scenario": "Manager team work board loads", + "type": "UAT", + "route": "/manager/work", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Manager" + }, + { + "variantId": "V251-WORK2-005", + "module": "Work Lifecycle E2E", + "role": "Partner", + "scenario": "Partner reviews page loads", + "type": "UAT", + "route": "/partner/reviews", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Partner" + }, + { + "variantId": "V251-WORK2-006", + "module": "Work Lifecycle E2E", + "role": "Client", + "scenario": "Client dashboard compliance item list loads", + "type": "UAT", + "route": "/client/dashboard", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-WORK2-007", + "module": "Work Lifecycle E2E", + "role": "Consultant", + "scenario": "Consultant assignment area loads", + "type": "UAT", + "route": "/consultants", + "_kind": "page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-WORK2-008", + "module": "Work Lifecycle E2E", + "role": "Staff", + "scenario": "Invalid engagement ID is safe", + "type": "VAPT", + "route": "/work/engagements/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Staff" + }, + { + "variantId": "V251-WORK2-009", + "module": "Work Lifecycle E2E", + "role": "Client", + "scenario": "Client cannot access internal engagement detail", + "type": "VAPT", + "route": "/work/engagements/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Client" + }, + { + "variantId": "V251-WORK2-010", + "module": "Work Lifecycle E2E", + "role": "Consultant", + "scenario": "Consultant cannot access internal engagement detail", + "type": "VAPT", + "route": "/work/engagements/999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": "Consultant" + }, + { + "variantId": "V251-WORK2-011", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Create engagement CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/engagements", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-012", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Generate tasks CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/engagements/999999/generate-tasks", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-013", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Assign task CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/tasks/999999/assign", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-014", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Staff start task invalid ID safe", + "type": "VAPT", + "route": "/work/tasks/999999/start", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-015", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Staff complete task invalid ID safe", + "type": "VAPT", + "route": "/work/tasks/999999/complete", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-016", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task upload document invalid ID rejected", + "type": "VAPT", + "route": "/work/tasks/999999/documents/upload", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-017", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Manager review invalid task rejected", + "type": "VAPT", + "route": "/work/tasks/999999/review", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-018", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Partner approve invalid engagement rejected", + "type": "VAPT", + "route": "/work/engagements/999999/approve", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-019", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Partner reject invalid engagement rejected", + "type": "VAPT", + "route": "/work/engagements/999999/reject", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-020", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Add internal comment CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/engagements/999999/comments", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-021", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Add client-visible comment invalid route safe", + "type": "VAPT", + "route": "/work/engagements/999999/client-comments", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-022", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Blocked task without reason rejected", + "type": "VAPT", + "route": "/work/tasks/999999/block", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-023", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Completed task edit invalid ID safe", + "type": "VAPT", + "route": "/work/tasks/999999/edit", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-024", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Locked FY task change rejected or safe", + "type": "VAPT", + "route": "/work/tasks/999999/status", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-025", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task priority update CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/tasks/999999/priority", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-026", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Task due date update CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/tasks/999999/due-date", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-027", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Work document download invalid ID safe", + "type": "VAPT", + "route": "/work/documents/999999/download", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-028", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Work communication internal note hidden from client route safe", + "type": "VAPT", + "route": "/client/work/engagements/999999/comments", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-029", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Cross-tenant engagement IDOR safe", + "type": "VAPT", + "route": "/work/engagements/999999?tenant_id=999999", + "_kind": "safe-page", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + }, + { + "variantId": "V251-WORK2-030", + "module": "Work Lifecycle E2E", + "role": "Anonymous/Attacker", + "scenario": "Engagement delete CSRF-less POST rejected", + "type": "VAPT", + "route": "/work/engagements/999999/delete", + "_kind": "post", + "_method": "GET", + "_file": "work-lifecycle-e2e.spec.js", + "_login_role": null + } +]; + +test.describe("v2.5.1 Additions - Work Lifecycle E2E", () => { + + for (const c of cases) { + test(`[${c.variantId}] ${c.scenario}`, async ({ page, request }) => { + const role = c._login_role || c.role; + const kind = c._kind; + const method = c._method || 'GET'; + + if (kind === 'api') { + const resp = await apiCall(request, method, c.route); + await expectApiSafe(resp); + return; + } + + if (kind === 'post') { + const resp = await apiCall(request, method || 'POST', c.route); + await expectApiSafe(resp); + const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); + expect(safe, `Unsafe POST status ${resp.status()} for ${c.route}`).toBeTruthy(); + return; + } + + if (role && role !== 'Public' && !/Anonymous|Attacker|API/.test(role)) { + await login(page, role); + } + + const resp = await safeGoto(page, c.route); + + if (kind === 'page') { + await expectRouteAvailable(page, resp); + if (/password|secret|token|otp/i.test(c.scenario)) await expectNoSecrets(page); + return; + } + + if (kind === 'public') { + await expectRouteAvailable(page, resp); + return; + } + + if (kind === 'public-no-sensitive') { + await expectPublicNoSensitive(page); + return; + } + + if (kind === 'no-secret') { + await expectRouteAvailable(page, resp); + await expectNoSecrets(page); + return; + } + + await expectNoBackendError(page); + await expectBlockedOrSafe(page, resp); + }); + } +});