tests/dashboard.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { createServer, request } from 'node:http';
import test from 'node:test';
import { startDashboard } from '../src/adapters/dashboard/server.js';
import { dashboardSnapshot } from '../src/application/dashboard.js';
import { FileDecisionStore } from '../src/adapters/storage/decision-store.js';
import { FileDecisionGovernanceStore } from '../src/adapters/storage/decision-governance-store.js';
import { serviceFixture } from './service-helpers.js';
async function fixture() {
const { d } = await serviceFixture();
const decisions = new FileDecisionStore(d.config.dataDir), governance = new FileDecisionGovernanceStore(d.config.dataDir);
const snapshot = () => dashboardSnapshot(d.config, '1.8.0', { candidates: d.candidates, goals: d.goals, reports: d.store, decisions, governance, services: d.services });
return { d, decisions, governance, snapshot };
}
test('read-only loopback console requires its own token and rejects cross-origin, mutation and file requests', async () => {
const { snapshot } = await fixture(); let reads = 0;
const dashboard = await startDashboard({ snapshot: async () => { reads++; return snapshot(); } }, 0);
const base = dashboard.url.split('/#')[0]!;
try {
const home = await fetch(base); assert.equal(home.status, 200);
assert.match(home.headers.get('content-security-policy')!, /frame-ancestors 'none'/);
const html = await home.text(); assert.ok(!html.includes(dashboard.token)); assert.ok(!html.includes('innerHTML'));
assert.equal((await fetch(base + '/api/snapshot')).status, 401);
assert.equal((await fetch(base + '/api/snapshot', { headers: { Authorization: 'Bearer ' + dashboard.token, Origin: 'https://evil.invalid' } })).status, 403);
const rebound = await new Promise<number>((resolve, reject) => {
const call = request(base + '/api/snapshot', { headers: { Authorization: 'Bearer ' + dashboard.token, Host: 'evil.invalid' } }, response => {
response.resume(); resolve(response.statusCode!);
}); call.on('error', reject); call.end();
});
assert.equal(rebound, 403);
assert.equal((await fetch(base + '/api/snapshot', { method: 'POST' })).status, 405);
assert.equal((await fetch(base + '/.env')).status, 404);
assert.equal((await fetch(base + '/api/snapshot', { headers: { Authorization: 'Bearer ' + dashboard.token } })).status, 200);
assert.equal(reads, 1);
} finally { await dashboard.close(); }
});
test('console snapshots expose bounded summaries without Issue prose, review notes, logs, source paths or other repositories', async () => {
const { d, snapshot } = await fixture();
await d.candidates.save({ schemaVersion: 1, id: 'a'.repeat(24), repository: d.config.repository, kind: 'issue', evidenceDigest: 'b'.repeat(24),
commit: 'c'.repeat(40), title: '<script>SECRET_TITLE</script>', detail: 'SECRET_ISSUE', priority: 1, priorityReasons: ['SECRET_REASON'],
status: 'observed', firstSeenAt: '', updatedAt: '', reason: 'SECRET_LOCAL_PATH' });
await d.candidates.save({ ...(await d.candidates.list())[0]!, id: 'd'.repeat(24), repository: 'foreign/repo' });
const value = await snapshot(), text = JSON.stringify(value);
assert.equal(value.counts.candidates, 1); assert.equal(value.candidates.length, 1);
assert.ok(!text.includes('SECRET_')); assert.ok(!text.includes('foreign/repo')); assert.ok(!text.includes(d.config.dataDir));
});
test('console errors are sanitized and occupied ports fail without leaking listeners', async () => {
const broken = await startDashboard({ snapshot: async () => { throw new Error('SECRET provider key'); } }, 0);
try {
const response = await fetch(broken.url.split('/#')[0] + '/api/snapshot', { headers: { Authorization: 'Bearer ' + broken.token } });
assert.equal(response.status, 503); assert.ok(!(await response.text()).includes('SECRET'));
} finally { await broken.close(); }
const server = createServer(); await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
const address = server.address(); assert.ok(address && typeof address === 'object');
try { await assert.rejects(startDashboard({ snapshot: async () => { throw new Error('unused'); } }, address.port), /EADDRINUSE/); }
finally { await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve())); }
});