import { Pool } from 'pg'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { runDatabaseMigrations } from '../../server/postgres/migration-runner'; import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository'; import type { Assessment, AssessmentProject, ScanRun } from '../../src/domain/canonical-model'; import type { ImportStagingData } from '../../src/persistence/assessor-repository'; import { RepositoryConflictError } from '../../src/persistence/repository-errors'; import { riskCommand } from '../domain/risk-acceptance-fixture'; const connectionString = process.env.ASSESSOR_TEST_DATABASE_URL; const describePostgres = connectionString ? describe : describe.skip; const timestamp = '2026-09-21T08:00:00.000Z'; const project: AssessmentProject = { id: 'pg-scoped-risk-project', name: 'Scoped risk integration', createdAt: timestamp, updatedAt: timestamp, attributes: {} }; const assessment: Assessment = { id: 'pg-scoped-risk-assessment', projectId: project.id, name: 'Scoped risk assessment', createdAt: timestamp, updatedAt: timestamp }; const scanRun: ScanRun = { id: 'pg-scoped-risk-run', assessmentId: assessment.id, sequence: 1, label: 'Defender run', tool: { id: 'defender-for-iot-vulnerabilities', name: 'Microsoft Defender for IoT', version: '1.2.0' }, importedAt: timestamp }; const confidence = { level: 'high' as const, score: 1, reasons: ['synthetic'] }; const provenance = { decisionSource: 'source-native' as const, confidence, mappingVersion: '1.0.0' }; function data(): ImportStagingData { const observations = (cve: string, rowIndex: number) => [{ name: 'Device A', ip: '192.0.2.10', site: 'Synthetic site', sensor: 'sensor-a', cve, severity: 'High', description: 'Synthetic', additionalSourceFields: { 'Device ID': 'DEV-A' }, source: { tableIndex: 0, rowIndex } }]; return { importFiles: [{ id: 'pg-scoped-risk-file', scanRunId: scanRun.id, fileName: 'defender.csv', format: 'CSV', sizeBytes: 100, importedAt: timestamp, detectedTool: scanRun.tool, detection: confidence, processedWith: { parserId: 'csv', parserVersion: '1.0.0', profileId: scanRun.tool.id, profileVersion: '1.2.0', mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking', reimportRequired: true }, sourceArtifact: { kind: 'not-retained', reason: 'Synthetic integration test' }, sha256: '9'.repeat(64) }], sourceTables: [{ id: 'pg-scoped-risk-table', importFileId: 'pg-scoped-risk-file', name: 'Vulnerabilities', index: 0, rowCount: 2, columnCount: 2, attributes: {} }], findingDefinitions: ['CVE-2026-1001', 'CVE-2026-1002'].map((cve, index) => ({ id: `pg-scoped-risk-definition-${index + 1}`, canonicalKey: `${scanRun.tool.id}:${cve}`, title: cve, frameworkReferences: [], createdAt: timestamp, updatedAt: timestamp })), findingOccurrences: ['CVE-2026-1001', 'CVE-2026-1002'].map((cve, index) => ({ id: `pg-scoped-risk-occurrence-${index + 1}`, definitionId: `pg-scoped-risk-definition-${index + 1}`, scanRunId: scanRun.id, importFileId: 'pg-scoped-risk-file', sourceTableId: 'pg-scoped-risk-table', sourceIdentity: { tool: scanRun.tool, originalRuleId: cve, canonicalRuleId: cve, originalTitle: cve }, sourcePosition: { rowIndex: index + 1 }, originalFields: [{ key: 'source-observations', label: 'Source Observations', columnIndex: 0, value: observations(cve, index + 1) }], fieldMappings: [], severity: { originalValue: 'High', normalizedValue: 'high' as const, provenance }, status: { originalValue: 'Open', normalizedValue: 'open' as const, provenance } })) }; } describePostgres('PostgreSQL scoped risk acceptance persistence', () => { let pool: Pool; let repository: PostgresAssessorRepository; beforeAll(async () => { pool = new Pool({ connectionString, max: 2, connectionTimeoutMillis: 5000 }); await runDatabaseMigrations(pool); await pool.query('DELETE FROM assessor_projects WHERE id=$1', [project.id]); repository = await PostgresAssessorRepository.create(pool); }, 30000); afterAll(async () => { if (pool) { await pool.query('DELETE FROM assessor_projects WHERE id=$1', [project.id]); await pool.end(); } }); it('persists, conflicts, revokes, exports, and cascades a grouped decision', async () => { await repository.saveProject(project); await repository.saveAssessment(assessment); const session = await repository.beginImport({ sessionId: 'pg-scoped-risk-session', projectId: project.id, assessmentId: assessment.id, scanRun }); await session.stage(data()); await session.commit(); const accepted = await repository.recordScopedRiskAcceptance(project.id, { target: { kind: 'device-snapshot', scanRunIds: [scanRun.id], deviceKey: 'device-id:dev-a' }, command: riskCommand('accept'), expectedRevision: 0 }); expect(accepted.target.members.map(item => item.cve)).toEqual(['CVE-2026-1001', 'CVE-2026-1002']); await expect(repository.recordScopedRiskAcceptance(project.id, { acceptanceId: accepted.id, command: riskCommand('draft'), expectedRevision: 0 })) .rejects.toBeInstanceOf(RepositoryConflictError); const revoked = await repository.recordScopedRiskAcceptance(project.id, { acceptanceId: accepted.id, command: riskCommand('revoke'), expectedRevision: 1 }); expect(revoked.revisions.map(item => item.action)).toEqual(['accept', 'revoke']); await expect(repository.listScopedRiskAcceptances(project.id)).resolves.toEqual([expect.objectContaining({ id: accepted.id, revision: 2 })]); await expect(repository.exportProject(project.id)).resolves.toMatchObject({ scopedRiskAcceptances: [expect.objectContaining({ id: accepted.id })] }); await repository.deleteProject(project.id, { projectId: project.id, cascade: true }); await expect(pool.query('SELECT count(*)::integer AS count FROM assessor_scoped_risk_acceptances WHERE project_id=$1', [project.id])) .resolves.toMatchObject({ rows: [{ count: 0 }] }); }, 30000); });