from __future__ import annotations import json import unittest from datetime import UTC, datetime from sqlalchemy import create_engine from sqlalchemy.orm import Session from govoplan_core.core.dsar import DsarProvider, DsarSubjectRef from govoplan_core.db.base import Base from govoplan_core.privacy.dsar_workflow import ( create_data_subject_request, search_data_subject_request, ) from govoplan_connectors.backend.db.models import ( ConnectorConfiguration, ConnectorDefinition, ConnectorDefinitionRevision, ConnectorKnowledgeProfile, ConnectorKnowledgeSyncRun, ConnectorSanctionsAcquisitionRun, ConnectorSimulationRun, ConnectorTabularSource, ) from govoplan_connectors.backend.dsar_provider import ( CONNECTORS_DSAR_CAPABILITY, ConnectorsDsarProvider, ) from govoplan_connectors.backend.manifest import manifest NOW = datetime(2026, 8, 22, 9, 0, tzinfo=UTC) class _Registry: def __init__(self, provider: ConnectorsDsarProvider) -> None: self.provider = provider def capability_names(self): return (CONNECTORS_DSAR_CAPABILITY,) def capability_owner(self, name): if name != CONNECTORS_DSAR_CAPABILITY: raise KeyError(name) return "connectors" def tenant_entitlement_resolver(self): class _Resolver: @staticmethod def resolve(session, tenant_id): del session, tenant_id return type("State", (), {"effective_modules": ("connectors",)})() return _Resolver() def require_tenant_capability(self, name, session, **kwargs): del session, kwargs if name != CONNECTORS_DSAR_CAPABILITY: raise KeyError(name) return self.provider def manifests(self): return (type("Manifest", (), {"id": "connectors"})(),) class ConnectorsDsarProviderTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(self.engine) self.session = Session(self.engine) self.provider = ConnectorsDsarProvider() self.assertIsInstance(self.provider, DsarProvider) self._seed() self.session.commit() def tearDown(self) -> None: self.session.close() self.engine.dispose() def _seed(self) -> None: self.session.add_all( ( ConnectorTabularSource( id="source-1", tenant_id="tenant-1", provider="snapshot", source_name="people", name="People import", description="Do not export this business description", status="active", schema_version=1, schema_=[{"name": "email"}], rows=[{"email": "third-party@example.test"}], fingerprint="source-fingerprint-do-not-export", row_count=1, byte_count=100, metadata_={"secret": "source-metadata-do-not-export"}, created_by="account-1", updated_by="account-1", created_at=NOW, updated_at=NOW, ), ConnectorTabularSource( id="source-other", tenant_id="tenant-2", provider="snapshot", source_name="other", name="Other tenant", status="active", schema_version=1, schema_=[], rows=[], fingerprint="other-fingerprint", row_count=0, byte_count=0, metadata_={}, created_by="account-1", updated_by="account-1", ), ConnectorSanctionsAcquisitionRun( id="acquisition-1", tenant_id="tenant-1", provider_id="un", source_id="consolidated", status="complete", attempt_count=1, request_evidence={"secret": "request-evidence-do-not-export"}, response_evidence={"secret": "response-evidence-do-not-export"}, started_at=NOW, finished_at=NOW, snapshot_id="snapshot-1", error="transport-detail-do-not-export", created_by="account-1", created_at=NOW, updated_at=NOW, ), ConnectorDefinition( id="definition-1", tenant_id="tenant-1", definition_key="address-reader", name="Address reader", description="Definition detail", status="active", current_revision=1, source_package="package-secret-do-not-export", local_definition=True, created_at=NOW, updated_at=NOW, ), ConnectorDefinitionRevision( id="definition-revision-1", definition_id="definition-1", revision=1, specification={"secret": "specification-do-not-export"}, definition_hash="definition-hash-do-not-export", origin="local", package_ref="package-ref-do-not-export", created_by="account-1", created_at=NOW, updated_at=NOW, ), ConnectorConfiguration( id="configuration-1", tenant_id="tenant-1", definition_id="definition-1", name="Production addresses", status="active", endpoint_url="https://secret.example.test/api", credential_ref="vault://secret-do-not-export", base_definition_revision=1, local_overrides={"secret": "override-do-not-export"}, protected_paths=["secret"], effective_configuration={"secret": "effective-do-not-export"}, effective_hash="configuration-hash-do-not-export", resource_revision=2, ambiguity_policy="manual_review", updated_by="account-1", created_at=NOW, updated_at=NOW, ), ConnectorSimulationRun( id="simulation-1", tenant_id="tenant-1", configuration_id="configuration-1", mode="dry_run", idempotency_key="simulation-idempotency-do-not-export", request_hash="simulation-request-hash-do-not-export", status="complete", review_state="approved", definition_revision=1, configuration_revision=2, configuration_hash="simulation-config-hash-do-not-export", input_hash="simulation-input-hash-do-not-export", summary={"secret": "summary-do-not-export"}, effects=[{"secret": "effect-do-not-export"}], diagnostics=[{"secret": "diagnostic-do-not-export"}], provenance={"secret": "provenance-do-not-export"}, created_by="account-1", reviewed_by="account-1", reviewed_at=NOW, review_reason="review-reason-do-not-export", created_at=NOW, updated_at=NOW, ), ConnectorKnowledgeProfile( id="knowledge-profile-1", tenant_id="tenant-1", configuration_id="configuration-1", status="active", product="bluespice", desired_maturity="migrate", discovered_maturity="migrate", source_authority_mode="external_mirror", default_visibility="restricted", default_acl_tokens=["group:secret-do-not-export"], namespace_mappings=[{"secret": "mapping-do-not-export"}], capabilities=["read", "synchronize", "migrate"], discovery_evidence={"secret": "discovery-do-not-export"}, health_details={"secret": "health-do-not-export"}, updated_by="account-1", created_at=NOW, updated_at=NOW, ), ConnectorKnowledgeSyncRun( id="knowledge-run-1", tenant_id="tenant-1", profile_id="knowledge-profile-1", mode="delta", idempotency_key="knowledge-idempotency-do-not-export", request_hash="knowledge-request-hash-do-not-export", status="completed", counts={"update": 1}, effects=[{"secret": "knowledge-effect-do-not-export"}], diagnostics=[{"secret": "knowledge-diagnostic-do-not-export"}], provenance={"secret": "knowledge-provenance-do-not-export"}, created_by="account-1", started_at=NOW, finished_at=NOW, created_at=NOW, updated_at=NOW, ), ) ) @staticmethod def _subject() -> DsarSubjectRef: return DsarSubjectRef(account_id="account-1") def test_search_exports_minimized_operator_attribution(self) -> None: records = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=self._subject() ) self.assertEqual( { "source_actor_attribution", "acquisition_actor_attribution", "definition_actor_attribution", "configuration_actor_attribution", "simulation_actor_attribution", "knowledge_profile_actor_attribution", "knowledge_run_actor_attribution", }, {record.resource_type for record in records}, ) exported = json.dumps([record.to_dict() for record in records]) for excluded in ( "third-party@example.test", "source-fingerprint-do-not-export", "source-metadata-do-not-export", "request-evidence-do-not-export", "response-evidence-do-not-export", "transport-detail-do-not-export", "specification-do-not-export", "definition-hash-do-not-export", "package-ref-do-not-export", "https://secret.example.test/api", "vault://secret-do-not-export", "override-do-not-export", "effective-do-not-export", "configuration-hash-do-not-export", "simulation-idempotency-do-not-export", "simulation-request-hash-do-not-export", "simulation-config-hash-do-not-export", "simulation-input-hash-do-not-export", "summary-do-not-export", "effect-do-not-export", "diagnostic-do-not-export", "provenance-do-not-export", "review-reason-do-not-export", "secret-do-not-export", "mapping-do-not-export", "discovery-do-not-export", "health-do-not-export", "knowledge-idempotency-do-not-export", "knowledge-request-hash-do-not-export", "knowledge-effect-do-not-export", "knowledge-diagnostic-do-not-export", "knowledge-provenance-do-not-export", ): self.assertNotIn(excluded, exported) def test_requires_exact_account_and_enforces_tenant(self) -> None: email_only = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=DsarSubjectRef(email="operator@example.test"), ) conflict = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=DsarSubjectRef( account_id="account-1", external_references={"connectors.account": "account-other"}, ), ) records = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=self._subject() ) self.assertEqual((), email_only) self.assertEqual((), conflict) self.assertNotIn("source-other", {record.resource_id for record in records}) def test_object_narrowing_does_not_broaden_the_search(self) -> None: records = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=DsarSubjectRef( account_id="account-1", external_references={"connectors.simulation": "simulation-1"}, ), ) self.assertEqual( [("simulation_actor_attribution", "simulation-1")], [(record.resource_type, record.resource_id) for record in records], ) def test_erasure_retains_external_operation_evidence(self) -> None: records = self.provider.search_subject( self.session, tenant_id="tenant-1", subject=self._subject() ) actions = self.provider.plan_erasure( self.session, tenant_id="tenant-1", subject=self._subject(), records=records, ) self.assertTrue(actions) self.assertTrue( all(action.kind == "retain" and not action.executable for action in actions) ) results = self.provider.execute_erasure( self.session, tenant_id="tenant-1", subject=self._subject(), actions=actions, request_id="dsar-connectors-1", ) self.assertTrue(all(result.status == "blocked" for result in results)) def test_manifest_and_core_workflow_discover_provider(self) -> None: self.assertIn(CONNECTORS_DSAR_CAPABILITY, manifest.capability_factories) self.assertIn( "connectors.data-subject-requests", {topic.id for topic in manifest.documentation}, ) row = create_data_subject_request( self.session, tenant_id="tenant-1", reference="DSAR-CONNECTORS-1", request_kind="access", subject=self._subject(), purpose="Connector attribution access request", legal_basis=None, due_at=None, requested_by_account_id="operator-1", ) search_data_subject_request( self.session, registry=_Registry(self.provider), row=row, expected_revision=row.resource_revision, ) self.assertEqual("searched", row.status) self.assertEqual(7, row.search_result["record_count"]) if __name__ == "__main__": unittest.main()