diff --git a/fixtures/url.js b/fixtures/url.js new file mode 100644 index 0000000..4ed075c --- /dev/null +++ b/fixtures/url.js @@ -0,0 +1,26 @@ +require('dotenv').config(); + +const DEFAULT_BASE_URL = 'https://erp-test.vavalam.com'; + +function normalizeBaseURL(value) { + const raw = (value || '').trim() || DEFAULT_BASE_URL; + return raw.replace(/\/+$/, ''); +} + +const BASE_URL = normalizeBaseURL( + process.env.BASE_URL || + process.env.APP_BASE_URL || + process.env.PLAYWRIGHT_BASE_URL || + DEFAULT_BASE_URL +); + +function absoluteUrl(route = '/') { + if (/^https?:\/\//i.test(route)) return route; + return new URL(route, `${BASE_URL}/`).toString(); +} + +module.exports = { + BASE_URL, + DEFAULT_BASE_URL, + absoluteUrl, +}; diff --git a/playwright.config.js b/playwright.config.js index f1ca42c..5343bb7 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,5 +1,6 @@ const { defineConfig } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL } = require('./fixtures/url'); module.exports = defineConfig({ testDir: './tests', @@ -15,7 +16,7 @@ module.exports = defineConfig({ ['list'] ], use: { - baseURL: process.env.BASE_URL || 'http://127.0.0.1:8000', + baseURL: BASE_URL, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure' diff --git a/run-api-checks.js b/run-api-checks.js index 0375b64..fb92f42 100644 --- a/run-api-checks.js +++ b/run-api-checks.js @@ -1,8 +1,8 @@ // Standalone runner: executes the API-level (no-browser) security checks // from the v2.0.4 suite against a live ERP. Produces JSON results. const { request } = require('@playwright/test'); +const { BASE_URL, absoluteUrl } = require('./fixtures/url'); -const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:8000'; const results = []; function record(id, title, status, detail) { diff --git a/tests/api-auth-rbac.spec.js b/tests/api-auth-rbac.spec.js index 85c022e..bcc12e9 100644 --- a/tests/api-auth-rbac.spec.js +++ b/tests/api-auth-rbac.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/audit-logs.spec.js b/tests/audit-logs.spec.js index 5697f94..6f5d009 100644 --- a/tests/audit-logs.spec.js +++ b/tests/audit-logs.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/billing-business-rules.spec.js b/tests/billing-business-rules.spec.js index e3d5b1e..1e6d251 100644 --- a/tests/billing-business-rules.spec.js +++ b/tests/billing-business-rules.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/consultants-documents.spec.js b/tests/consultants-documents.spec.js index c9b6743..9a253b1 100644 --- a/tests/consultants-documents.spec.js +++ b/tests/consultants-documents.spec.js @@ -25,6 +25,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound, uploadPath } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -92,7 +93,7 @@ test.describe('CONS: Consultant portal', () => { 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`, + `${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'); @@ -115,7 +116,7 @@ test.describe('CONS: Consultant portal', () => { 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`, + `${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'); @@ -182,7 +183,7 @@ test.describe('DOC-STOR: Document storage and permanent vault', () => { 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`, + `${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'); @@ -216,14 +217,14 @@ test.describe('DOC-STOR: Document storage and permanent vault', () => { }); 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); + const resp = await request.get(`${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); + const resp = await request.get(`${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(); @@ -232,7 +233,7 @@ test.describe('DOC-STOR: Document storage and permanent vault', () => { 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`, + `${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'); @@ -256,7 +257,7 @@ test.describe('DOC-STOR: Document storage and permanent vault', () => { 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`, + `${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'); @@ -271,7 +272,7 @@ test.describe('DOC-STOR: Document storage and permanent vault', () => { }); 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); + const resp = await request.get(`${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 index cb08631..79e5c1f 100644 --- a/tests/document-security-deep.spec.js +++ b/tests/document-security-deep.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/email-integration.spec.js b/tests/email-integration.spec.js index 1c8720d..8ca71c2 100644 --- a/tests/email-integration.spec.js +++ b/tests/email-integration.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/employee-hr-business-rules.spec.js b/tests/employee-hr-business-rules.spec.js index cc642f9..797d3e4 100644 --- a/tests/employee-hr-business-rules.spec.js +++ b/tests/employee-hr-business-rules.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/employees.spec.js b/tests/employees.spec.js index 81a6889..ad4d36e 100644 --- a/tests/employees.spec.js +++ b/tests/employees.spec.js @@ -28,6 +28,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -132,7 +133,7 @@ test.describe('EMP-HR: Attendance management', () => { }); 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`, { + const resp = await request.post(`${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'); @@ -142,7 +143,7 @@ test.describe('EMP-HR: Attendance management', () => { 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`, + `${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'); @@ -184,7 +185,7 @@ test.describe('EMP-HR: Leave management', () => { 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`, + `${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'); @@ -270,7 +271,7 @@ test.describe('EMP-HR: Employee documents', () => { 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`, + `${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'); @@ -313,7 +314,7 @@ test.describe('EMP-HR: Payroll', () => { 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`, + `${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'); @@ -323,7 +324,7 @@ test.describe('EMP-HR: Payroll', () => { 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`, + `${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'); @@ -421,7 +422,7 @@ test.describe('EMP-ESS: Employee self-service portal', () => { }); 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`, { + const resp = await request.post(`${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'); @@ -498,7 +499,7 @@ test.describe('EMP-SEC: Security probes', () => { 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`, { + const resp = await page.request.post(`${BASE_URL}/employees/imports/commit`, { form: { csrf_token: 'invalid' }, }).catch(() => null); if (!resp) test.skip(true, 'Request failed at network level'); @@ -509,7 +510,7 @@ test.describe('EMP-SEC: Security probes', () => { 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`, + `${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'); diff --git a/tests/full-excel-variants.spec.js b/tests/full-excel-variants.spec.js index baf8ce5..68aa272 100644 --- a/tests/full-excel-variants.spec.js +++ b/tests/full-excel-variants.spec.js @@ -1,5 +1,6 @@ const { test, expect, request } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const matrix = require('../data/generated-test-matrix.json'); const { login, logout, fillFirst, clickFirst } = require('../fixtures/auth'); const { expectNoServerError, expectBlockedOrSafe, expectSecurityHeaders, expectCookieFlags } = require('../fixtures/assertions'); @@ -58,7 +59,7 @@ async function runSecuritySpecific(page, context, v) { return; } if (/SEC-007|GEN-011/i.test(id)) { - const api = await request.newContext({ baseURL: process.env.BASE_URL }); + const api = await request.newContext({ baseURL: 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; diff --git a/tests/marketplace-leads.spec.js b/tests/marketplace-leads.spec.js index 5f6aad4..1ac96ef 100644 --- a/tests/marketplace-leads.spec.js +++ b/tests/marketplace-leads.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/notice-case-business-rules.spec.js b/tests/notice-case-business-rules.spec.js index 3022004..065ada1 100644 --- a/tests/notice-case-business-rules.spec.js +++ b/tests/notice-case-business-rules.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/noticecases-services-work.spec.js b/tests/noticecases-services-work.spec.js index d04656b..e2b5775 100644 --- a/tests/noticecases-services-work.spec.js +++ b/tests/noticecases-services-work.spec.js @@ -22,6 +22,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -98,7 +99,7 @@ test.describe('CASE: Notice case sub-pages', () => { 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`, + `${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'); @@ -108,7 +109,7 @@ test.describe('CASE: Notice case sub-pages', () => { 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`, + `${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'); @@ -179,7 +180,7 @@ test.describe('SVC: Services depth', () => { 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`, + `${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'); @@ -203,7 +204,7 @@ test.describe('SVC: Services depth', () => { 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`, + `${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'); @@ -212,7 +213,7 @@ test.describe('SVC: Services depth', () => { 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`, + `${BASE_URL}/services/bulk-lock`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); @@ -222,7 +223,7 @@ test.describe('SVC: Services depth', () => { 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`, + `${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'); @@ -245,7 +246,7 @@ test.describe('SVC: Services depth', () => { 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`, + `${BASE_URL}/services/tasks/bulk-update`, { form: { csrf_token: '' } } ).catch(() => null); if (!resp || [404, 405].includes(resp.status())) test.skip(true, 'Route not available'); @@ -270,7 +271,7 @@ test.describe('WORK: Work detail and task operations', () => { 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`, + `${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'); @@ -280,7 +281,7 @@ test.describe('WORK: Work detail and task operations', () => { 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`, + `${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'); @@ -304,7 +305,7 @@ test.describe('WORK: Work detail and task operations', () => { 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`, + `${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'); @@ -322,7 +323,7 @@ test.describe('WORK: Work detail and task operations', () => { 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`, + `${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'); diff --git a/tests/partners-billing.spec.js b/tests/partners-billing.spec.js index d046b63..3813999 100644 --- a/tests/partners-billing.spec.js +++ b/tests/partners-billing.spec.js @@ -20,6 +20,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -73,7 +74,7 @@ test.describe('PART: Partner portal', () => { 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`, + `${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'); @@ -169,7 +170,7 @@ test.describe('BILL: Invoices and payments', () => { 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`, + `${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'); @@ -179,7 +180,7 @@ test.describe('BILL: Invoices and payments', () => { 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`, + `${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'); diff --git a/tests/platform-billing.spec.js b/tests/platform-billing.spec.js index 591e02a..c090028 100644 --- a/tests/platform-billing.spec.js +++ b/tests/platform-billing.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/system-settings-tenancy.spec.js b/tests/system-settings-tenancy.spec.js index 2866a83..c0f7b2f 100644 --- a/tests/system-settings-tenancy.spec.js +++ b/tests/system-settings-tenancy.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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); diff --git a/tests/v204-security-additions.spec.js b/tests/v204-security-additions.spec.js index 7112104..2181a07 100644 --- a/tests/v204-security-additions.spec.js +++ b/tests/v204-security-additions.spec.js @@ -1,5 +1,6 @@ const { test, expect, request } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login, fillFirst, clickFirst } = require('../fixtures/auth'); const { expectBlockedOrSafe, expectCookieFlags, expectSecurityHeaders } = require('../fixtures/assertions'); const { @@ -17,7 +18,7 @@ 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 api = await request.newContext({ baseURL: 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 } } }, @@ -39,7 +40,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { }); test('V204-SEC-002 reset-password with random token is rejected safely', async () => { - const api = await request.newContext({ baseURL: process.env.BASE_URL }); + const api = await request.newContext({ baseURL: BASE_URL }); const resp = await api.post('/auth/reset-password', { data: { token: 'invalid-token-for-vapt', password: 'NewPassword@123' }, }).catch(() => null); @@ -49,7 +50,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { }); 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 api = await request.newContext({ baseURL: 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', { @@ -63,7 +64,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { 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, + baseURL: BASE_URL, extraHTTPHeaders: { 'X-Tenant-Code': process.env.TENANT_B_CODE || 'UAT-B', 'X-Branch-Code': process.env.BRANCH_B_CODE || 'UAT-BB', @@ -82,7 +83,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { test('V204-CTX-002 wrong context secret must not enable spoofed headers', async ({ browser }) => { const context = await browser.newContext({ - baseURL: process.env.BASE_URL, + baseURL: BASE_URL, extraHTTPHeaders: { 'X-Tenant-Code': process.env.TENANT_B_CODE || 'UAT-B', 'X-Branch-Code': process.env.BRANCH_B_CODE || 'UAT-BB', @@ -106,7 +107,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { } 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, + baseURL: BASE_URL, extraHTTPHeaders: { 'X-Tenant-Code': process.env.TENANT_A_CODE || 'UAT-A', 'X-Branch-Code': process.env.BRANCH_A_CODE || 'UAT-BA', @@ -222,7 +223,7 @@ test.describe('v2.0.4 additional security / FY / context checks', () => { }); test('V204-STORAGE-001 storage agent endpoints require node authentication', async () => { - const api = await request.newContext({ baseURL: process.env.BASE_URL }); + const api = await request.newContext({ baseURL: BASE_URL }); const endpoints = [ '/documents/storage-agent/jobs/pending', '/documents/storage-agent/download-requests/pending', diff --git a/tests/vapt-targeted.spec.js b/tests/vapt-targeted.spec.js index ced95b6..af0e859 100644 --- a/tests/vapt-targeted.spec.js +++ b/tests/vapt-targeted.spec.js @@ -1,5 +1,6 @@ const { test, expect, request } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login, fillFirst, clickFirst } = require('../fixtures/auth'); const { expectBlockedOrSafe, expectNoServerError, expectSecurityHeaders } = require('../fixtures/assertions'); @@ -14,7 +15,7 @@ test('VAPT-TARGET-002 unauthorized protected URL redirects or blocks', async ({ }); test('VAPT-TARGET-003 CSRF negative request should not create case', async () => { - const api = await request.newContext({ baseURL: process.env.BASE_URL }); + const api = await request.newContext({ baseURL: 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(); }); diff --git a/tests/work-lifecycle-e2e.spec.js b/tests/work-lifecycle-e2e.spec.js index 6606bae..9436b92 100644 --- a/tests/work-lifecycle-e2e.spec.js +++ b/tests/work-lifecycle-e2e.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('@playwright/test'); require('dotenv').config(); +const { BASE_URL, absoluteUrl } = require('../fixtures/url'); const { login } = require('../fixtures/auth'); const { expectNoBackendError, readBody, blockedOrNotFound } = require('../fixtures/v204-helpers'); const { expectBlockedOrSafe } = require('../fixtures/assertions'); @@ -31,7 +32,7 @@ async function expectPublicNoSensitive(page) { } async function apiCall(request, method, route) { - const url = `${process.env.BASE_URL}${route}`; + const url = absoluteUrl(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);