120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { GpoBackupStatusRecord } from '../../src/api/assessor-api-contract';
|
|
import type { Assessment, ImportFileRecord, ScanRun } from '../../src/domain/canonical-model';
|
|
import { groupProjectImportFiles } from '../../src/features/project-import/datahub-import-groups';
|
|
|
|
function file(overrides: Partial<ImportFileRecord>): ImportFileRecord {
|
|
return {
|
|
id: 'file',
|
|
scanRunId: 'run-1',
|
|
fileName: 'report.csv',
|
|
format: 'CSV',
|
|
sizeBytes: 10,
|
|
importedAt: '2026-07-18T08:00:00.000Z',
|
|
detectedTool: { id: 'generic', name: 'Tabular Data' },
|
|
detection: { level: 'unknown', score: 0, reasons: [] },
|
|
processedWith: { parserId: 'csv', parserVersion: '1', profileId: 'generic', profileVersion: '1', mappingVersion: '1' },
|
|
sourceArtifact: { kind: 'not-retained', reason: 'test' },
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function assessment(id: string, effectiveAt?: string): Assessment {
|
|
return {
|
|
id,
|
|
projectId: 'project',
|
|
name: id,
|
|
effectiveAt,
|
|
createdAt: '2026-08-04T08:00:00.000Z',
|
|
updatedAt: '2026-08-04T08:00:00.000Z'
|
|
};
|
|
}
|
|
|
|
function run(id: string, assessmentId: string): ScanRun {
|
|
return {
|
|
id,
|
|
assessmentId,
|
|
sequence: 1,
|
|
label: id,
|
|
tool: { id: 'multiple', name: 'Assessment source' },
|
|
importedAt: '2026-08-04T08:00:00.000Z'
|
|
};
|
|
}
|
|
|
|
const gpo: GpoBackupStatusRecord = {
|
|
version: 1,
|
|
artifactId: 'gpo-1',
|
|
fileName: 'gpo.zip',
|
|
sizeBytes: 50,
|
|
submittedAt: '2026-08-04T08:00:00.000Z',
|
|
updatedAt: '2026-08-04T08:00:00.000Z',
|
|
state: 'partial',
|
|
attempts: 1,
|
|
maxAttempts: 3,
|
|
gpoCount: 3,
|
|
backupInstanceCount: 3
|
|
};
|
|
|
|
describe('Datahub assessment timeslot grouping', () => {
|
|
it('shows one source entry for a multi-file scanner batch', () => {
|
|
const groups = groupProjectImportFiles([
|
|
file({ id: 'summary', fileName: 'Assessment_summary.csv' }),
|
|
file({ id: 'findings', fileName: 'Indicators_results.csv', sizeBytes: 30, detectedTool: { id: 'purple-knight', name: 'PurpleKnight' } }),
|
|
file({ id: 'detail', fileName: 'SI000134.csv', sizeBytes: 20 })
|
|
]);
|
|
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
scanRunIds: ['run-1'],
|
|
fileCount: 3,
|
|
sourceCount: 1,
|
|
sizeBytes: 60,
|
|
tool: { id: 'purple-knight', name: 'PurpleKnight' },
|
|
formats: ['CSV']
|
|
});
|
|
});
|
|
|
|
it('keeps genuinely different assessment dates as separate timeslots', () => {
|
|
const groups = groupProjectImportFiles([
|
|
file({ id: 'old', scanRunId: 'run-old', lastModifiedAt: '2026-07-01T08:00:00.000Z' }),
|
|
file({ id: 'new', scanRunId: 'run-new', lastModifiedAt: '2026-07-12T08:00:00.000Z' })
|
|
]);
|
|
expect(groups.map(group => group.id)).toEqual(['timeslot-2026-07-12', 'timeslot-2026-07-01']);
|
|
});
|
|
|
|
it('coalesces CIS-CAT, PingCastle, PurpleKnight, Maester and GPO into the one 22.07 timeslot', () => {
|
|
const groups = groupProjectImportFiles([
|
|
file({ id: 'cis', scanRunId: 'run-cis', detectedTool: { id: 'cis-cat', name: 'CIS-CAT' }, lastModifiedAt: '2026-07-22T08:00:00.000Z' }),
|
|
file({ id: 'ping', scanRunId: 'run-ping', detectedTool: { id: 'pingcastle', name: 'PingCastle' }, lastModifiedAt: '2026-07-22T08:00:02.000Z' }),
|
|
file({ id: 'purple', scanRunId: 'run-purple', detectedTool: { id: 'purple-knight', name: 'PurpleKnight' }, lastModifiedAt: '2026-07-22T08:00:03.000Z' }),
|
|
file({ id: 'maester', scanRunId: 'run-maester', detectedTool: { id: 'maester', name: 'Maester' }, lastModifiedAt: '2026-07-22T08:00:04.000Z' })
|
|
], [], [], [gpo]);
|
|
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
id: 'timeslot-2026-07-22',
|
|
fileCount: 4,
|
|
sourceCount: 5,
|
|
sizeBytes: 90,
|
|
tool: { id: 'multiple', name: 'Multiple Tools' }
|
|
});
|
|
expect(groups[0].scanRunIds).toEqual(['run-cis', 'run-ping', 'run-purple', 'run-maester']);
|
|
expect(groups[0].gpoBackups).toHaveLength(1);
|
|
});
|
|
|
|
it('uses an explicit timeslot date instead of the later upload timestamp', () => {
|
|
const slot = assessment('slot-1', '2026-07-22T12:00:00.000Z');
|
|
const scanRun = run('run-late-upload', slot.id);
|
|
const groups = groupProjectImportFiles([
|
|
file({ id: 'late', scanRunId: scanRun.id, lastModifiedAt: '2026-08-04T08:00:00.000Z' })
|
|
], [slot], [scanRun], [{ ...gpo, assessmentId: slot.id }]);
|
|
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
id: 'timeslot-2026-07-22',
|
|
assessmentIds: ['slot-1'],
|
|
scanRunIds: ['run-late-upload'],
|
|
sourceCount: 2
|
|
});
|
|
});
|
|
}); |