feat: add control panel
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import { SettingsService } from '../src/settings/settings.service.js';
|
||||
|
||||
const viewer = { actor: 'viewer-1', role: 'viewer', project: 'default', tenant: 'default' };
|
||||
const admin = { actor: 'admin-1', role: 'org-admin', project: 'default', tenant: 'default' };
|
||||
|
||||
function withTempStore(fn: (storeFile: string) => void) {
|
||||
const prev = process.env.CASAN_CP_STORE_FILE;
|
||||
const storeFile = join(mkdtempSync(join(tmpdir(), 'cp-settings-')), 'settings.json');
|
||||
process.env.CASAN_CP_STORE_FILE = storeFile;
|
||||
try {
|
||||
fn(storeFile);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.CASAN_CP_STORE_FILE;
|
||||
else process.env.CASAN_CP_STORE_FILE = prev;
|
||||
}
|
||||
}
|
||||
|
||||
test('settings list is readable by viewer and exposes policy/capabilities', () => {
|
||||
withTempStore(() => {
|
||||
const svc = new SettingsService();
|
||||
const res = svc.list(viewer) as any;
|
||||
assert.ok(res.policy['compression.enabled']);
|
||||
assert.equal(res.capabilities.can_write_standard, false);
|
||||
assert.equal(res.capabilities.can_write_sensitive, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('viewer cannot write settings', () => {
|
||||
withTempStore(() => {
|
||||
const svc = new SettingsService();
|
||||
assert.throws(
|
||||
() => svc.set({ key: 'compression.enabled', value: true, reason: 'test viewer deny' }, viewer),
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('org-admin writes and rolls back through governed store with audit', () => {
|
||||
withTempStore((storeFile) => {
|
||||
const svc = new SettingsService();
|
||||
const first = svc.set({ key: 'compression.enabled', value: true, reason: 'enable compression' }, admin) as any;
|
||||
const second = svc.set({ key: 'compression.enabled', value: false, reason: 'disable compression' }, admin) as any;
|
||||
const rolled = svc.rollback({ key: 'compression.enabled', reason: 'rollback compression' }, admin) as any;
|
||||
|
||||
assert.equal(first.setting.version, 1);
|
||||
assert.equal(second.setting.version, 2);
|
||||
assert.equal(rolled.setting.version, 3);
|
||||
assert.equal(rolled.setting.value, true);
|
||||
assert.equal(rolled.audit_verify.ok, true);
|
||||
assert.ok(existsSync(storeFile));
|
||||
|
||||
const raw = JSON.parse(readFileSync(storeFile, 'utf8'));
|
||||
assert.equal(raw.audit.length, 3);
|
||||
assert.equal(raw.audit[0].action, 'set');
|
||||
assert.equal(raw.audit[2].action, 'rollback');
|
||||
});
|
||||
});
|
||||
|
||||
test('sensitive setting requires approval even for org-admin in dev gate', () => {
|
||||
withTempStore(() => {
|
||||
const svc = new SettingsService();
|
||||
assert.throws(
|
||||
() => svc.set({ key: 'security.strict', value: false, reason: 'loosen strict mode' }, admin),
|
||||
ForbiddenException,
|
||||
);
|
||||
const res = svc.set({
|
||||
key: 'security.strict',
|
||||
value: true,
|
||||
reason: 'tighten strict mode with approval token',
|
||||
approval: 'approved-in-dev',
|
||||
}, admin) as any;
|
||||
assert.equal(res.setting.value, true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user