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 * as XLSX from 'xlsx'; import { createAssessorHttpServer } from '../../server/assessor-http-server'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; import type { ImportBatchPreview } from '../../src/domain/import-workflow'; import type { ImportBatchCompleteResponse } from '../../src/api/assessor-api-contract'; import { riskCommand } from '../domain/risk-acceptance-fixture'; import type { CanonicalAssessmentModel, FindingReview } from '../../src/domain/canonical-model'; import { confirmEntryMappings, recognitionOverridesFromResult } from '../../src/features/import-workflow/import-workflow'; import { importProfiles } from '../../src/profiles/profile-catalog'; describe('batch import HTTP workflow', () => { let directory: string; let server: Server; let baseUrl: string; let currentTime: Date; beforeEach(async () => { currentTime = new Date('2026-07-17T08:00:00.000Z'); directory = await mkdtemp(join(tmpdir(), 'assessor-batch-')); await writeFile(join(directory, 'index.html'), '
ASSESSOR
', 'utf8'); server = createAssessorHttpServer({ repository: new LocalAssessorRepository(), staticRoot: directory, now: () => currentTime }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).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) => fetch(`${baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1' }, body: JSON.stringify(body) }); it('automatically persists reference provenance on import and guards explicit idempotent reprocessing', async () => { const bytes = Buffer.from('Rule ID;Title;Severity;Status;MITRE ATT&CK;MITRE D3FEND;Description\nSYN-REF;Synthetic reference;High;Failed;T1059.001;D3-CH;Consider T1003'); const file = { fileName: 'synthetic-references.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file] })).json() as ImportBatchPreview; const entry = preview.entries[0]; const response = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] }); expect(response.status).toBe(200); const complete = await response.json() as ImportBatchCompleteResponse; const model = await (await fetch(`${baseUrl}/api/v1/assessments/${complete.assessmentId}/model`)).json() as CanonicalAssessmentModel; expect(model.referenceRuns).toHaveLength(1); expect(model.referenceRuns![0].results[0].references.map(ref => ref.id)).toEqual(['T1059.001', 'D3-CH', 'T1003']); expect(model.findingDefinitions[0].frameworkReferences).toEqual([]); const path = `/api/v1/projects/${complete.projectId}/references`; expect(await (await fetch(`${baseUrl}${path}`)).json()).toMatchObject({ totalScans: 1, indexedScans: 1, pendingScans: 0 }); expect((await fetch(`${baseUrl}${path}/recognize`, { method: 'POST', body: '{}' })).status).toBe(403); expect(await (await mutate(`${path}/recognize`, {})).json()).toMatchObject({ processedScans: 0, unchangedScans: 1 }); expect((await mutate('/api/v1/projects/foreign-client/references/recognize', {})).status).toBe(404); const reloaded = await (await fetch(`${baseUrl}/api/v1/assessments/${complete.assessmentId}/model`)).json() as CanonicalAssessmentModel; expect(reloaded).toEqual(model); }); it('records risk acceptance through the protected API and preserves it on reload and ordinary review updates', async () => { const bytes = Buffer.from('Rule ID;Title;Severity;Status\nSYN-1;Synthetic risk;High;Failed'); const file = { fileName: 'synthetic.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file] })).json() as ImportBatchPreview; const entry = preview.entries[0]; const complete = await (await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] })).json() as ImportBatchCompleteResponse; const model = await (await fetch(`${baseUrl}/api/v1/assessments/${complete.assessmentId}/model`)).json() as CanonicalAssessmentModel; const occurrenceId = model.findingOccurrences[0].id; const path = `/api/v1/projects/${complete.projectId}/risk-acceptances`; const body = { occurrenceId, expectedRevision: 0, command: riskCommand() }; const noHeader = await fetch(`${baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); expect(noHeader.status).toBe(403); expect((await mutate(path, { ...body, command: { ...body.command, details: {} } })).status).toBe(400); const response = await mutate(path, body); expect(response.status).toBe(200); const { review } = await response.json() as { review: FindingReview }; expect(review.riskAcceptance?.revisions[0]).toMatchObject({ action: 'accept', targetOccurrenceId: occurrenceId, approvalIdentityVerified: false }); expect(Date.parse(review.riskAcceptance!.revisions[0].recordedAt)).toBeGreaterThan(0); expect((await mutate(path, body)).status).toBe(409); expect((await mutate(`/api/v1/projects/other/risk-acceptances`, { ...body, expectedRevision: 1 })).status).toBe(404); expect((await mutate('/api/v1/reviews', { review: { ...review, riskAcceptance: undefined, revision: 2 }, expectedRevision: 1 })).status).toBe(400); expect((await mutate('/api/v1/reviews', { review: { ...review, comment: 'Synthetic comment', revision: 2 }, expectedRevision: 1 })).status).toBe(200); const after = await (await fetch(`${baseUrl}/api/v1/assessments/${complete.assessmentId}/model`)).json() as CanonicalAssessmentModel; expect(after.findingOccurrences).toEqual(model.findingOccurrences); expect(after.reviews[0].riskAcceptance).toEqual(review.riskAcceptance); }); it('previews files independently, marks duplicates and remembers completed hashes', async () => { const bytes = Buffer.from('Rule ID;Title;Severity\nCSV-1;Server import;High', 'utf8'); const file = { fileName: 'findings.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }; const previewResponse = await mutate('/api/v1/import-batches/preview', { files: [file, { ...file, fileName: 'copy.csv' }] }); expect(previewResponse.status).toBe(200); const preview = await previewResponse.json() as ImportBatchPreview; expect(preview.entries).toHaveLength(2); expect(preview.entries[0].dataset?.sheets[0].analysis.records.length).toBeGreaterThan(0); expect(preview.entries[1]).toMatchObject({ status: 'duplicate', selected: false, duplicateOf: 'findings.csv' }); expect(preview.entries[0].sha256).toBe(preview.entries[1].sha256); const completed = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: preview.entries[0].id, sha256: preview.entries[0].sha256, fileName: 'findings.csv', sizeBytes: bytes.length, lastModified: 0 }] }); expect(completed.status).toBe(200); const completion = await completed.json() as ImportBatchCompleteResponse; expect(completion).toMatchObject({ fingerprints: [{ importCount: 1 }], repositoryRevision: 1, importedFiles: 1, importedOccurrences: 1 }); const stored = await fetch(`${baseUrl}/api/v1/assessments/${completion.assessmentId}/model`); await expect(stored.json()).resolves.toMatchObject({ project: { id: completion.projectId }, scanRuns: [{ id: completion.scanRunId }], importFiles: [{ sha256: preview.entries[0].sha256 }], findingOccurrences: [expect.objectContaining({ sourceIdentity: expect.objectContaining({ originalRuleId: 'CSV-1' }) })] }); const repeated = await mutate('/api/v1/import-batches/preview', { files: [file] }); await expect(repeated.json()).resolves.toMatchObject({ entries: [{ status: 'duplicate', selected: false, duplicateOf: 'findings.csv' }] }); }); it('imports into an existing tenant without replacing its name', async () => { const project = { id: 'project-test', name: 'Test', description: 'Mandant', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString(), attributes: {} }; const saved = await mutate('/api/v1/projects', project); expect(saved.status).toBe(200); const bytes = Buffer.from('Rule ID;Title;Severity\nPK-1;Purple Knight finding;High', 'utf8'); const file = { fileName: 'purple-knight.csv', sizeBytes: bytes.length, lastModified: currentTime.getTime(), bytesBase64: bytes.toString('base64'), delimiter: ';' }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file], targetProjectId: project.id })).json() as ImportBatchPreview; const entry = preview.entries[0]; const completed = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, targetProjectId: project.id, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] }); expect(completed.status).toBe(200); const completedBody = await completed.json() as ImportBatchCompleteResponse; await expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}`)).json()).resolves.toMatchObject({ id: project.id, name: 'Test' }); await expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).json()).resolves.toMatchObject({ project: { name: 'Test' }, importFiles: [{ fileName: 'purple-knight.csv' }] }); const changedBytes = Buffer.from('Rule ID;Title;Severity\nPK-2;Changed Purple Knight finding;High', 'utf8'); const possibleDuplicate = await mutate('/api/v1/import-batches/preview', { targetProjectId: project.id, files: [{ ...file, fileName: 'purple-knight-copy.csv', sizeBytes: changedBytes.length, bytesBase64: changedBytes.toString('base64') }] }); await expect(possibleDuplicate.json()).resolves.toMatchObject({ entries: [{ status: 'warning', selected: true, possibleDuplicateOf: expect.stringContaining('Purple') }] }); const deleted = await fetch(`${baseUrl}/api/v1/projects/${project.id}/scan-runs/${completedBody.scanRunId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1' }, body: JSON.stringify({ projectId: project.id, scanRunId: completedBody.scanRunId, cascade: true }) }); expect(deleted.status).toBe(200); await expect(deleted.json()).resolves.toMatchObject({ deleted: true }); await expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/import-files`)).json()).resolves.toEqual([]); }); it('stores repeated source uploads as separate scan runs inside one selected timeslot', async () => { const project = { id: 'timeslot-project', name: 'Synthetic timeslot project', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString(), attributes: {} }; const timeslot = { id: 'timeslot-assessment', projectId: project.id, name: '22.07.2026', effectiveAt: '2026-07-22T12:00:00.000Z', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString() }; expect((await mutate('/api/v1/projects', project)).status).toBe(200); expect((await mutate('/api/v1/assessments', timeslot)).status).toBe(200); const upload = async (fileName: string, ruleId: string) => { const bytes = Buffer.from(`Rule ID;Title;Severity\n${ruleId};${ruleId} title;High`, 'utf8'); const file = { fileName, sizeBytes: bytes.length, lastModified: currentTime.getTime(), bytesBase64: bytes.toString('base64'), delimiter: ';' }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file], targetProjectId: project.id, targetAssessmentId: timeslot.id })).json() as ImportBatchPreview; const entry = preview.entries[0]; const response = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, targetProjectId: project.id, targetAssessmentId: timeslot.id, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] }); expect(response.status).toBe(200); }; await upload('source-one.csv', 'SOURCE-1'); await upload('source-two.csv', 'SOURCE-2'); await expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-history`)).json()).resolves.toMatchObject({ assessments: [{ id: timeslot.id, effectiveAt: timeslot.effectiveAt }], scanRuns: [expect.objectContaining({ assessmentId: timeslot.id, sequence: 1 }), expect.objectContaining({ assessmentId: timeslot.id, sequence: 2 })] }); await expect((await fetch(`${baseUrl}/api/v1/assessments/${timeslot.id}/model`)).json()).resolves.toMatchObject({ assessment: { id: timeslot.id, effectiveAt: timeslot.effectiveAt }, scanRuns: [expect.objectContaining({ sequence: 1 }), expect.objectContaining({ sequence: 2 })], importFiles: [{ fileName: 'source-one.csv' }, { fileName: 'source-two.csv' }] }); }); it('previews and completes a Maester Excel import whose native error text contains U+0000', async () => { const project = { id: 'maester-null-project', name: 'Maester null project', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString(), attributes: {} }; const timeslot = { id: 'maester-null-assessment', projectId: project.id, name: '20.09.2026', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString() }; expect((await mutate('/api/v1/projects', project)).status).toBe(200); expect((await mutate('/api/v1/assessments', timeslot)).status).toBe(200); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([ ['ID', 'Title', 'Severity', 'Status', 'Result'], ['MT.9912', 'Synthetic Graph error', 'High', 'Error', 'HttpStatusCode=401 ErrorMessage=\u0000Synthetic denial'] ]), 'Tests'); const bytes = Buffer.from(XLSX.write(workbook, { type: 'array', bookType: 'xlsx' })); const file = { fileName: 'Maester.xlsx', sizeBytes: bytes.length, lastModified: currentTime.getTime(), bytesBase64: bytes.toString('base64') }; const previewResponse = await mutate('/api/v1/import-batches/preview', { files: [file], targetProjectId: project.id, targetAssessmentId: timeslot.id }); expect(previewResponse.status).toBe(200); const preview = await previewResponse.json() as ImportBatchPreview; expect(preview.entries[0]).toMatchObject({ toolId: 'maester', recordCount: 1 }); const entry = confirmEntryMappings(preview.entries[0], importProfiles); const completedResponse = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, targetProjectId: project.id, targetAssessmentId: timeslot.id, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified, recognitionOverrides: recognitionOverridesFromResult(entry.dataset?.recognition, entry.dataset?.mapping) }] }); expect(completedResponse.status).toBe(200); const completed = await completedResponse.json() as ImportBatchCompleteResponse; const model = await (await fetch(`${baseUrl}/api/v1/assessments/${completed.assessmentId}/model`)).json() as CanonicalAssessmentModel; expect(model.evidence[0].value).toBe('HttpStatusCode=401 ErrorMessage=\\u0000Synthetic denial'); expect(model.diagnostics).toContainEqual(expect.objectContaining({ code: 'unicode-null-escaped' })); }); it('keeps a malformed file as a controlled row instead of failing the whole preview', async () => { const valid = Buffer.from('Name\nFinding', 'utf8'); const response = await mutate('/api/v1/import-batches/preview', { files: [ { fileName: 'valid.csv', sizeBytes: valid.length, lastModified: 0, bytesBase64: valid.toString('base64') }, { fileName: 'invalid.exe', sizeBytes: 3, lastModified: 0, bytesBase64: Buffer.from('bad').toString('base64') } ] }); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ entries: [expect.objectContaining({ dataset: expect.any(Object) }), { status: 'error', selected: false }] }); }); it('reimports a scan into the same timeslot and keeps the superseded run auditable', async () => { const project = { id: 'reimport-project', name: 'Synthetic reimport project', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString(), attributes: {} }; const timeslot = { id: 'reimport-assessment', projectId: project.id, name: '22.07.2026', effectiveAt: '2026-07-22T12:00:00.000Z', createdAt: currentTime.toISOString(), updatedAt: currentTime.toISOString() }; expect((await mutate('/api/v1/projects', project)).status).toBe(200); expect((await mutate('/api/v1/assessments', timeslot)).status).toBe(200); const importSource = async (ruleId: string, replacesScanRunId?: string) => { const bytes = Buffer.from(`Rule ID;Title;Severity\n${ruleId};${ruleId} title;High`, 'utf8'); const file = { fileName: 'generic.csv', sizeBytes: bytes.length, lastModified: currentTime.getTime(), bytesBase64: bytes.toString('base64'), delimiter: ';' }; const target = { targetProjectId: project.id, targetAssessmentId: timeslot.id, replacesScanRunId }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file], ...target })).json() as ImportBatchPreview; expect(preview.replacesScanRunId).toBe(replacesScanRunId); const entry = preview.entries[0]; const response = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }], ...target }); expect(response.status).toBe(200); const completed = await response.json() as ImportBatchCompleteResponse; expect(completed.replacedScanRunId).toBe(replacesScanRunId); return completed; }; const original = await importSource('OLD-1'); currentTime = new Date('2026-07-17T09:00:00.000Z'); const changedBytes = Buffer.from('Rule ID;Title;Severity\nNEW-1;Different scan;High', 'utf8'); const rejected = await mutate('/api/v1/import-batches/preview', { files: [{ fileName: 'generic.csv', sizeBytes: changedBytes.length, lastModified: currentTime.getTime(), bytesBase64: changedBytes.toString('base64'), delimiter: ';' }], targetProjectId: project.id, targetAssessmentId: timeslot.id, replacesScanRunId: original.scanRunId }); expect(rejected.status).toBe(409); await expect(rejected.json()).resolves.toMatchObject({ error: { code: 'reimport_source_mismatch' } }); const replacement = await importSource('OLD-1', original.scanRunId); const activeModel = await (await fetch(`${baseUrl}/api/v1/assessments/${timeslot.id}/model`)).json(); expect(activeModel).toMatchObject({ scanRuns: [{ id: replacement.scanRunId, supersedesScanRunId: original.scanRunId }], findingOccurrences: [expect.objectContaining({ sourceIdentity: expect.objectContaining({ originalRuleId: 'OLD-1' }) })] }); expect(activeModel.scanRuns).toHaveLength(1); expect(activeModel.findingOccurrences).toHaveLength(1); const exported = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/export`)).json(); expect(exported.scanRuns).toEqual(expect.arrayContaining([ expect.objectContaining({ id: original.scanRunId, supersededByScanRunId: replacement.scanRunId }), expect.objectContaining({ id: replacement.scanRunId, supersedesScanRunId: original.scanRunId }) ])); expect(exported.findingOccurrences).toHaveLength(2); }); it('rejects a changed completion selection without creating partial repository data', async () => { const bytes = Buffer.from('Rule ID;Title\nSAFE-1;Original', 'utf8'); const response = await mutate('/api/v1/import-batches/preview', { files: [{ fileName: 'safe.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }] }); const preview = await response.json() as ImportBatchPreview; const entry = preview.entries[0]; const changed = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: entry.id, sha256: '0'.repeat(64), fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] }); expect(changed.status).toBe(400); await expect(changed.json()).resolves.toMatchObject({ error: { code: 'import_batch_selection_mismatch' } }); await expect((await fetch(`${baseUrl}/api/v1/projects`)).json()).resolves.toEqual([]); }); it('rejects XML entity declarations with a structured per-file resource diagnostic', async () => { const bytes = Buffer.from(']>&e;', 'utf8'); const file = { fileName: 'entity.xml', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64') }; const previewResponse = await mutate('/api/v1/import-batches/preview', { files: [file] }); expect(previewResponse.status).toBe(200); await expect(previewResponse.json()).resolves.toMatchObject({ entries: [{ status: 'error', selected: false, errorMessages: [expect.stringContaining('xml-doctype-rejected')] }] }); const directResponse = await mutate('/api/v1/import-sources/parse', file); expect(directResponse.status).toBe(422); await expect(directResponse.json()).resolves.toMatchObject({ error: { code: 'xml-doctype-rejected' } }); }); it('requires explicit mapping confirmation and persists the validated profile decision', async () => { const bytes = Buffer.from('Alpha;Beta\none;two', 'utf8'); const file = { fileName: 'CIS-CAT-SYNTHETIC.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }; const preview = await (await mutate('/api/v1/import-batches/preview', { files: [file] })).json() as ImportBatchPreview; const entry = preview.entries[0]; expect(entry).toMatchObject({ toolId: 'cis-cat', mappingsToConfirm: expect.any(Number) }); expect(entry.mappingsToConfirm).toBeGreaterThan(0); const selection = { entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }; const rejected = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [selection] }); expect(rejected.status).toBe(409); await expect(rejected.json()).resolves.toMatchObject({ error: { code: 'import_mapping_confirmation_required' } }); const invalid = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ ...selection, recognitionOverrides: { sourceTool: { toolId: 'unregistered', toolName: 'Unregistered' } } }] }); expect(invalid.status).toBe(400); await expect(invalid.json()).resolves.toMatchObject({ error: { code: 'invalid_recognition_overrides' } }); const invalidCandidate = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ ...selection, recognitionOverrides: { candidateFieldRoles: [{ decisionId: 'missing', role: 'title' }] } }] }); expect(invalidCandidate.status).toBe(400); await expect(invalidCandidate.json()).resolves.toMatchObject({ error: { code: 'invalid_recognition_overrides' } }); const recognition = entry.dataset!.recognition!; const recognitionOverrides = { sourceTool: recognition.source.requiresConfirmation ? { toolId: recognition.source.selected.toolId, toolName: recognition.source.selected.toolName } : undefined, tableSelections: Object.fromEntries(recognition.tables.filter(item => item.requiresConfirmation).map(item => [item.tableId, item.selected])), fieldRoles: recognition.fields.filter(item => item.requiresConfirmation).map(item => ({ tableId: item.tableId, columnIndex: item.columnIndex, role: item.proposedRole ?? item.appliedRole })) }; const completed = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ ...selection, recognitionOverrides }] }); expect(completed.status).toBe(200); const result = await completed.json() as ImportBatchCompleteResponse; await expect((await fetch(`${baseUrl}/api/v1/assessments/${result.assessmentId}/model`)).json()).resolves.toMatchObject({ importFiles: [{ detectedTool: { id: 'cis-cat' }, processedWith: { profileId: 'cis-cat' } }] }); }); it('expires previews and leaves the repository unchanged', async () => { const bytes = Buffer.from('Rule ID;Title\nTTL-1;Expires', 'utf8'); const response = await mutate('/api/v1/import-batches/preview', { files: [{ fileName: 'expires.csv', sizeBytes: bytes.length, lastModified: 0, bytesBase64: bytes.toString('base64'), delimiter: ';' }] }); const preview = await response.json() as ImportBatchPreview; currentTime = new Date('2026-07-17T08:16:00.000Z'); const entry = preview.entries[0]; const completion = await mutate('/api/v1/import-batches/complete', { batchId: preview.batchId, entries: [{ entryId: entry.id, sha256: entry.sha256, fileName: entry.fileName, sizeBytes: entry.sizeBytes, lastModified: entry.lastModified }] }); expect(completion.status).toBe(409); await expect(completion.json()).resolves.toMatchObject({ error: { code: 'import_batch_expired' } }); await expect((await fetch(`${baseUrl}/api/v1/projects`)).json()).resolves.toEqual([]); }); });