from __future__ import annotations from dataclasses import replace from datetime import timedelta from types import SimpleNamespace import hashlib import unittest from sqlalchemy import create_engine from sqlalchemy.orm import Session from govoplan_core.auth import ApiPrincipal from govoplan_core.core.access import PrincipalRef from govoplan_core.core.sanctions import ( CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS, SanctionsScreeningFreshnessRequest, SanctionsScreeningPolicy, SanctionsScreeningRequest, SanctionsScreeningSubject, SanctionsSnapshotPayload, SanctionsSnapshotReference, ) from govoplan_core.db.base import Base, utcnow from govoplan_risk_compliance.backend.db.models import ( RiskAssuranceEdge, RiskAssuranceNode, RiskSanctionsAddress, RiskSanctionsAlias, RiskSanctionsDate, RiskSanctionsEntry, RiskSanctionsIdentifier, RiskSanctionsListSnapshot, RiskScreeningCandidate, RiskScreeningDisposition, RiskScreeningException, RiskScreeningRun, RiskScreeningSubjectSnapshot, ) from govoplan_risk_compliance.backend.capabilities import ( RiskComplianceSanctionsScreeningProvider, ) from govoplan_risk_compliance.backend.permissions import ( SANCTIONS_ADMIN_SCOPE, SANCTIONS_READ_SCOPE, SANCTIONS_REVIEW_SCOPE, SANCTIONS_SCREEN_SCOPE, ) from govoplan_risk_compliance.backend.review import ( DispositionInput, record_disposition, ) from govoplan_risk_compliance.backend.sanctions_catalog import ( RiskSanctionsAccessError, RiskSanctionsConflictError, import_connector_snapshot, ) from govoplan_risk_compliance.backend.screening import ( MATCHER_VERSION, ScreeningPolicy, ScreeningSubject, assess_screening_freshness, list_rescreening_requirements, run_screening, screening_evidence_ref, ) UN_XML = b""" 42 1 Example Person Example programme QI.42.26 2026-01-02 Good Example Alias Passport P-123 456 1980-01-02 Example City Example Country 84 1 Example Trading Company QE.84.26 2026-01-03 """ TABLES = ( RiskAssuranceNode.__table__, RiskAssuranceEdge.__table__, RiskSanctionsListSnapshot.__table__, RiskSanctionsEntry.__table__, RiskSanctionsAlias.__table__, RiskSanctionsIdentifier.__table__, RiskSanctionsDate.__table__, RiskSanctionsAddress.__table__, RiskScreeningSubjectSnapshot.__table__, RiskScreeningRun.__table__, RiskScreeningCandidate.__table__, RiskScreeningDisposition.__table__, RiskScreeningException.__table__, ) def principal( account_id: str = "account-1", *, scopes: tuple[str, ...] = ( SANCTIONS_READ_SCOPE, SANCTIONS_SCREEN_SCOPE, SANCTIONS_REVIEW_SCOPE, SANCTIONS_ADMIN_SCOPE, ), ) -> ApiPrincipal: return ApiPrincipal( principal=PrincipalRef( account_id=account_id, membership_id=f"membership-{account_id}", tenant_id="tenant-1", scopes=frozenset(scopes), ), account=SimpleNamespace(id=account_id), user=SimpleNamespace(id=account_id), ) class _SnapshotProvider: def __init__(self) -> None: digest = hashlib.sha256(UN_XML).hexdigest() self.snapshot = SanctionsSnapshotReference( ref="sanctions-snapshot:fixture", tenant_id="tenant-1", provider_id="synthetic.un_fixture", publisher="United Nations Security Council fixture", jurisdiction="UN", list_type="consolidated_sanctions", source_id="synthetic-un-v1", source_version="fixture-v1", publication_at=utcnow(), effective_at=utcnow(), acquired_at=utcnow(), content_type="application/xml", byte_count=len(UN_XML), sha256=digest, parser_version="unsc-xml-v1", raw_evidence_ref="connector-evidence:fixture", connector_run_id="run-fixture", ) def list_snapshots(self, session, principal, *, limit=100): del session, principal, limit return (self.snapshot,) def get_snapshot(self, session, principal, *, snapshot_ref): del session, principal return self.snapshot if snapshot_ref == self.snapshot.ref else None def read_snapshot(self, session, principal, *, snapshot_ref): del session, principal if snapshot_ref != self.snapshot.ref: raise ValueError("not found") return SanctionsSnapshotPayload( snapshot=self.snapshot, content=UN_XML, ) class _Registry: def __init__(self, provider) -> None: self.provider = provider def has_capability(self, name: str) -> bool: return name == CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS def capability(self, name: str): return self.provider if self.has_capability(name) else None class SanctionsScreeningTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine, tables=TABLES) self.session = Session(self.engine) self.provider = _SnapshotProvider() self.registry = _Registry(self.provider) self.list_snapshot, _ = import_connector_snapshot( self.session, principal(), registry=self.registry, connector_snapshot_ref=self.provider.snapshot.ref, ) self.session.commit() def tearDown(self) -> None: self.session.close() self.engine.dispose() def test_import_is_immutable_idempotent_and_normalized(self) -> None: second, created = import_connector_snapshot( self.session, principal(), registry=self.registry, connector_snapshot_ref=self.provider.snapshot.ref, ) self.assertFalse(created) self.assertEqual(self.list_snapshot.id, second.id) self.assertEqual(2, second.entry_count) person = self.session.query(RiskSanctionsEntry).filter_by( source_entry_id="QI.42.26" ).one() self.assertEqual("example person", person.normalized_name) self.assertEqual("example alias", person.aliases[0].normalized_name) self.assertEqual("p123456", person.identifiers[0].normalized_value) self.assertEqual( "INDIVIDUALS/INDIVIDUAL[1]", person.raw_evidence_locator, ) def test_screening_is_versioned_explainable_and_idempotent(self) -> None: item, created = run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="screening-1", subject=ScreeningSubject( subject_type="person", primary_name="Exampel Person", ), ) self.assertTrue(created) self.assertEqual("potential", item.outcome) self.assertEqual(MATCHER_VERSION, item.matcher_version) self.assertEqual(1, item.candidate_count) self.assertEqual("name_fuzzy", item.candidates[0].match_kind) self.assertEqual("pending", item.candidates[0].review_status) self.assertFalse( item.candidates[0].evidence[0]["auto_confirmed"] ) self.assertEqual( 6, self.session.query(RiskAssuranceNode) .filter( RiskAssuranceNode.tenant_id == "tenant-1", RiskAssuranceNode.superseded_at.is_(None), ) .count(), ) self.assertEqual( 5, self.session.query(RiskAssuranceEdge) .filter( RiskAssuranceEdge.tenant_id == "tenant-1", RiskAssuranceEdge.superseded_at.is_(None), ) .count(), ) item_id = item.id self.session.commit() self.session.expire_all() replay, replay_created = run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="screening-1", subject=ScreeningSubject( subject_type="person", primary_name="Exampel Person", ), ) self.assertFalse(replay_created) self.assertEqual(item_id, replay.id) self.assertEqual(6, self.session.query(RiskAssuranceNode).count()) self.assertEqual(5, self.session.query(RiskAssuranceEdge).count()) with self.assertRaises(RiskSanctionsConflictError): run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="screening-1", subject=ScreeningSubject( subject_type="person", primary_name="Different Person", ), ) def test_identifier_match_is_exact(self) -> None: item, _ = run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="identifier-1", subject=ScreeningSubject( subject_type="person", identifiers=( { "type": "passport", "value": "P123-456", }, ), ), ) self.assertEqual("identifier_exact", item.candidates[0].match_kind) self.assertEqual(100, item.candidates[0].score) def test_stale_list_has_explicit_outcome(self) -> None: self.list_snapshot.acquired_at = utcnow() - timedelta(days=30) item, _ = run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="stale-1", subject=ScreeningSubject( subject_type="person", primary_name="Example Person", ), policy=ScreeningPolicy(max_snapshot_age_days=7), ) self.assertEqual("stale", item.outcome) self.assertEqual(0, item.candidate_count) def test_freshness_reconciles_source_subject_matcher_and_policy(self) -> None: item, _ = run_screening( self.session, principal(), list_snapshot_id=self.list_snapshot.id, idempotency_key="freshness-1", subject=ScreeningSubject( subject_type="person", primary_name="No Match", subject_ref="person-1", ), ) item.matcher_version = "sanctions-matcher-legacy" self.provider.snapshot = replace( self.provider.snapshot, ref="sanctions-snapshot:fixture-v2", source_version="fixture-v2", acquired_at=utcnow(), connector_run_id="run-fixture-v2", ) current_snapshot, created = import_connector_snapshot( self.session, principal(), registry=self.registry, connector_snapshot_ref=self.provider.snapshot.ref, ) self.assertTrue(created) assessment = assess_screening_freshness( self.session, principal(), evidence_ref=screening_evidence_ref(item.id), current_subject=ScreeningSubject( subject_type="person", primary_name="Changed Subject", subject_ref="person-1", ), policy=ScreeningPolicy(fuzzy_threshold=0.9), failure_policy="degraded", ) self.assertFalse(assessment.fresh) self.assertEqual(current_snapshot.id, assessment.current_list_snapshot_id) self.assertEqual("degraded", assessment.gate_decision) self.assertIn("source_snapshot_changed", assessment.reasons) self.assertIn("subject_changed", assessment.reasons) self.assertIn("matcher_version_changed", assessment.reasons) self.assertIn("policy_changed", assessment.reasons) requirements = list_rescreening_requirements( self.session, principal(), ) self.assertEqual([item.id], [value.run.id for value in requirements]) def test_versioned_capability_returns_stable_gate_evidence(self) -> None: provider = RiskComplianceSanctionsScreeningProvider() request = SanctionsScreeningRequest( list_snapshot_id=self.list_snapshot.id, idempotency_key="capability-1", subject=SanctionsScreeningSubject( subject_type="entity", primary_name="No Match Company", subject_ref="entity-1", ), policy=SanctionsScreeningPolicy( failure_policy="review", ), ) result = provider.request_screening( self.session, principal(), request, ) replay = provider.request_screening( self.session, principal(), request, ) changed = provider.check_freshness( self.session, principal(), SanctionsScreeningFreshnessRequest( evidence_ref=result.evidence.ref, current_subject=SanctionsScreeningSubject( subject_type="entity", primary_name="Changed Company", subject_ref="entity-1", ), expected_list_snapshot_id=self.list_snapshot.id, policy=request.policy, ), ) self.assertTrue(result.created) self.assertFalse(replay.created) self.assertEqual(result.evidence.ref, replay.evidence.ref) self.assertEqual("allow", result.freshness.gate_decision) self.assertFalse(changed.fresh) self.assertEqual("review", changed.gate_decision) self.assertIn("subject_changed", changed.reasons) def test_review_is_separated_and_exception_requires_review(self) -> None: submitter = principal( scopes=( SANCTIONS_READ_SCOPE, SANCTIONS_SCREEN_SCOPE, ) ) item, _ = run_screening( self.session, submitter, list_snapshot_id=self.list_snapshot.id, idempotency_key="review-1", subject=ScreeningSubject( subject_type="person", primary_name="Example Person", ), ) candidate = item.candidates[0] with self.assertRaises(RiskSanctionsAccessError): record_disposition( self.session, principal( scopes=(SANCTIONS_REVIEW_SCOPE,) ), candidate_id=candidate.id, disposition=DispositionInput( decision="false_positive", reason="Independent evidence excludes this subject.", ), ) reviewer = principal( "account-2", scopes=(SANCTIONS_REVIEW_SCOPE,), ) reviewed, disposition = record_disposition( self.session, reviewer, candidate_id=candidate.id, disposition=DispositionInput( decision="false_positive", reason="Independent evidence excludes this subject.", exception_scope="subject_entry", expires_at=utcnow() + timedelta(days=30), ), ) self.assertEqual("false_positive", reviewed.review_status) self.assertEqual("independent", disposition.separation_status) self.assertEqual( 1, self.session.query(RiskScreeningException).count(), ) expired = assess_screening_freshness( self.session, principal(), evidence_ref=screening_evidence_ref(item.id), expected_list_snapshot_id=self.list_snapshot.id, failure_policy="review", now=disposition.expires_at + timedelta(seconds=1), ) self.assertIn("disposition_expired", expired.reasons) self.assertEqual("review", expired.gate_decision) repeated, _ = run_screening( self.session, submitter, list_snapshot_id=self.list_snapshot.id, idempotency_key="review-2", subject=ScreeningSubject( subject_type="person", primary_name="Example Person", ), ) self.assertEqual( "exception_review", repeated.candidates[0].review_status, ) self.assertEqual( "prior_exception", repeated.candidates[0].evidence[-1]["kind"], ) if __name__ == "__main__": unittest.main()