import { mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ReportProjection } from '../../src/reporting/report-projection'; import { renderReportFiles, reportEngineVersions, type ReportRun } from '../../src/reporting/report-run'; import { buildReportProjection } from '../../src/reporting/report-projection'; import { FileSystemReportArchive, REPORT_ARCHIVE_FILES, reportArchiveProjectDirectory, resolveReportArchiveRoot } from '../../server/reporting/report-archive'; function dummyRun(projectId = 'dummy-project'): ReportRun { const projection: ReportProjection = buildReportProjection({ generatedAt: '2026-09-30T08:00:00Z', project: { id: projectId, name: 'Dummy Customer' }, results: [], domains: [], domainOf: () => 'unclassified' }); return { projection, revision: 3, versions: reportEngineVersions(), files: renderReportFiles(projection), scope: { projectId, generatedAt: projection.generatedAt, assessmentIds: ['dummy-assessment'] } }; } let root: string; beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'assessor-report-archive-')); }); afterEach(async () => { await rm(root, { recursive: true, force: true, maxRetries: 3 }); }); describe('Report archive', () => { it('archives one run write-once with a verifiable manifest and no partial folder', async () => { const archive = new FileSystemReportArchive(root); const run = dummyRun(); const manifest = await archive.write(run, new Date('2026-09-30T08:00:01.234Z')); expect(manifest.runId).toMatch(/^20260930T080001Z-[0-9a-f]{8}$/); expect(manifest).toMatchObject({ version: 1, handling: 'internal-confidential', projectId: 'dummy-project', repositoryRevision: 3, scope: { assessmentIds: ['dummy-assessment'] }, versions: reportEngineVersions() }); const projectDirectory = join(root, reportArchiveProjectDirectory('dummy-project')); expect(await readdir(projectDirectory)).toEqual([manifest.runId]); expect((await readdir(join(projectDirectory, manifest.runId))).sort()).toEqual(Object.values(REPORT_ARCHIVE_FILES).sort()); const archived = await archive.read('dummy-project', manifest.runId); expect(archived.management).toBe(run.files.management); expect(archived.technical).toBe(run.files.technical); expect(renderReportFiles(archived.projection)).toEqual(run.files); const file = join(projectDirectory, manifest.runId, REPORT_ARCHIVE_FILES.management); expect((await stat(file)).mode & 0o222).toBe(0); // Root (e.g. a CI job container) bypasses file permission bits; the mode check above still applies. if (process.getuid?.() !== 0) await expect(writeFile(file, 'changed')).rejects.toThrow(); }); it('detects a changed archived file instead of trusting it', async () => { const archive = new FileSystemReportArchive(root); const manifest = await archive.write(dummyRun(), new Date('2026-09-30T08:00:00Z')); const file = join(root, reportArchiveProjectDirectory('dummy-project'), manifest.runId, REPORT_ARCHIVE_FILES.technical); await rm(file, { force: true }); await writeFile(file, 'tampered'); await expect(archive.read('dummy-project', manifest.runId)).rejects.toThrow(/Manifest/); await expect(archive.read('dummy-project', '../escape')).rejects.toThrow(/Lauf-ID/); }); it('keeps separate runs and removes every run of a client together with the client only', async () => { const archive = new FileSystemReportArchive(root); await archive.write(dummyRun(), new Date('2026-09-30T08:00:00Z')); await archive.write(dummyRun(), new Date('2026-09-30T08:00:00Z')); await archive.write(dummyRun('other-dummy-project'), new Date('2026-09-30T08:00:00Z')); expect(await readdir(join(root, reportArchiveProjectDirectory('dummy-project')))).toHaveLength(2); await archive.deleteProject('dummy-project'); await archive.deleteProject('dummy-project'); expect(await readdir(root)).toEqual([reportArchiveProjectDirectory('other-dummy-project')]); }); it('never places the archive in the repository, the served app or a OneDrive folder', () => { const repository = join(root, 'repo'); expect(resolveReportArchiveRoot({ ASSESSOR_REPORT_ARCHIVE_DIR: join(root, 'archive') }, [repository])) .toBe(join(root, 'archive')); expect(() => resolveReportArchiveRoot({ ASSESSOR_REPORT_ARCHIVE_DIR: join(repository, 'reports') }, [repository])) .toThrow(/Repository/); expect(() => resolveReportArchiveRoot({ ASSESSOR_REPORT_ARCHIVE_DIR: 'relative/archive' }, [repository])).toThrow(/absolut/); expect(() => resolveReportArchiveRoot({ ASSESSOR_REPORT_ARCHIVE_DIR: join(root, 'OneDrive - Dummy', 'archive') }, [repository])) .toThrow(/OneDrive/); expect(() => resolveReportArchiveRoot({ ASSESSOR_REPORT_ARCHIVE_DIR: join(root, 'sync', 'archive'), OneDrive: join(root, 'sync') }, [repository])) .toThrow(/OneDrive/); expect(resolveReportArchiveRoot({ LOCALAPPDATA: 'C:\\Users\\dummy\\AppData\\Local' }, [], 'win32', 'C:\\Users\\dummy')) .toMatch(/AppData[\\/]Local[\\/]ASSESSOR[\\/]report-archive$/); expect(resolveReportArchiveRoot({}, [], 'linux', '/home/dummy')).toMatch(/\.local[\\/]share[\\/]assessor[\\/]report-archive$/); }); it('writes nothing outside its own root', async () => { const archive = new FileSystemReportArchive(join(root, 'archive')); await archive.write(dummyRun(), new Date('2026-09-30T08:00:00Z')); expect(await readdir(root)).toEqual(['archive']); expect(JSON.parse(await readFile(join(root, 'archive', reportArchiveProjectDirectory('dummy-project'), (await readdir(join(root, 'archive', reportArchiveProjectDirectory('dummy-project'))))[0], REPORT_ARCHIVE_FILES.manifest), 'utf8')).projectId) .toBe('dummy-project'); }); });