import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
import type { AddressInfo } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { Server } from 'node:http';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createAssessorHttpServer } from '../../server/assessor-http-server';
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
import { FileSystemReportArchive, reportArchiveProjectDirectory } from '../../server/reporting/report-archive';
import { REPORT_PROJECTION_VERSION } from '../../src/reporting/report-projection';
const timestamp = '2026-07-17T08:00:00.000Z';
const project = { id: 'project-1', name: 'Project 1', createdAt: timestamp, updatedAt: timestamp, attributes: {} };
const assessment = { id: 'assessment-1', projectId: project.id, name: 'Assessment 1', createdAt: timestamp, updatedAt: timestamp };
const scanRun = {
id: 'run-1', assessmentId: assessment.id, sequence: 1, label: 'Run 1',
tool: { id: 'generic', name: 'Generic' }, importedAt: timestamp
};
describe('local ASSESSOR HTTP server', () => {
let directory: string;
let archiveDirectory: string;
let server: Server;
let baseUrl: string;
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'assessor-server-'));
await writeFile(join(directory, 'index.html'), '
ASSESSORLocal app', 'utf8');
archiveDirectory = await mkdtemp(join(tmpdir(), 'assessor-report-archive-'));
server = createAssessorHttpServer({
repository: new LocalAssessorRepository(), staticRoot: directory,
reportArchive: new FileSystemReportArchive(archiveDirectory)
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${address.port}`;
});
afterEach(async () => {
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
await rm(directory, { recursive: true, force: true });
await rm(archiveDirectory, { recursive: true, force: true, maxRetries: 3 });
});
const mutate = (path: string, body: unknown, origin = baseUrl) => fetch(`${baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: origin },
body: JSON.stringify(body)
});
it('serves the browser app and a same-origin health endpoint with hardening headers', async () => {
const page = await fetch(`${baseUrl}/`);
expect(page.status).toBe(200);
expect(await page.text()).toContain('Local app');
expect(page.headers.get('content-security-policy')).toContain("default-src 'self'");
expect(page.headers.get('x-content-type-options')).toBe('nosniff');
const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.25.2', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
});
expect(health.headers.get('access-control-allow-origin')).toBeNull();
});
it('rejects unmarked and cross-origin mutations', async () => {
const missingHeader = await fetch(`${baseUrl}/api/v1/projects`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(project)
});
expect(missingHeader.status).toBe(403);
await expect(missingHeader.json()).resolves.toMatchObject({ error: { code: 'missing_mutation_header' } });
const foreignOrigin = await mutate('/api/v1/projects', project, 'https://untrusted.example');
expect(foreignOrigin.status).toBe(403);
await expect(foreignOrigin.json()).resolves.toMatchObject({ error: { code: 'cross_origin_rejected' } });
});
it('persists work package flags independently and leaves the complete Core unchanged', async () => {
await mutate('/api/v1/projects', project);
await mutate('/api/v1/assessments', assessment);
const packagesUrl = `${baseUrl}/api/v1/projects/${project.id}/work-packages?assessmentIds=${assessment.id}`;
const coreBefore = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).text();
expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/work-packages`)).status).toBe(400);
expect(await (await fetch(packagesUrl)).json()).toEqual({ status: 'context-required' });
await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`);
const packages = await (await fetch(packagesUrl)).json();
expect(packages).toMatchObject({ status: 'ready', result: { projectId: project.id, assessmentIds: [assessment.id], packages: [] } });
expect(await (await fetch(packagesUrl)).json()).toEqual(packages);
expect(await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).text()).toBe(coreBefore);
});
it('exposes repository operations and preserves atomic import sessions across HTTP calls', async () => {
expect((await mutate('/api/v1/projects', project)).status).toBe(200);
expect((await mutate('/api/v1/assessments', assessment)).status).toBe(200);
expect((await mutate('/api/v1/import-sessions', {
sessionId: 'session-1', projectId: project.id, assessmentId: assessment.id, scanRun
})).status).toBe(201);
expect((await mutate('/api/v1/import-sessions/session-1/stage', {
importFiles: [{
id: 'file-1', scanRunId: scanRun.id, fileName: 'input.csv', format: 'CSV', sizeBytes: 10,
importedAt: timestamp, detectedTool: scanRun.tool,
detection: { level: 'high', score: 1, reasons: ['synthetic'] },
processedWith: { parserId: 'csv', parserVersion: '1.0.0', profileId: 'generic', profileVersion: '1.0.0', mappingVersion: '1.0.0' },
sourceArtifact: { kind: 'not-retained', reason: 'test' }
}],
reportSections: [{
version: 1, id: 'section-1', scanRunId: scanRun.id, importFileId: 'file-1', tool: scanRun.tool,
sectionKey: 'sectionDomainInformation', label: 'Domain information', ordinal: 0, state: 'observed',
tables: [], source: { objectPath: 'html/sections/domain' }, extractorVersion: '2.0.0', attributes: {}
}],
environmentObservations: [{
version: 1, id: 'observation-1', scanRunId: scanRun.id, importFileId: 'file-1', tool: scanRun.tool,
subject: { type: 'domain-information' }, property: 'domain.functional_level', label: 'Domain functional level',
originalValue: '2016', normalizedValue: 2016, valueType: 'number', state: 'observed',
sectionKey: 'sectionDomainInformation', source: { objectPath: 'html/sections/domain/table/0' }, mappingVersion: '2.0.0', attributes: {}
}]
})).status).toBe(200);
const beforeCommit = await fetch(`${baseUrl}/api/v1/assessments/${assessment.id}/model`);
expect((await beforeCommit.json()).scanRuns).toHaveLength(0);
expect((await mutate('/api/v1/import-sessions/session-1/commit', {})).status).toBe(200);
const afterCommit = await fetch(`${baseUrl}/api/v1/assessments/${assessment.id}/model`);
expect((await afterCommit.json()).scanRuns).toHaveLength(1);
const projects = await fetch(`${baseUrl}/api/v1/projects`);
expect(await projects.json()).toEqual([project]);
const environment = await fetch(`${baseUrl}/api/v1/projects/${project.id}/environment-snapshot?scanRunIds=${scanRun.id}`);
expect(environment.status).toBe(200);
await expect(environment.json()).resolves.toMatchObject({
projectId: project.id, scanRunIds: [scanRun.id], coverage: { observed: 2 },
observations: [expect.objectContaining({ property: 'domain.functional_level', normalizedValue: 2016 })]
});
const infoPath = `${baseUrl}/api/v1/projects/${project.id}/client-information`;
const initialRevision = (await (await fetch(`${baseUrl}/api/v1/health`)).json()).repositoryRevision;
const information = await fetch(`${infoPath}?assessmentId=${assessment.id}&scanRunIds=${scanRun.id}&areas=directory&properties=domain.functional_level&limit=1`);
expect(information.status).toBe(200);
await expect(information.json()).resolves.toMatchObject({
contractVersion: '1.1.0', purpose: 'internal-analysis', scopeMode: 'current-imports', repositoryRevision: initialRevision,
projectId: project.id, assessmentId: assessment.id, scanRunIds: [scanRun.id], totalObservations: 1,
observations: [expect.objectContaining({ originalValue: '2016', normalizedValue: 2016, importFileId: 'file-1' })]
});
await expect((await fetch(`${infoPath}?includeSuperseded=true`)).json()).resolves.toMatchObject({ scopeMode: 'including-history' });
await expect((await fetch(`${infoPath}?properties=missing`)).json()).resolves.toMatchObject({ totalObservations: 0, observations: [] });
for (const invalid of ['areas=invalid', 'limit=201', 'offset=', 'limit=1&limit=2', 'includeSuperseded=yes', 'unknown=x', 'assessmentId=foreign', 'scanRunIds=foreign']) {
expect((await fetch(`${infoPath}?${invalid}`)).status, invalid).toBe(400);
}
expect((await fetch(`${baseUrl}/api/v1/projects/foreign/client-information`)).status).toBe(404);
expect((await (await fetch(`${baseUrl}/api/v1/health`)).json()).repositoryRevision).toBe(initialRevision);
const environmentResults = await fetch(
`${baseUrl}/api/v1/results?projectId=${project.id}&sourceKind=environment-observation&environmentProperty=domain.functional_level&environmentState=observed`
);
expect(environmentResults.status).toBe(200);
await expect(environmentResults.json()).resolves.toEqual({ total: 0, items: [] });
const environmentResult = await fetch(`${baseUrl}/api/v1/results/environment-observation%3Aobservation-1`);
expect(environmentResult.status).toBe(404);
const report = await fetch(`${baseUrl}/api/v1/projects/${project.id}/report-projection?assessmentIds=${assessment.id}&scanRunIds=${scanRun.id}`);
expect(report.status).toBe(200);
await expect(report.json()).resolves.toMatchObject({
version: REPORT_PROJECTION_VERSION, project: { id: project.id }, assessmentId: assessment.id,
environment: expect.arrayContaining([{ key: 'Domain' }, { key: 'Forest' }, { key: 'Domain Controller' }]),
coverage: { total: 0 }, diagnostics: expect.any(Array), provenance: expect.any(Array)
});
const metrics = await fetch(`${baseUrl}/api/v1/projects/${project.id}/timeslot-result-metrics?assessmentIds=${assessment.id}&scanRunIds=${scanRun.id}`);
expect(metrics.status).toBe(200);
await expect(metrics.json()).resolves.toEqual({ all: 0, deviation: 0, passed: 0, notApplicable: 0, notRun: 0, error: 0 });
const missingScope = await fetch(`${baseUrl}/api/v1/projects/${project.id}/timeslot-result-metrics?scanRunIds=${scanRun.id}`);
expect(missingScope.status).toBe(400);
// Report Engine: one run archives both files and returns the requested one unchanged.
const reportRun = await mutate(`/api/v1/projects/${project.id}/report-runs`,
{ kind: 'management', assessmentIds: [assessment.id], scanRunIds: [scanRun.id] });
expect(reportRun.status).toBe(200);
const run = await reportRun.json();
expect(run).toMatchObject({ kind: 'management', fileName: expect.stringMatching(/^assessor-management-report-\d{4}-\d{2}-\d{2}\.html$/) });
const archived = await new FileSystemReportArchive(archiveDirectory).read(project.id, run.runId);
expect(archived.management).toBe(run.html);
expect(run.html).toContain('Context-Heatmap');
expect(archived.projection.context?.repositoryRevision).toBe(run.repositoryRevision);
expect(archived.projection.context?.assessmentIds).toEqual([assessment.id]);
expect(archived.manifest).toMatchObject({ projectId: project.id, scope: { assessmentIds: [assessment.id], scanRunIds: [scanRun.id] } });
// The Technical Report reads the same published Context, so its problem fields match the Management Report.
const technicalRun = await mutate(`/api/v1/projects/${project.id}/report-runs`,
{ kind: 'technical', assessmentIds: [assessment.id], scanRunIds: [scanRun.id] });
expect(technicalRun.status).toBe(200);
const technical = await technicalRun.json();
expect(technical).toMatchObject({ kind: 'technical', fileName: expect.stringMatching(/^assessor-technical-report-\d{4}-\d{2}-\d{2}\.html$/) });
const technicalArchive = await new FileSystemReportArchive(archiveDirectory).read(project.id, technical.runId);
expect(technicalArchive.technical).toBe(technical.html);
expect(technicalArchive.projection.context?.repositoryRevision).toBe(technical.repositoryRevision);
expect(technical.html).toContain('id="problemfelder"');
expect((await mutate(`/api/v1/projects/${project.id}/report-runs`, { kind: 'raw' })).status).toBe(400);
// Context layer (Phase 13E): read-only, scoped to the selected timeslot and cached per revision.
expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context`)).status).toBe(400);
expect((await fetch(`${baseUrl}/api/v1/projects/unknown-dummy/assessment-context?assessmentIds=${assessment.id}`)).status).toBe(404);
const contextResponse = await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`);
expect(contextResponse.status).toBe(200);
const assessmentContext = await contextResponse.json();
expect(assessmentContext).toMatchObject({
version: 1, engine: { id: 'assessment-context-engine' }, projectId: project.id, assessmentIds: [assessment.id],
facts: [], coverage: { observations: 0 },
clusters: expect.arrayContaining([expect.objectContaining({ clusterId: 'credential-relay-exposure', state: 'undetermined', unknown: 26 })])
});
const cachedContext = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`)).json();
expect(cachedContext.generatedAt).toBe(assessmentContext.generatedAt);
// Work package flags are scoped, persisted separately and leave the entire Core export unchanged.
const coreBefore = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).text();
expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/work-packages`)).status).toBe(400);
const packagesUrl = `${baseUrl}/api/v1/projects/${project.id}/work-packages?assessmentIds=${assessment.id}`;
const packages = await (await fetch(packagesUrl)).json();
expect(packages).toMatchObject({ status: 'ready', result: { projectId: project.id, assessmentIds: [assessment.id], packages: [] } });
expect(await (await fetch(packagesUrl)).json()).toEqual(packages);
expect(await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).text()).toBe(coreBefore);
const deletedProject = await fetch(`${baseUrl}/api/v1/projects/${project.id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl },
body: JSON.stringify({ projectId: project.id, cascade: true })
});
expect(deletedProject.status).toBe(200);
await expect(readdir(join(archiveDirectory, reportArchiveProjectDirectory(project.id)))).rejects.toThrow();
});
it('keeps ASSESSOR running and refuses only report runs when the Report Engine is unavailable', async () => {
const unavailable = createAssessorHttpServer({ repository: new LocalAssessorRepository(), staticRoot: directory, reportPort: null });
await new Promise(resolve => unavailable.listen(0, '127.0.0.1', resolve));
const url = `http://127.0.0.1:${(unavailable.address() as AddressInfo).port}`;
try {
expect((await fetch(`${url}/api/v1/health`)).status).toBe(200);
const refused = await fetch(`${url}/api/v1/projects/${project.id}/report-projection`);
expect(refused.status).toBe(503);
await expect(refused.json()).resolves.toMatchObject({ error: { code: 'report_engine_unavailable' } });
expect((await fetch(`${url}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`)).status).toBe(503);
const noArchive = await fetch(`${url}/api/v1/projects/${project.id}/report-runs`, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: url },
body: JSON.stringify({ kind: 'technical' })
});
expect(noArchive.status).toBe(503);
await expect(noArchive.json()).resolves.toMatchObject({ error: { code: 'report_archive_unavailable' } });
const unknownProject = await mutate('/api/v1/projects/unknown-dummy/report-runs', { kind: 'technical' });
expect(unknownProject.status).toBe(404);
await expect(readdir(archiveDirectory)).resolves.toEqual([]);
} finally {
await new Promise(resolve => unavailable.close(() => resolve()));
}
});
it('validates import metadata before file content reaches a future server parser', async () => {
const accepted = await mutate('/api/v1/import-sources/validate', { fileName: 'report.xlsx', sizeBytes: 2048 });
await expect(accepted.json()).resolves.toMatchObject({ accepted: true, format: 'XLSX' });
const rejected = await mutate('/api/v1/import-sources/validate', { fileName: 'payload.exe', sizeBytes: 10 });
expect(rejected.status).toBe(415);
await expect(rejected.json()).resolves.toMatchObject({ error: { code: 'unsupported_import_format' } });
});
it('deletes a confirmed assessment through the project-bound timeslot endpoint', async () => {
await mutate('/api/v1/projects', project);
await mutate('/api/v1/assessments', assessment);
const response = await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessments/${assessment.id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl },
body: JSON.stringify({ projectId: project.id, assessmentId: assessment.id, cascade: true })
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ deleted: true });
const projects = await fetch(`${baseUrl}/api/v1/projects`);
await expect(projects.json()).resolves.toEqual([project]);
});
it('accepts the current Maester HTML wrapper through the upload/preview API with synthetic Dummy data', async () => {
const bytes = Buffer.from('Maester Dummy', 'utf8');
const response = await mutate('/api/v1/import-sources/parse', {
fileName: 'TestResults-Dummy.html', sizeBytes: bytes.length, lastModified: 0,
mediaType: 'text/html', bytesBase64: bytes.toString('base64')
});
expect(response.status).toBe(200);
const imported = await response.json();
expect(imported).toMatchObject({ format: 'HTML', profileId: 'maester',
extraction: { extractor: { id: 'maester-html-source-extractor', version: '1.2.1' } } });
expect(imported.extraction.candidates).toHaveLength(1);
expect(imported.extraction.candidates[0].source.objectPath).toBe('$.Tests[0]');
// Includes cold HTML-runtime/worker startup on a synchronized Windows drive.
}, 15_000);
it('parses an uploaded source through the backend API', async () => {
const bytes = Buffer.from('Rule ID;Title;Severity\nCSV-1;Server import;High', 'utf8');
const response = await mutate('/api/v1/import-sources/parse', {
fileName: 'findings.csv',
sizeBytes: bytes.length,
lastModified: 0,
mediaType: 'text/csv',
bytesBase64: bytes.toString('base64'),
delimiter: ';'
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
fileName: 'findings.csv',
format: 'CSV',
sheets: [expect.objectContaining({ analysis: expect.objectContaining({ records: expect.any(Array) }) })]
});
});
it('returns structured errors and an audit trail without request bodies', async () => {
await mutate('/api/v1/projects', project);
const missing = await fetch(`${baseUrl}/api/v1/unknown`);
expect(missing.status).toBe(404);
await expect(missing.json()).resolves.toMatchObject({
error: { code: 'api_route_not_found', requestId: expect.stringMatching(/^request-/) }
});
const audit = await fetch(`${baseUrl}/api/v1/audit-events`);
const events = await audit.json() as Array>;
expect(events.some(event => event.path === '/api/v1/projects' && event.method === 'POST' && event.status === 200)).toBe(true);
expect(events).toContainEqual(expect.objectContaining({ durationMs: expect.any(Number) }));
expect(JSON.stringify(events)).not.toContain(project.name);
});
it('exposes the unified result, facet and category read endpoints without mutation headers', async () => {
const results = await fetch(`${baseUrl}/api/v1/results?severity=high&severity=critical`);
expect(results.status).toBe(200);
await expect(results.json()).resolves.toEqual({ total: 0, items: [] });
const facets = await fetch(`${baseUrl}/api/v1/results/facets`);
expect(facets.status).toBe(200);
await expect(facets.json()).resolves.toMatchObject({ total: 0, facets: expect.any(Array) });
const categories = await fetch(`${baseUrl}/api/v1/results/categories`);
expect(categories.status).toBe(200);
await expect(categories.json()).resolves.toEqual([]);
const missing = await fetch(`${baseUrl}/api/v1/results/scanner-finding%3Amissing`);
expect(missing.status).toBe(404);
await expect(missing.json()).resolves.toMatchObject({ error: { code: 'repository_not_found' } });
});
});