import { expect, type Page, test } from '@playwright/test'; /** * Policy documents & acknowledgments e2e (Workstream 11). Proves the RBAC and * persistence contract: director/office_manager manage documents; the four * campus staff roles read + acknowledge; external roles are locked out; * acknowledgment is per-version and idempotent. * * Requires the backend running with the database migrated + seeded. */ const USER_PASSWORD = 'flatlogicUser123!'; const BACKEND_API_URL = process.env.VITE_BACKEND_API_URL || 'http://localhost:8080/api'; const DOCS = `${BACKEND_API_URL}/policy_documents`; const ACKS = `${BACKEND_API_URL}/policy_acknowledgments`; const DIRECTOR = 'director@flatlogic.com'; const OFFICE_MANAGER = 'office_manager@flatlogic.com'; const TEACHER = 'teacher@flatlogic.com'; const STUDENT = 'student@flatlogic.com'; /** Denials surface as 400 (permission middleware) or 401/403. */ const DENIED = [400, 401, 403]; interface DocRow { readonly id: string; readonly title: string; readonly version: number; } interface AckRow { readonly policyDocumentId: string; readonly version: number; } async function logout(page: Page): Promise { await page.request.post(`${BACKEND_API_URL}/auth/signout`); await page.context().clearCookies(); } async function login(page: Page, email: string): Promise { await logout(page); await page.goto('/login'); await page.getByPlaceholder('you@school.edu').fill(email); await page.getByPlaceholder('Enter your password').fill(USER_PASSWORD); await page.getByRole('button', { name: 'Sign In', exact: true }).click(); await page.waitForURL((url) => !url.pathname.startsWith('/login'), { timeout: 10_000, }); } async function findDoc(page: Page, title: string): Promise { const res = await page.request.get(DOCS); expect(res.status()).toBe(200); const body = (await res.json()) as { rows?: DocRow[] }; return (body.rows ?? []).find((row) => row.title === title); } test.describe('Policy documents & acknowledgments', () => { test('director creates a policy document that persists', async ({ page }) => { await login(page, DIRECTOR); const title = `Safety Protocol ${Date.now()}`; const res = await page.request.post(DOCS, { data: { data: { title, category: 'safety_protocol', body: 'v1' } }, }); expect(res.ok()).toBe(true); const doc = await findDoc(page, title); expect(doc, 'created document is listed').toBeTruthy(); expect(doc!.version).toBe(1); }); test('office_manager may manage; teacher may read but not create', async ({ page, }) => { await login(page, OFFICE_MANAGER); const title = `Handbook Policy ${Date.now()}`; const created = await page.request.post(DOCS, { data: { data: { title, category: 'handbook_policy' } }, }); expect(created.ok()).toBe(true); // Teacher reads the org's documents but cannot create one. await login(page, TEACHER); expect(await findDoc(page, title), 'teacher can read').toBeTruthy(); const teacherCreate = await page.request.post(DOCS, { data: { data: { title: 'Teacher should not create', category: 'handbook_policy' } }, }); expect(DENIED).toContain(teacherCreate.status()); }); test('a campus staff member acknowledges a document (idempotent per version)', async ({ page, }) => { // Director publishes a document. await login(page, DIRECTOR); const title = `Ack Target ${Date.now()}`; await page.request.post(DOCS, { data: { data: { title, category: 'safety_protocol', body: 'v1' } }, }); const doc = await findDoc(page, title); expect(doc).toBeTruthy(); const documentId = doc!.id; // Teacher acknowledges it twice — the second is idempotent. await login(page, TEACHER); expect((await page.request.post(ACKS, { data: { data: { policyDocumentId: documentId } } })).ok()).toBe(true); expect((await page.request.post(ACKS, { data: { data: { policyDocumentId: documentId } } })).ok()).toBe(true); const acksRes = await page.request.get(`${ACKS}?policyDocumentId=${documentId}`); expect(acksRes.status()).toBe(200); const acks = ((await acksRes.json()) as { rows?: AckRow[] }).rows ?? []; const forDoc = acks.filter((a) => a.policyDocumentId === documentId); expect(forDoc).toHaveLength(1); expect(forDoc[0]?.version).toBe(1); }); test('editing a document bumps the version and requires re-acknowledgment', async ({ page, }) => { await login(page, DIRECTOR); const title = `Versioned Doc ${Date.now()}`; await page.request.post(DOCS, { data: { data: { title, category: 'handbook_policy', body: 'v1' } }, }); const v1 = await findDoc(page, title); expect(v1!.version).toBe(1); // Teacher acknowledges v1. await login(page, TEACHER); await page.request.post(ACKS, { data: { data: { policyDocumentId: v1!.id } } }); // Director edits → version bumps to 2. await login(page, DIRECTOR); const put = await page.request.put(`${DOCS}/${v1!.id}`, { data: { id: v1!.id, data: { title, body: 'v2 updated' } }, }); expect(put.ok()).toBe(true); const v2 = await findDoc(page, title); expect(v2!.version).toBe(2); // Teacher re-acknowledges → a second ack row (for v2) now exists. await login(page, TEACHER); await page.request.post(ACKS, { data: { data: { policyDocumentId: v1!.id } } }); const acksRes = await page.request.get(`${ACKS}?policyDocumentId=${v1!.id}`); const acks = ((await acksRes.json()) as { rows?: AckRow[] }).rows ?? []; expect(acks.filter((a) => a.policyDocumentId === v1!.id).length).toBeGreaterThanOrEqual(2); }); test('external roles cannot read or acknowledge policies', async ({ page }) => { await login(page, STUDENT); expect(DENIED).toContain((await page.request.get(DOCS)).status()); const ack = await page.request.post(ACKS, { data: { data: { policyDocumentId: '00000000-0000-4000-8000-000000000000' } }, }); expect(DENIED).toContain(ack.status()); }); });