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'), 'ASSESSOR
Local 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' } }); }); });