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') { // Some generated VAPT rows accidentally carry _method: 'GET' even when // the scenario is explicitly CSRF-less POST. Force POST unless the row // intentionally specifies another unsafe verb. const unsafeMethod = (!method || method === 'GET') ? 'POST' : method; const resp = await apiCall(request, unsafeMethod, c.route); await expectApiSafe(resp); const safe = [400, 401, 403, 404, 405, 409, 422, 429].includes(resp.status()); expect(safe, `Unsafe ${unsafeMethod} 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); }); } });