Assessor_2.0/tests/persistence/client-information.test.ts

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