// apps/hq/test/authz-gates.test.ts — D24 (red-team): destructive/sensitive ops gated managerial. import express from 'express' import { describe, it, expect, afterAll } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createStaff } from '../src/auth' import { createClient } from '../src/repos-clients' import { createModule, setPrice } from '../src/repos-modules' import { createDraft, issueDocument } from '../src/repos-documents' import { createInteraction } from '../src/repos-interactions' import { apiRouter } from '../src/api' const servers: { close: () => void }[] = [] afterAll(() => { for (const s of servers) s.close() }) async function ctx() { const db = openDb(':memory:'); await seedIfEmpty(db) await createStaff(db, { email: 'owner@t.in', displayName: 'O', role: 'owner', password: 'owner-pass-1' }) await createStaff(db, { email: 'staff@t.in', displayName: 'S', role: 'staff', password: 'staff-pass-1' }) const staffB = await createStaff(db, { email: 'staff2@t.in', displayName: 'S2', role: 'staff', password: 'staff-pass-2' }) const c = await createClient(db, 'u1', { name: 'Bank', stateCode: '32' }) const m = await createModule(db, 'u1', { code: 'CORE', name: 'Core' }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' }) const inv = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }], })).id) const app = express(); app.use(express.json()); app.locals['db'] = db app.set('trust proxy', 1) app.use('/api', apiRouter(db, { keyHex: '11'.repeat(32) })) const server = app.listen(0); servers.push(server) const base = `http://localhost:${(server.address() as { port: number }).port}/api` const tok = async (e: string, p: string) => ((await (await fetch(`${base}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: e, password: p }), })).json()) as { token: string }).token return { db, c, m, inv, staffB, base, tok } } const H = (t: string) => ({ 'content-type': 'application/json', authorization: `Bearer ${t}` }) describe('D24 authorization gates', () => { it('cancel / credit-note / public share are managerial-only; owner can', async () => { const x = await ctx() const staff = await x.tok('staff@t.in', 'staff-pass-1') const owner = await x.tok('owner@t.in', 'owner-pass-1') for (const path of [`/documents/${x.inv.id}/cancel`, `/documents/${x.inv.id}/credit-note`, `/documents/${x.inv.id}/share`]) { const staffRes = await fetch(`${x.base}${path}`, { method: 'POST', headers: H(staff), body: '{}' }) expect(staffRes.status, `${path} staff`).toBe(403) } // Owner can mint a share (the one that has no other preconditions). const share = await fetch(`${x.base}/documents/${x.inv.id}/share`, { method: 'POST', headers: H(owner), body: '{}' }) expect(share.status).toBe(200) }) it('a staff user cannot edit another employee’s interaction; the author can', async () => { const x = await ctx() // Author = staffB; log it directly with staffB's id. const it2 = await createInteraction(x.db, x.staffB.id, { clientId: x.c.id, typeCode: 'call', onDate: '2026-07-10' }) const other = await x.tok('staff@t.in', 'staff-pass-1') // different staff const author = await x.tok('staff2@t.in', 'staff-pass-2') // the author const deny = await fetch(`${x.base}/interactions/${it2.id}`, { method: 'PATCH', headers: H(other), body: JSON.stringify({ notes: 'tampered' }) }) expect(deny.status).toBe(403) const ok = await fetch(`${x.base}/interactions/${it2.id}`, { method: 'PATCH', headers: H(author), body: JSON.stringify({ notes: 'my note' }) }) expect(ok.status).toBe(200) }) })