import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ForbiddenException } from '@nestjs/common'; import { ApprovalsService } from '../src/approvals/approvals.service.js'; const projectAdmin = { actor: 'alice', role: 'project-admin', project: 'default', tenant: 'default' }; const approver = { actor: 'bob', role: 'approver', project: 'default', tenant: 'default' }; const orgAdmin = { actor: 'root', role: 'org-admin', project: 'default', tenant: 'default' }; async function withTempGovernance(fn: (paths: { inbox: string; store: string }) => Promise | void) { const prevInbox = process.env.CASAN_APPROVAL_INBOX_FILE; const prevStrict = process.env.CASAN_APPROVAL_STRICT; const prevStore = process.env.CASAN_CP_STORE_FILE; const prevKeyDir = process.env.CASAN_CP_KEY_DIR; const prevPub = process.env.CASAN_CP_PUB; const prevTenantRoot = process.env.CASAN_TENANT_STATE_ROOT; const work = mkdtempSync(join(tmpdir(), 'cp-approval-')); process.env.CASAN_TENANT_STATE_ROOT = work; process.env.CASAN_APPROVAL_INBOX_FILE = join(work, 'default', 'approvals', 'approval-inbox.json'); process.env.CASAN_CP_STORE_FILE = join(work, 'settings.json'); process.env.CASAN_CP_KEY_DIR = join(work, 'keys'); process.env.CASAN_CP_PUB = join(work, 'cp.pub'); try { await fn({ inbox: process.env.CASAN_APPROVAL_INBOX_FILE, store: process.env.CASAN_CP_STORE_FILE }); } finally { if (prevInbox === undefined) delete process.env.CASAN_APPROVAL_INBOX_FILE; else process.env.CASAN_APPROVAL_INBOX_FILE = prevInbox; if (prevStrict === undefined) delete process.env.CASAN_APPROVAL_STRICT; else process.env.CASAN_APPROVAL_STRICT = prevStrict; if (prevStore === undefined) delete process.env.CASAN_CP_STORE_FILE; else process.env.CASAN_CP_STORE_FILE = prevStore; if (prevKeyDir === undefined) delete process.env.CASAN_CP_KEY_DIR; else process.env.CASAN_CP_KEY_DIR = prevKeyDir; if (prevPub === undefined) delete process.env.CASAN_CP_PUB; else process.env.CASAN_CP_PUB = prevPub; if (prevTenantRoot === undefined) delete process.env.CASAN_TENANT_STATE_ROOT; else process.env.CASAN_TENANT_STATE_ROOT = prevTenantRoot; } } test('approval inbox submit -> approve applies governed setting and writes oversight', async () => { await withTempGovernance(async ({ inbox, store }) => { const svc = new ApprovalsService(); const submitted = svc.submit({ action: 'settings.write', target: 'security.strict', risk: 'high', sensitive: true, reason: 'tighten strict mode', payload: { key: 'security.strict', value: true }, }, projectAdmin) as any; assert.equal(submitted.proposal.status, 'pending'); assert.equal(submitted.proposal.delegation.requires_approval, true); const decided = await svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'approved by reviewer' }, approver) as any; assert.equal(decided.proposal.status, 'approved'); assert.equal(decided.applied.value, true); const inboxRaw = JSON.parse(readFileSync(inbox, 'utf8')); assert.equal(inboxRaw.oversight.length, 2); assert.equal(inboxRaw.oversight[1].event, 'proposal_approved'); const storeRaw = JSON.parse(readFileSync(store, 'utf8')); assert.equal(storeRaw.settings['security.strict'].value, true); assert.equal(storeRaw.audit.length, 1); }); }); test('approval inbox denies forged JWT in strict mode without deciding proposal', async () => { await withTempGovernance(async ({ inbox }) => { const svc = new ApprovalsService(); const submitted = svc.submit({ action: 'settings.write', target: 'security.strict', risk: 'high', sensitive: true, reason: 'strict approval required', payload: { key: 'security.strict', value: true }, }, projectAdmin) as any; process.env.CASAN_APPROVAL_STRICT = '1'; await assert.rejects( svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'forged jwt', approvalJwt: 'fake.jwt.token' }, approver), ForbiddenException, ); const inboxRaw = JSON.parse(readFileSync(inbox, 'utf8')); assert.equal(inboxRaw.proposals[0].status, 'pending'); assert.equal(inboxRaw.oversight.length, 1); }); }); test('approval inbox enforces separation of duties', async () => { await withTempGovernance(async () => { const svc = new ApprovalsService(); const submitted = svc.submit({ action: 'settings.write', target: 'compression.enabled', risk: 'standard', reason: 'change by root', payload: { key: 'compression.enabled', value: true }, }, orgAdmin) as any; await assert.rejects( svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'self approve' }, orgAdmin), ForbiddenException, ); }); });