Files
CASAN/packages/casan-control-panel/backend/test/approvals.test.ts
T
2026-07-19 09:37:16 +07:00

166 lines
7.1 KiB
TypeScript

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> | 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('operations owner request remains visible and actionable for an independent reviewer', async () => {
await withTempGovernance(async () => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'goal.workspace.execute',
target: 'customer-portal',
risk: 'high',
sensitive: true,
reason: 'apply the reviewed workspace patch',
payload: { goal_id: 'goal-123', project_id: 'customer-portal' },
}, projectAdmin) as { proposal: { id: string; status: string; proposer: string } };
const reviewerInbox = svc.list(approver, 'pending') as {
count: number;
proposals: Array<{ id: string; proposer: string; status: string }>;
};
const visible = reviewerInbox.proposals.find((proposal) => proposal.id === submitted.proposal.id);
assert.equal(reviewerInbox.count, 1);
assert.equal(visible?.proposer, projectAdmin.actor);
assert.equal(visible?.status, 'pending');
const decided = await svc.decide({
id: submitted.proposal.id,
decision: 'approve',
reason: 'independent reviewer verified scope and controls',
}, approver) as { proposal: { status: string; approver: string } };
assert.equal(decided.proposal.status, 'approved');
assert.equal(decided.proposal.approver, approver.actor);
});
});
test('project reviewer cannot see or decide another project approval', async () => {
await withTempGovernance(async () => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'goal.workspace.execute',
target: 'project-alpha',
risk: 'high',
sensitive: true,
reason: 'project alpha patch',
payload: { goal_id: 'goal-alpha', project_id: 'project-alpha' },
}, { ...projectAdmin, project: 'project-alpha' }) as { proposal: { id: string } };
const otherProjectReviewer = { ...approver, project: 'project-beta' };
const reviewerInbox = svc.list(otherProjectReviewer, 'pending') as { count: number; proposals: Array<{ id: string }> };
assert.equal(reviewerInbox.count, 0);
assert.equal(reviewerInbox.proposals.length, 0);
await assert.rejects(
svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'wrong project' }, otherProjectReviewer),
ForbiddenException,
);
});
});
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,
);
});
});