from __future__ import annotations from types import SimpleNamespace 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.search import ( SearchAuthorizationRequest, SearchBackfillRequest, SearchResourceReference, ) from govoplan_core.db.base import Base from govoplan_datasources.backend.db.models import DatasourceRecord from govoplan_datasources.backend.manifest import get_manifest from govoplan_datasources.backend.search_source import ( DatasourcesSearchSource, PROVIDER_ID, RESOURCE_TYPE, ) from govoplan_datasources.backend.service import ADMIN_SCOPE, CATALOGUE_READ_SCOPE class DatasourcesSearchSourceTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine("sqlite://") Base.metadata.create_all( self.engine, tables=(DatasourceRecord.__table__,), ) self.session = Session(self.engine) self.session.add_all( ( _datasource("source-1", "tenant-1", "Monthly source"), _datasource("source-other", "tenant-2", "Other tenant"), ) ) self.session.commit() self.source = DatasourcesSearchSource() def tearDown(self) -> None: self.session.close() self.engine.dispose() def test_manifest_registers_optional_search_source(self) -> None: manifest = get_manifest() self.assertIn("search", manifest.optional_dependencies) self.assertIn( PROVIDER_ID, {registration.id for registration in manifest.search_sources}, ) def test_backfill_is_tenant_bound_and_excludes_protected_payloads(self) -> None: page = self.source.backfill( self.session, request=SearchBackfillRequest( tenant_id="tenant-1", provider_id=PROVIDER_ID, resource_type=RESOURCE_TYPE, rebuild_id="rebuild-1", ), ) self.assertEqual(("source-1",), tuple(doc.resource_id for doc in page.documents)) document = page.documents[0] serialized = repr(document) self.assertNotIn("must-not-be-indexed", serialized) self.assertNotIn("secret_field", serialized) self.assertNotIn("credential-1", serialized) self.assertTrue(document.requires_authorization_recheck) self.assertEqual( "/datasources?datasource=datasource%3Asource-1", document.url, ) def test_authorization_rechecks_scope_tenant_and_current_existence(self) -> None: reference = SearchResourceReference( tenant_id="tenant-1", module_id="datasources", resource_type=RESOURCE_TYPE, resource_id="source-1", ) request = SearchAuthorizationRequest(reference=reference, source_revision="1") self.assertTrue( self.source.authorize( self.session, _principal({CATALOGUE_READ_SCOPE}), requests=(request,), )[reference.key] ) self.assertTrue( self.source.authorize( self.session, _principal({ADMIN_SCOPE}), requests=(request,), )[reference.key] ) self.assertFalse( self.source.authorize( self.session, _principal(set()), requests=(request,), )[reference.key] ) other_reference = SearchResourceReference( tenant_id="tenant-2", module_id="datasources", resource_type=RESOURCE_TYPE, resource_id="source-other", ) other_request = SearchAuthorizationRequest( reference=other_reference, source_revision="1", ) self.assertFalse( self.source.authorize( self.session, _principal({CATALOGUE_READ_SCOPE}), requests=(other_request,), )[other_reference.key] ) def _datasource(identifier: str, tenant_id: str, name: str) -> DatasourceRecord: return DatasourceRecord( id=identifier, tenant_id=tenant_id, source_name=identifier.replace("-", "_"), name=name, description="Safe catalogue description", kind="database", mode="cached", shape="tabular", status="active", provider="connectors.sql", provider_ref="credential-1", schema_=[{"name": "secret_field", "type": "string"}], provenance_={"query": "must-not-be-indexed"}, metadata_={"password": "must-not-be-indexed"}, ) def _principal(scopes: set[str]) -> ApiPrincipal: return ApiPrincipal( principal=PrincipalRef( account_id="account-1", membership_id="user-1", tenant_id="tenant-1", scopes=frozenset(scopes), ), account=SimpleNamespace(id="account-1"), user=SimpleNamespace(id="user-1"), ) if __name__ == "__main__": unittest.main()