Upgrade Playwright suite to v2.5.1 SQLite deep testing
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user