from __future__ import annotations import uuid import pytest from sqlalchemy.exc import IntegrityError from app.core.exceptions import AppException from app.domains.data_access import constants as c from app.domains.data_access.models import ( OWNER_RESOLUTION_FALLBACK, OWNER_RESOLUTION_RESOLVED, OWNER_RESOLUTION_UNRESOLVED, OWNER_SOURCE_BOOTSTRAP, OWNER_SOURCE_DATASET_INHERITED, OWNER_SOURCE_MANUAL, RESOURCE_STATUS_ACTIVE, RESOURCE_STATUS_SUSPENDED, DataAccessResource, ) from app.domains.data_access.service import DataAccessService from app.integrations.models import Integration async def _integration_id(db_session) -> str: row = Integration( kind=c.DOMAIN_DATA_ACCESS, provider=c.PROVIDER_BIGQUERY, name="Data Access BigQuery", slug=f"dax-bq-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, ) db_session.add(row) await db_session.flush() return str(row.id) def _resource( *, integration_id: str, project_id: str = "helix-data-prod", dataset_id: str = "finance_2026", status: str = RESOURCE_STATUS_ACTIVE, owner_person_id: str | None = None, owner_resolution_status: str = OWNER_RESOLUTION_UNRESOLVED, ) -> DataAccessResource: return DataAccessResource( integration_id=integration_id, provider=c.PROVIDER_BIGQUERY, resource_type=c.RESOURCE_TYPE_DATASET, project_id=project_id, dataset_id=dataset_id, display_name=f"{project_id}.{dataset_id}", requestable=True, status=status, owner_person_id=owner_person_id, owner_resolution_status=owner_resolution_status, owner_source=OWNER_SOURCE_BOOTSTRAP, source_version="bootstrap-2026-06-10", source_etag="etag-001", ) async def test_duplicate_active_locator_is_rejected(db_session) -> None: integration_id = await _integration_id(db_session) db_session.add(_resource(integration_id=integration_id)) await db_session.flush() db_session.add(_resource(integration_id=integration_id)) with pytest.raises(IntegrityError): await db_session.flush() async def test_historical_suspended_duplicate_locator_is_allowed(db_session) -> None: integration_id = await _integration_id(db_session) db_session.add(_resource(integration_id=integration_id)) db_session.add( _resource( integration_id=integration_id, status=RESOURCE_STATUS_SUSPENDED, ), ) await db_session.flush() async def test_same_locator_is_allowed_for_different_integrations(db_session) -> None: first_integration_id = await _integration_id(db_session) second_integration_id = await _integration_id(db_session) db_session.add(_resource(integration_id=first_integration_id)) db_session.add(_resource(integration_id=second_integration_id)) await db_session.flush() async def test_case_distinct_dataset_ids_are_distinct_resources(db_session) -> None: integration_id = await _integration_id(db_session) db_session.add( _resource(integration_id=integration_id, dataset_id="Finance_2026"), ) db_session.add( _resource(integration_id=integration_id, dataset_id="finance_2026"), ) await db_session.flush() async def test_unresolved_and_fallback_owner_rows_grant_no_owner_authority( db_session, ) -> None: integration_id = await _integration_id(db_session) unresolved = _resource( integration_id=integration_id, dataset_id="unresolved_owner", owner_person_id=None, owner_resolution_status=OWNER_RESOLUTION_UNRESOLVED, ) fallback = _resource( integration_id=integration_id, dataset_id="fallback_owner", owner_person_id=None, owner_resolution_status=OWNER_RESOLUTION_FALLBACK, ) db_session.add_all([unresolved, fallback]) await db_session.flush() assert unresolved.resource_owner_authority_enabled is False assert fallback.resource_owner_authority_enabled is False async def test_resolved_owner_row_enables_owner_authority( db_session, employee_user, ) -> None: owner, _ = employee_user integration_id = await _integration_id(db_session) resource = _resource( integration_id=integration_id, owner_person_id=str(owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(resource) await db_session.flush() assert resource.resource_owner_authority_enabled is True async def test_table_resource_defaults_to_dataset_owner_and_is_idempotent( db_session, employee_user, ) -> None: owner, _ = employee_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() service = DataAccessService() first = await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) second = await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) assert first.resource_id == second.resource_id assert first.resource_type == c.RESOURCE_TYPE_TABLE assert first.table_id == "account_daily" assert first.owner_person_id == str(owner.id) assert first.owner_source == OWNER_SOURCE_DATASET_INHERITED async def test_existing_table_with_empty_owner_is_repaired_from_dataset_owner( db_session, employee_user, ) -> None: dataset_owner, _ = employee_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(dataset_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() table = DataAccessResource( integration_id=integration_id, provider=c.PROVIDER_BIGQUERY, resource_type=c.RESOURCE_TYPE_TABLE, project_id=dataset.project_id, dataset_id=dataset.dataset_id, parent_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", display_name="account_daily", requestable=True, status=RESOURCE_STATUS_ACTIVE, owner_person_id=None, owner_resolution_status=OWNER_RESOLUTION_UNRESOLVED, owner_source=OWNER_SOURCE_MANUAL, source_version=dataset.source_version, source_etag=dataset.source_etag, ) db_session.add(table) await db_session.flush() resolved = await DataAccessService().resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) assert resolved.resource_id == table.id assert resolved.owner_person_id == str(dataset_owner.id) assert resolved.owner_resolution_status == OWNER_RESOLUTION_RESOLVED assert resolved.owner_source == OWNER_SOURCE_DATASET_INHERITED async def test_dataset_owner_can_reassign_one_table_owner( db_session, employee_user, admin_user, ) -> None: dataset_owner, _ = employee_user next_owner, _ = admin_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(dataset_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() service = DataAccessService() await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) reassigned = await service.reassign_bigquery_table_owner( db_session, dataset_resource_id=dataset.id, table_id="account_daily", actor=dataset_owner, new_owner_person_id=next_owner.id, trace_id="test-table-owner-reassign", ) assert reassigned.owner_person_id == str(next_owner.id) assert reassigned.owner_source == OWNER_SOURCE_MANUAL async def test_current_table_owner_can_transfer_their_table( db_session, employee_user, admin_user, ) -> None: dataset_owner, _ = employee_user table_owner, _ = admin_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(dataset_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() service = DataAccessService() await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) await service.reassign_bigquery_table_owner( db_session, dataset_resource_id=dataset.id, table_id="account_daily", actor=dataset_owner, new_owner_person_id=table_owner.id, trace_id="test-table-owner-first-transfer", ) transferred = await service.reassign_bigquery_table_owner( db_session, dataset_resource_id=dataset.id, table_id="account_daily", actor=table_owner, new_owner_person_id=dataset_owner.id, trace_id="test-table-owner-self-transfer", ) assert transferred.owner_person_id == str(dataset_owner.id) assert transferred.owner_source == OWNER_SOURCE_MANUAL async def test_table_owner_cannot_transfer_table_to_themselves( db_session, employee_user, ) -> None: table_owner, _ = employee_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(table_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() service = DataAccessService() await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) with pytest.raises(AppException) as exc_info: await service.reassign_bigquery_table_owner( db_session, dataset_resource_id=dataset.id, table_id="account_daily", actor=table_owner, new_owner_person_id=table_owner.id, ) assert exc_info.value.code == "DATA_ACCESS_TABLE_OWNER_SAME_PERSON" assert exc_info.value.status_code == 422 async def test_current_dataset_owner_can_transfer_dataset_and_inherited_tables( db_session, employee_user, admin_user, ) -> None: dataset_owner, _ = employee_user next_owner, _ = admin_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(dataset_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() service = DataAccessService() inherited = await service.resolve_requestable_bigquery_table_resource( db_session, dataset_resource_id=dataset.id, table_id="account_daily", table_type="TABLE", ) transferred = await service.transfer_bigquery_dataset_owner( db_session, dataset_resource_id=dataset.id, actor=dataset_owner, new_owner_person_id=next_owner.id, trace_id="test-dataset-owner-self-transfer", ) inherited_row = await db_session.get(DataAccessResource, inherited.resource_id) assert transferred.owner_person_id == str(next_owner.id) assert transferred.owner_source == "bootstrap_transferred" assert inherited_row is not None assert inherited_row.owner_person_id == str(next_owner.id) assert inherited_row.owner_source == OWNER_SOURCE_DATASET_INHERITED async def test_dataset_owner_cannot_transfer_dataset_to_themselves( db_session, employee_user, ) -> None: dataset_owner, _ = employee_user integration_id = await _integration_id(db_session) dataset = _resource( integration_id=integration_id, owner_person_id=str(dataset_owner.id), owner_resolution_status=OWNER_RESOLUTION_RESOLVED, ) db_session.add(dataset) await db_session.flush() with pytest.raises(AppException) as exc_info: await DataAccessService().transfer_bigquery_dataset_owner( db_session, dataset_resource_id=dataset.id, actor=dataset_owner, new_owner_person_id=dataset_owner.id, ) assert exc_info.value.code == "DATA_ACCESS_DATASET_OWNER_SAME_PERSON" assert exc_info.value.status_code == 422