from __future__ import annotations 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, SanctionsSnapshotPayload, SanctionsSnapshotReference, ) from govoplan_core.db.base import Base, utcnow from govoplan_risk_compliance.backend.db.models import ( RiskSanctionsAddress, RiskSanctionsAlias, RiskSanctionsDate, RiskSanctionsEntry, RiskSanctionsIdentifier, RiskSanctionsListSnapshot, RiskScreeningCandidate, RiskScreeningDisposition, RiskScreeningException, RiskScreeningRun, RiskScreeningSubjectSnapshot, ) 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, run_screening, ) 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 = ( 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"] ) 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) 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_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(), ) 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()