Assessor_2.0/tests/server/report-archive.test.ts

95 lines
5.7 KiB
TypeScript

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