521 lines
21 KiB
JavaScript
521 lines
21 KiB
JavaScript
/**
|
|
* =============================================================================
|
|
* 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 { BASE_URL, absoluteUrl } = require('../fixtures/url');
|
|
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(`${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(
|
|
`${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(
|
|
`${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/documents?employee_id=${idOr('EMPLOYEE_A_ID')}`);
|
|
await expectNoBackendError(page);
|
|
const fileInput = page.locator('input[type="file"][name="document_file"], input[type="file"]').first();
|
|
expect(await fileInput.count(), 'Employee document upload input should be present on /employees/documents').toBeGreaterThan(0);
|
|
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(
|
|
`${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(
|
|
`${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(
|
|
`${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(`${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(`${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(
|
|
`${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();
|
|
});
|
|
|
|
});
|