100 lines
7.5 KiB
TypeScript
100 lines
7.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { clientInformationFixture } from '../client-information-fixture';
|
|
import { clientInformationReadPortFrom } from '../../src/persistence/client-information-read-port';
|
|
import { selectClientInformation } from '../../src/domain/client-information';
|
|
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
|
|
import type { ScanRun } from '../../src/domain/canonical-model';
|
|
|
|
describe('published Client Information read port', () => {
|
|
it('filters exact properties, subjects, areas and sources, preserving conflicting original values and provenance', async () => {
|
|
const snapshot = clientInformationFixture();
|
|
const original = structuredClone(snapshot);
|
|
const reader = clientInformationReadPortFrom({ getEnvironmentSnapshot: async () => snapshot }, async () => 17);
|
|
const result = await reader.queryClientInformation({ projectId: 'client-1', assessmentId: 'slot-1', scanRunIds: ['run-1'],
|
|
properties: ['domain.fqdn'], subjectIds: ['object-1'], areas: ['directory'], toolIds: ['synthetic'] });
|
|
expect(result).toMatchObject({ contractVersion: '1.1.0', purpose: 'internal-analysis', repositoryRevision: 17, totalObservations: 2 });
|
|
expect(result.observations.map(item => item.originalValue).sort()).toEqual(['other.invalid', 'synthetic.invalid']);
|
|
result.observations[0].originalValue = 'consumer mutation';
|
|
expect(snapshot).toEqual(original);
|
|
expect(Object.keys(reader)).toEqual(['queryClientInformation']);
|
|
expect(Object.isFrozen(reader)).toBe(true);
|
|
});
|
|
|
|
it('keeps false, zero, restrictions and full original tables; paginates without changing totals', () => {
|
|
const snapshot = clientInformationFixture();
|
|
const all = selectClientInformation(snapshot, { projectId: 'client-1' }, 1);
|
|
expect(all.observations.map(item => item.originalValue)).toEqual(expect.arrayContaining([false, 0]));
|
|
const restricted = selectClientInformation(snapshot, { projectId: 'client-1', states: ['access-denied'] }, 1);
|
|
expect(restricted.totalObservations).toBe(1);
|
|
expect(restricted.coverage.restricted).toBe(1);
|
|
const first = selectClientInformation(snapshot, { projectId: 'client-1', limit: 2 }, 1);
|
|
const next = selectClientInformation(snapshot, { projectId: 'client-1', limit: 2, offset: 2 }, 1);
|
|
expect(first.totalObservations).toBe(6);
|
|
expect(first.observations).toHaveLength(2);
|
|
expect(next.observations.some(item => first.observations.some(previous => previous.id === item.id))).toBe(false);
|
|
expect(all.sections).toEqual(snapshot.sections);
|
|
expect(selectClientInformation(snapshot, { projectId: 'client-1', properties: ['missing'] }, 1).totalObservations).toBe(0);
|
|
});
|
|
|
|
it('retries changed revisions, rejects cross-scope data and fails after bounded unstable reads', async () => {
|
|
const snapshot = clientInformationFixture();
|
|
const revision = vi.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(2).mockResolvedValue(2);
|
|
const getEnvironmentSnapshot = vi.fn(async () => snapshot);
|
|
const reader = clientInformationReadPortFrom({ getEnvironmentSnapshot }, revision);
|
|
expect((await reader.queryClientInformation({ projectId: 'client-1' })).repositoryRevision).toBe(2);
|
|
expect(getEnvironmentSnapshot).toHaveBeenCalledTimes(2);
|
|
await expect(reader.queryClientInformation({ projectId: 'foreign' })).rejects.toThrow('different scope');
|
|
await expect(reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['foreign'] })).rejects.toThrow('different scope');
|
|
let counter = 0;
|
|
await expect(clientInformationReadPortFrom({ getEnvironmentSnapshot }, async () => counter++).queryClientInformation({ projectId: 'client-1' }))
|
|
.rejects.toThrow('changed during loading');
|
|
for (const query of [{ areas: ['typo'] }, { limit: 201 }, { offset: -1 }, { states: ['passed'] }]) {
|
|
await expect(reader.queryClientInformation({ projectId: 'client-1', ...query } as never)).rejects.toThrow();
|
|
}
|
|
});
|
|
|
|
it('preserves exact nested source links beyond a section page without guessing duplicate or foreign-file targets', () => {
|
|
const snapshot = clientInformationFixture();
|
|
const parent = snapshot.sections[0];
|
|
parent.detailLinks = [{ targetId: 'child', label: 'Native details' }, { targetId: 'ambiguous', label: 'Ambiguous' }, { targetId: 'foreign', label: 'Foreign' }];
|
|
snapshot.sections.push(
|
|
{ ...parent, id: 'child-id', sectionKey: 'child', ordinal: 2, detailLinks: [{ targetId: 'nested', label: 'Nested' }] },
|
|
{ ...parent, id: 'nested-id', sectionKey: 'nested', ordinal: 3, detailLinks: [] },
|
|
{ ...parent, id: 'ambiguous-1', sectionKey: 'ambiguous', ordinal: 4, detailLinks: [] },
|
|
{ ...parent, id: 'ambiguous-2', sectionKey: 'ambiguous', ordinal: 5, detailLinks: [] },
|
|
{ ...parent, id: 'foreign-id', sectionKey: 'foreign', importFileId: 'file-2', ordinal: 6, detailLinks: [] });
|
|
const result = selectClientInformation(snapshot, { projectId: 'client-1', sectionLimit: 1 }, 1);
|
|
expect(result.totalSections).toBe(6);
|
|
expect(result.sections.map(item => item.id)).toEqual(['section-1', 'child-id', 'nested-id']);
|
|
expect(result.linkedSectionIds).toEqual(['child-id', 'nested-id']);
|
|
});
|
|
|
|
it('queries real repository history explicitly and rejects another client or assessment without writes', async () => {
|
|
const repository = new LocalAssessorRepository();
|
|
const stamp = '2026-10-01T10:00:00Z';
|
|
await repository.saveProject({ id: 'client-1', name: 'Synthetic', createdAt: stamp, updatedAt: stamp, attributes: {} });
|
|
await repository.saveAssessment({ id: 'slot-1', projectId: 'client-1', name: 'Synthetic slot', createdAt: stamp, updatedAt: stamp });
|
|
const fixture = clientInformationFixture();
|
|
for (const [index, id] of ['old', 'run-1'].entries()) {
|
|
const run: ScanRun = { id, assessmentId: 'slot-1', sequence: index + 1, label: id,
|
|
importedAt: index ? '2026-10-02T10:00:00Z' : stamp, tool: { id: 'synthetic', name: 'Synthetic' } };
|
|
const session = await repository.beginImport({ sessionId: id, projectId: 'client-1', assessmentId: 'slot-1', scanRun: run });
|
|
await session.stage({ importFiles: [{ id: `file-${id}`, scanRunId: id, fileName: 'synthetic.csv', format: 'CSV', sizeBytes: 1, sha256: 'a'.repeat(64),
|
|
importedAt: stamp, detectedTool: run.tool, detection: { level: 'high', score: 1, reasons: ['synthetic'] },
|
|
processedWith: { parserId: 'csv', parserVersion: '1', profileId: 'generic', profileVersion: '1', mappingVersion: '1' },
|
|
sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }],
|
|
environmentObservations: [{ ...fixture.observations[0], id: `obs-${id}`, scanRunId: id, importFileId: `file-${id}`, sectionKey: undefined }] });
|
|
await session.commit();
|
|
}
|
|
await repository.adoptScanRunReplacement('client-1', 'old', 'run-1', stamp);
|
|
const revision = repository.revision;
|
|
const reader = clientInformationReadPortFrom(repository, async () => repository.revision);
|
|
expect((await reader.queryClientInformation({ projectId: 'client-1' })).scanRunIds).toEqual(['run-1']);
|
|
expect((await reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['old'], includeSuperseded: true })).observations[0].id).toBe('obs-old');
|
|
await expect(reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['old'] })).rejects.toThrow();
|
|
await expect(reader.queryClientInformation({ projectId: 'client-1', assessmentId: 'foreign' })).rejects.toThrow();
|
|
await expect(reader.queryClientInformation({ projectId: 'foreign' })).rejects.toThrow();
|
|
expect(repository.revision).toBe(revision);
|
|
});
|
|
});
|