import { mkdtemp, 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'; 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 server: Server; let baseUrl: string; beforeEach(async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-server-')); await writeFile(join(directory, 'index.html'), 'ASSESSOR
Local app
', 'utf8'); server = createAssessorHttpServer({ repository: new LocalAssessorRepository(), staticRoot: directory }); 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 }); }); 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', 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('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 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.toMatchObject({ total: 1, items: [expect.objectContaining({ resultProjectionId: 'environment-observation:observation-1', environmentProperty: 'domain.functional_level', environmentOriginalValue: '2016', environmentNormalizedValue: 2016 })] }); const environmentResult = await fetch(`${baseUrl}/api/v1/results/environment-observation%3Aobservation-1`); expect(environmentResult.status).toBe(200); await expect(environmentResult.json()).resolves.toMatchObject({ sourceResultId: 'observation-1', sourceKind: 'environment-observation', sourceLocator: { objectPath: expect.any(String) } }); }); 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('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' } }); }); });