Initial Playwright ERP UAT VAPT test suite v2.4.1 IMAP
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
const { test, expect, request } = require('@playwright/test');
|
||||
require('dotenv').config();
|
||||
const matrix = require('../data/generated-test-matrix.json');
|
||||
const { login, logout, fillFirst, clickFirst } = require('../fixtures/auth');
|
||||
const { expectNoServerError, expectBlockedOrSafe, expectSecurityHeaders, expectCookieFlags } = require('../fixtures/assertions');
|
||||
|
||||
function title(v) {
|
||||
return `[${v.variantId}] ${v.sheet} :: ${v.scenario} :: ${v.variantName}`;
|
||||
}
|
||||
|
||||
function unauthorizedRole(v) {
|
||||
const r = (v.role || '').toLowerCase();
|
||||
if (r.includes('client')) return 'Staff';
|
||||
if (r.includes('staff') || r.includes('employee')) return 'Client';
|
||||
if (r.includes('consultant')) return 'Client';
|
||||
return 'Staff';
|
||||
}
|
||||
|
||||
async function safeGoto(page, route) {
|
||||
const resp = await page.goto(route || '/employee/dashboard');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
return resp;
|
||||
}
|
||||
|
||||
async function submitBlankFormIfAny(page) {
|
||||
const submit = page.locator('form button[type="submit"], form input[type="submit"]').first();
|
||||
if (await submit.count()) {
|
||||
await submit.click().catch(() => {});
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function boundaryInputIfAny(page) {
|
||||
const longText = 'X'.repeat(2048);
|
||||
const input = page.locator('form input[type="text"], form textarea').first();
|
||||
if (await input.count()) await input.fill(longText).catch(() => {});
|
||||
await submitBlankFormIfAny(page);
|
||||
}
|
||||
|
||||
async function runSecuritySpecific(page, context, v) {
|
||||
const id = v.sourceId;
|
||||
if (/SEC-001/i.test(id)) {
|
||||
await page.goto('/login');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await fillFirst(page, ['input[name="email"]','input[name="login_id"]','input[type="email"]'], process.env.SYSTEM_ADMIN_EMAIL || 'admin@auditfirm.local');
|
||||
await fillFirst(page, ['input[name="password"]','input[type="password"]'], 'wrong-password-' + i);
|
||||
await clickFirst(page, ['button[type="submit"]','input[type="submit"]']);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
}
|
||||
await expectNoServerError(page);
|
||||
return;
|
||||
}
|
||||
if (/SEC-002/i.test(id)) {
|
||||
await login(page, 'System Admin');
|
||||
await logout(page);
|
||||
const resp = await safeGoto(page, '/employee/dashboard');
|
||||
await expectBlockedOrSafe(page, resp);
|
||||
return;
|
||||
}
|
||||
if (/SEC-007|GEN-011/i.test(id)) {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const resp = await api.post('/notice-cases/new', { form: { title: 'csrf-test-no-token' } });
|
||||
expect([400,401,403,404,405,422,303].includes(resp.status())).toBeTruthy();
|
||||
return;
|
||||
}
|
||||
if (/SEC-017/i.test(id)) {
|
||||
await login(page, 'System Admin');
|
||||
await expectCookieFlags(context);
|
||||
return;
|
||||
}
|
||||
if (/SEC-026/i.test(id)) {
|
||||
const resp = await page.goto('/login');
|
||||
await expectSecurityHeaders(resp);
|
||||
return;
|
||||
}
|
||||
if (/SEC-010|SEC-103|SEC-104|SEC-110|SEC-111|DOM-VAPT/i.test(id)) {
|
||||
await login(page, 'Staff');
|
||||
const probes = ['/documents/../../app/main.py','/documents/999999/download','/notice-cases/999999999','/notice-cases/999999999/documents/999999/download','/domains/999999999/edit'];
|
||||
for (const p of probes) {
|
||||
const resp = await safeGoto(page, p);
|
||||
await expectBlockedOrSafe(page, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await login(page, v.role || 'System Admin');
|
||||
const resp = await safeGoto(page, v.route);
|
||||
await expectNoServerError(page);
|
||||
if (v.variantType === 'headers') await expectCookieFlags(context);
|
||||
}
|
||||
|
||||
for (const v of matrix) {
|
||||
test(title(v), async ({ page, context }) => {
|
||||
test.info().annotations.push({ type: 'sourceId', description: v.sourceId });
|
||||
test.info().annotations.push({ type: 'variantType', description: v.variantType });
|
||||
test.info().annotations.push({ type: 'expected', description: String(v.expected || '').slice(0, 250) });
|
||||
|
||||
if (v.automation === 'manual') {
|
||||
test.skip(true, v.manualReason || 'Marked manual in generated matrix');
|
||||
}
|
||||
|
||||
if (v.variantType === 'security' || v.sheet.startsWith('VAPT_')) {
|
||||
await runSecuritySpecific(page, context, v);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v.variantType === 'rbac') {
|
||||
await login(page, unauthorizedRole(v));
|
||||
const resp = await safeGoto(page, v.route);
|
||||
await expectNoServerError(page);
|
||||
// A safe result may be allowed page for common dashboards, but must not leak obvious restricted data or crash.
|
||||
if (!['/login','/employee/dashboard'].includes(v.route)) {
|
||||
const body = await page.locator('body').innerText().catch(()=>'');
|
||||
expect(body).not.toMatch(/Traceback|Internal Server Error|Exception/i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await login(page, v.role || 'System Admin');
|
||||
const resp = await safeGoto(page, v.route);
|
||||
await expectNoServerError(page);
|
||||
|
||||
if (v.variantType === 'positive') {
|
||||
expect(resp.status()).toBeLessThan(500);
|
||||
} else if (v.variantType === 'negative' || v.variantType === 'validation') {
|
||||
await submitBlankFormIfAny(page);
|
||||
await expectNoServerError(page);
|
||||
} else if (v.variantType === 'boundary') {
|
||||
await boundaryInputIfAny(page);
|
||||
await expectNoServerError(page);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* =============================================================================
|
||||
* UAT_FY_Lock_Backup -- Financial Year Lock & Backup UAT scaffolding
|
||||
* =============================================================================
|
||||
*
|
||||
* ⚠️ UNTESTED SCAFFOLDING -- READ BEFORE RUNNING ⚠️
|
||||
*
|
||||
* These 30 cases correspond to the UAT_FY_Lock_Backup sheet, which existed in
|
||||
* the checklist but was NOT part of the generated automation matrix. This file
|
||||
* was added in v2.4 to close that gap.
|
||||
*
|
||||
* It has NOT been executed or verified against a running ERP. The route paths,
|
||||
* selectors, and assertions below are best-effort guesses based on the ERP's
|
||||
* known routes (e.g. /system-settings/financial-years...). They are very likely
|
||||
* to need adjustment for your build. Every case is marked test.fixme() so the
|
||||
* suite does NOT report these as passing until a human has:
|
||||
* 1. Confirmed the real routes/selectors for each step,
|
||||
* 2. Implemented the assertion,
|
||||
* 3. Removed the test.fixme() wrapper for that case.
|
||||
*
|
||||
* DO NOT report these as automated coverage until that work is done.
|
||||
* Corresponding checklist rows remain "Not Started" / manual.
|
||||
* =============================================================================
|
||||
*/
|
||||
const { test, expect } = require('@playwright/test');
|
||||
require('dotenv').config();
|
||||
const { login } = require('../fixtures/auth');
|
||||
|
||||
const FY_BASE = '/system-settings/financial-years';
|
||||
const activeFY = process.env.ACTIVE_FY || process.env.DEFAULT_YEAR_CODE || '2025-26';
|
||||
|
||||
// Each entry maps to a checklist Test ID. status: 'fixme' until verified by a human.
|
||||
const FY_CASES = [
|
||||
['FY-001', 'Create financial year'],
|
||||
['FY-002', 'Duplicate FY prevention'],
|
||||
['FY-003', 'Mark current FY'],
|
||||
['FY-004', 'Header FY selector persistence'],
|
||||
['FY-005', 'Logout/login FY reset/default'],
|
||||
['FY-006', 'Staff tenant/branch from session'],
|
||||
['FY-007', 'Tenant switch updates session'],
|
||||
['FY-008', 'Branch switch updates session'],
|
||||
['FY-009', 'Engagement list active FY'],
|
||||
['FY-010', 'Direct URL cross-FY engagement'],
|
||||
['FY-011', 'Task list active FY'],
|
||||
['FY-012', 'Direct URL cross-FY task update'],
|
||||
['FY-013', 'Task/engagement document FY path'],
|
||||
['FY-014', 'Cross-FY document download'],
|
||||
['FY-015', 'Notice/case list active FY'],
|
||||
['FY-016', 'Notice engagement link same FY'],
|
||||
['FY-017', 'Invoice list active FY'],
|
||||
['FY-018', 'Client portal billing active FY'],
|
||||
['FY-019', 'Lock financial year'],
|
||||
['FY-020', 'Blocked writes in locked FY'],
|
||||
['FY-021', 'Unlock/reopen control'],
|
||||
['FY-022', 'Generate FY backup ZIP'],
|
||||
['FY-023', 'Backup export completeness'],
|
||||
['FY-024', 'Unauthorized backup access'],
|
||||
['FY-025', 'Backup not in static path'],
|
||||
['FY-026', 'Context switch auditability'],
|
||||
['FY-027', 'Fresh PostgreSQL upgrade'],
|
||||
['FY-028', 'Billing FY backfill'],
|
||||
['FY-029', 'Public header spoof ignored'],
|
||||
['FY-030', 'Trusted header requires secret'],
|
||||
];
|
||||
|
||||
test.describe('UAT_FY_Lock_Backup (UNTESTED SCAFFOLDING - all fixme)', () => {
|
||||
for (const [id, scenario] of FY_CASES) {
|
||||
// test.fixme keeps these visibly pending and NEVER green until implemented.
|
||||
test.fixme(`${id} :: ${scenario}`, async ({ page }) => {
|
||||
// TODO(human-verify): implement against real routes/selectors for this build.
|
||||
// Reference starting points (verify before trusting):
|
||||
// list: GET ${FY_BASE}
|
||||
// create: GET/POST ${FY_BASE}/new
|
||||
// lock: POST ${FY_BASE}/{fy_id}/lock
|
||||
// unlock: POST ${FY_BASE}/{fy_id}/unlock
|
||||
// backup: POST ${FY_BASE}/{fy_id}/backup/export
|
||||
// download: GET ${FY_BASE}/backups/{export_id}/download
|
||||
await login(page, 'System Admin');
|
||||
await page.goto(FY_BASE);
|
||||
// assertion intentionally omitted -- must be written per case before enabling.
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
const { test, expect, request } = require('@playwright/test');
|
||||
require('dotenv').config();
|
||||
const { login, fillFirst, clickFirst } = require('../fixtures/auth');
|
||||
const { expectBlockedOrSafe, expectCookieFlags, expectSecurityHeaders } = require('../fixtures/assertions');
|
||||
const {
|
||||
readBody,
|
||||
expectNoBackendError,
|
||||
extractCsrfFromPage,
|
||||
blockedOrNotFound,
|
||||
switchFinancialYearIfPossible,
|
||||
uploadPath,
|
||||
} = require('../fixtures/v204-helpers');
|
||||
|
||||
const activeFY = process.env.ACTIVE_FY || process.env.DEFAULT_YEAR_CODE || '2025-26';
|
||||
const previousFY = process.env.PREVIOUS_FY || '2024-25';
|
||||
const tenantBName = process.env.TENANT_B_NAME || 'UAT Tenant B';
|
||||
|
||||
test.describe('v2.0.4 additional security / FY / context checks', () => {
|
||||
test('V204-SEC-001 forgot-password API must not expose reset token', async () => {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const email = process.env.FIRM_ADMIN_EMAIL || process.env.SYSTEM_ADMIN_EMAIL || 'admin@auditfirm.local';
|
||||
const candidates = [
|
||||
{ url: '/auth/forgot-password', opts: { data: { email } } },
|
||||
{ url: '/auth/forgot-password', opts: { form: { email } } },
|
||||
{ url: '/forgot-password', opts: { form: { email } } },
|
||||
];
|
||||
let checked = false;
|
||||
for (const c of candidates) {
|
||||
const resp = await api.post(c.url, c.opts).catch(() => null);
|
||||
if (!resp) continue;
|
||||
if ([404,405].includes(resp.status())) continue;
|
||||
checked = true;
|
||||
expect(resp.status()).toBeLessThan(500);
|
||||
const text = await resp.text();
|
||||
expect(text).not.toMatch(/reset_token|password_reset_token|invite_token/i);
|
||||
expect(text).not.toMatch(/[A-Za-z0-9_\-]{32,}\.[A-Za-z0-9_\-]{16,}/);
|
||||
}
|
||||
expect(checked).toBeTruthy();
|
||||
});
|
||||
|
||||
test('V204-SEC-002 reset-password with random token is rejected safely', async () => {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const resp = await api.post('/auth/reset-password', {
|
||||
data: { token: 'invalid-token-for-vapt', password: 'NewPassword@123' },
|
||||
}).catch(() => null);
|
||||
if (!resp || [404,405].includes(resp.status())) test.skip(true, 'Reset password API route not available in this environment');
|
||||
expect(resp.status()).toBeLessThan(500);
|
||||
expect([400,401,403,404,405,409,422,429].includes(resp.status())).toBeTruthy();
|
||||
});
|
||||
|
||||
test('V204-SEC-003 API token/login brute force attempts do not create 500 errors', async () => {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const email = process.env.SYSTEM_ADMIN_EMAIL || 'admin@auditfirm.local';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const resp = await api.post('/auth/token', {
|
||||
form: { username: email, email, password: `wrong-password-${i}` },
|
||||
}).catch(() => null);
|
||||
if (!resp || [404,405].includes(resp.status())) test.skip(true, 'API token route not available in this environment');
|
||||
expect(resp.status()).toBeLessThan(500);
|
||||
expect([400,401,403,422,429].includes(resp.status())).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-CTX-001 public tenant/branch/FY headers are ignored without secret', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
baseURL: process.env.BASE_URL,
|
||||
extraHTTPHeaders: {
|
||||
'X-Tenant-Code': process.env.TENANT_B_CODE || 'UAT-B',
|
||||
'X-Branch-Code': process.env.BRANCH_B_CODE || 'UAT-BB',
|
||||
'X-Year-Code': previousFY,
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await login(page, 'Staff');
|
||||
await page.goto('/system-settings');
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
const body = await readBody(page);
|
||||
expect(body).not.toMatch(new RegExp(tenantBName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'));
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('V204-CTX-002 wrong context secret must not enable spoofed headers', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
baseURL: process.env.BASE_URL,
|
||||
extraHTTPHeaders: {
|
||||
'X-Tenant-Code': process.env.TENANT_B_CODE || 'UAT-B',
|
||||
'X-Branch-Code': process.env.BRANCH_B_CODE || 'UAT-BB',
|
||||
'X-Year-Code': previousFY,
|
||||
'X-AuditFirm-Context-Secret': 'wrong-secret-for-vapt',
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await login(page, 'Staff');
|
||||
await page.goto('/billing');
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
const body = await readBody(page);
|
||||
expect(body).not.toMatch(new RegExp(tenantBName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'));
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('V204-CTX-003 trusted context headers require explicit secret when enabled', async ({ browser }) => {
|
||||
if ((process.env.TRUST_CONTEXT_HEADERS || '').toLowerCase() !== 'true') {
|
||||
test.skip(true, 'Trusted context header mode is disabled, as expected for public UAT/prod');
|
||||
}
|
||||
if (!process.env.CONTEXT_HEADER_SECRET) test.fail(true, 'TRUST_CONTEXT_HEADERS=true but CONTEXT_HEADER_SECRET is empty');
|
||||
const context = await browser.newContext({
|
||||
baseURL: process.env.BASE_URL,
|
||||
extraHTTPHeaders: {
|
||||
'X-Tenant-Code': process.env.TENANT_A_CODE || 'UAT-A',
|
||||
'X-Branch-Code': process.env.BRANCH_A_CODE || 'UAT-BA',
|
||||
'X-Year-Code': activeFY,
|
||||
'X-AuditFirm-Context-Secret': process.env.CONTEXT_HEADER_SECRET,
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await login(page, 'System Admin');
|
||||
await page.goto('/system-settings');
|
||||
await expectNoBackendError(page);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('V204-FY-001 FY selector/session context is visible after login', async ({ page }) => {
|
||||
await login(page, 'System Admin');
|
||||
await page.goto('/system-settings');
|
||||
await expectNoBackendError(page);
|
||||
const body = await readBody(page);
|
||||
const selectorExists = await page.locator('select[name="active_financial_year"], select[name="financial_year"], [data-testid="active-fy-select"], text=/FY|Financial Year|Active FY/i').count();
|
||||
expect(selectorExists || /financial year|active fy|fy\s*[:\-]/i.test(body)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('V204-FY-002 switching FY does not crash core transactional pages', async ({ page }) => {
|
||||
await login(page, 'System Admin');
|
||||
await page.goto('/system-settings');
|
||||
await switchFinancialYearIfPossible(page, activeFY);
|
||||
for (const route of ['/services/engagements', '/documents', '/notice-cases', '/billing', '/billing/payments']) {
|
||||
const resp = await page.goto(route).catch(() => null);
|
||||
if (resp && [404,405].includes(resp.status())) continue;
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-LOCK-001 locked FY write attempt should be blocked or safely rejected', async ({ page }) => {
|
||||
if (!process.env.LOCKED_FY) test.skip(true, 'Set LOCKED_FY in .env after locking a financial year');
|
||||
await login(page, 'System Admin');
|
||||
await page.goto('/system-settings');
|
||||
await switchFinancialYearIfPossible(page, process.env.LOCKED_FY);
|
||||
await page.goto('/notice-cases/new');
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
const csrf = await extractCsrfFromPage(page);
|
||||
if (!csrf) test.skip(true, 'Notice create form/CSRF not available');
|
||||
await fillFirst(page, ['input[name="title"]', 'textarea[name="title"]'], 'Locked FY write negative');
|
||||
await fillFirst(page, ['input[name="reference_no"]'], `LOCKED-FY-${Date.now()}`);
|
||||
await clickFirst(page, ['button[type="submit"]', 'input[type="submit"]']);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
const body = await readBody(page);
|
||||
expect(/locked|closed|not allowed|cannot|permission|forbidden|access denied/i.test(body) || !page.url().match(/notice-cases\/\d+$/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('V204-BACKUP-001 backup screens/files are not accessible anonymously', async ({ page }) => {
|
||||
const probes = [
|
||||
'/system-settings/financial-years/1/backup',
|
||||
'/system-settings/financial-years/backup',
|
||||
'/system-settings/year-backups/1/download',
|
||||
'/data/year_backups/test.zip',
|
||||
'/year_backups/test.zip',
|
||||
'/static/year_backups/test.zip',
|
||||
];
|
||||
for (const p of probes) {
|
||||
const resp = await page.goto(p).catch(() => null);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
const body = await readBody(page);
|
||||
await blockedOrNotFound(resp, body);
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-BACKUP-002 low privilege user cannot access backup export', async ({ page }) => {
|
||||
await login(page, 'Staff');
|
||||
for (const p of ['/system-settings/financial-years/1/backup', '/system-settings/year-backups/1/download']) {
|
||||
const resp = await page.goto(p).catch(() => null);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
const body = await readBody(page);
|
||||
await blockedOrNotFound(resp, body);
|
||||
await expectNoBackendError(page);
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-UPLOAD-001 executable upload to notice/case documents is blocked', async ({ page }) => {
|
||||
if (!process.env.NOTICE_CASE_A_ID) test.skip(true, 'Set NOTICE_CASE_A_ID for upload security test');
|
||||
await login(page, 'System Admin');
|
||||
await page.goto(`/notice-cases/${process.env.NOTICE_CASE_A_ID}`);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
if (!(await fileInput.count())) test.skip(true, 'No file input found on notice case detail page');
|
||||
await fileInput.setInputFiles(uploadPath('not-a-pdf.exe'));
|
||||
await fillFirst(page, ['input[name="title"]'], 'Executable upload negative');
|
||||
await clickFirst(page, ['form:has(input[type="file"]) button[type="submit"]', 'button[type="submit"]']);
|
||||
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('V204-UPLOAD-002 oversized upload is blocked or rejected safely', async ({ page }) => {
|
||||
if (!process.env.NOTICE_CASE_A_ID) test.skip(true, 'Set NOTICE_CASE_A_ID for upload size test');
|
||||
await login(page, 'System Admin');
|
||||
await page.goto(`/notice-cases/${process.env.NOTICE_CASE_A_ID}`);
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
if (!(await fileInput.count())) test.skip(true, 'No file input found on notice case detail page');
|
||||
await fileInput.setInputFiles(uploadPath('large-file.bin'));
|
||||
await fillFirst(page, ['input[name="title"]'], 'Oversized upload negative');
|
||||
await clickFirst(page, ['form:has(input[type="file"]) button[type="submit"]', 'button[type="submit"]']);
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||
await expectNoBackendError(page);
|
||||
const body = await readBody(page);
|
||||
expect(/too large|size|not allowed|invalid|blocked|upload failed|forbidden/i.test(body)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('V204-STORAGE-001 storage agent endpoints require node authentication', async () => {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const endpoints = [
|
||||
'/documents/storage-agent/jobs/pending',
|
||||
'/documents/storage-agent/download-requests/pending',
|
||||
];
|
||||
for (const url of endpoints) {
|
||||
const resp = await api.get(url);
|
||||
expect(resp.status()).toBeLessThan(500);
|
||||
expect([400,401,403,404,405,422].includes(resp.status())).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-HEADERS-001 security headers and cookie flags remain valid', async ({ page, context }) => {
|
||||
const resp = await page.goto('/login');
|
||||
await expectSecurityHeaders(resp);
|
||||
await login(page, 'System Admin');
|
||||
await expectCookieFlags(context);
|
||||
if ((process.env.EXPECT_SECURE_COOKIES || '').toLowerCase() === 'true') {
|
||||
const cookies = await context.cookies();
|
||||
const session = cookies.find(c => /session|auth|token|sid/i.test(c.name));
|
||||
if (session) expect(session.secure).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('V204-CSP-001 strict CSP mode should not use unsafe-inline when enforced', async ({ page }) => {
|
||||
if ((process.env.EXPECT_STRICT_CSP || '').toLowerCase() !== 'true') {
|
||||
test.skip(true, 'Set EXPECT_STRICT_CSP=true after inline JS/CSS has been removed');
|
||||
}
|
||||
const resp = await page.goto('/login');
|
||||
const csp = resp.headers()['content-security-policy'] || '';
|
||||
expect(csp).toBeTruthy();
|
||||
expect(csp).not.toMatch(/unsafe-inline/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
const { test, expect, request } = require('@playwright/test');
|
||||
require('dotenv').config();
|
||||
const { login, fillFirst, clickFirst } = require('../fixtures/auth');
|
||||
const { expectBlockedOrSafe, expectNoServerError, expectSecurityHeaders } = require('../fixtures/assertions');
|
||||
|
||||
test('VAPT-TARGET-001 security headers on login', async ({ page }) => {
|
||||
const resp = await page.goto('/login');
|
||||
await expectSecurityHeaders(resp);
|
||||
});
|
||||
|
||||
test('VAPT-TARGET-002 unauthorized protected URL redirects or blocks', async ({ page }) => {
|
||||
const resp = await page.goto('/notice-cases');
|
||||
await expectBlockedOrSafe(page, resp);
|
||||
});
|
||||
|
||||
test('VAPT-TARGET-003 CSRF negative request should not create case', async () => {
|
||||
const api = await request.newContext({ baseURL: process.env.BASE_URL });
|
||||
const resp = await api.post('/notice-cases/new', { form: { title: 'csrf-negative', reference_no: 'CSRF-NEG' } });
|
||||
expect([400,401,403,404,405,422,303].includes(resp.status())).toBeTruthy();
|
||||
});
|
||||
|
||||
test('VAPT-TARGET-004 basic XSS payload should not execute in notice case list', async ({ page }) => {
|
||||
await login(page, 'System Admin');
|
||||
await page.goto('/notice-cases');
|
||||
await expectNoServerError(page);
|
||||
const body = await page.locator('body').innerText().catch(()=>'');
|
||||
expect(body).not.toContain('<script>alert(1)</script>');
|
||||
});
|
||||
|
||||
test('VAPT-TARGET-005 OTP brute force does not crash and should throttle/block', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await fillFirst(page, ['input[name="email"]','input[name="login_id"]','input[type="email"]'], process.env.SYSTEM_ADMIN_EMAIL || 'admin@auditfirm.local');
|
||||
await fillFirst(page, ['input[name="password"]','input[type="password"]'], process.env.SYSTEM_ADMIN_PASSWORD || 'admin123');
|
||||
await clickFirst(page, ['button[type="submit"]','input[type="submit"]']);
|
||||
await page.waitForLoadState('domcontentloaded').catch(()=>{});
|
||||
if (!page.url().includes('/otp')) test.skip(true, 'OTP page not enabled in this environment');
|
||||
for (let i=0;i<8;i++) {
|
||||
await fillFirst(page, ['input[name="otp"]','input[name="code"]','input[type="text"]'], '000000');
|
||||
await clickFirst(page, ['button[type="submit"]','input[type="submit"]']);
|
||||
await page.waitForLoadState('domcontentloaded').catch(()=>{});
|
||||
}
|
||||
await expectNoServerError(page);
|
||||
});
|
||||
Reference in New Issue
Block a user