"""MigrationAuthorizationBackend tests.""" import logging from typing import Any from unittest.mock import MagicMock import pytest from python_pdp_sdk import ( MigrationAuthorizationBackend, PdpAuthorizationBackend, WouldDenyMetadata, ) from python_pdp_sdk.backends.authorization_backend import ( AuthorizationBackend, ResourceAction, ) from python_pdp_sdk.backends.exceptions import UnauthenticatedException from python_pdp_sdk.connectors.ows_pdp.models.allowed_tenant import AllowedTenant from python_pdp_sdk.connectors.ows_pdp.models.tenant import Tenant from python_pdp_sdk.connectors.ows_pdp.models.tenant_type import TenantType # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def mock_inner() -> MagicMock: """Return mocked inner AuthorizationBackend.""" return MagicMock(spec=PdpAuthorizationBackend) @pytest.fixture() def mock_dd_api(monkeypatch: pytest.MonkeyPatch) -> MagicMock: """Patch datadog.api on the migration_backend module and return the mock.""" mock_api = MagicMock() monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.datadog.api", mock_api ) return mock_api @pytest.fixture(autouse=True) def _mock_dd_initialize(monkeypatch: pytest.MonkeyPatch) -> None: """Prevent real datadog.initialize calls from touching global state in tests.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.datadog.initialize", MagicMock(), ) @pytest.fixture() def backend(mock_inner: MagicMock) -> MigrationAuthorizationBackend: """Return a fully configured MigrationAuthorizationBackend with metrics enabled.""" return MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="ows-test", environment="qa", dd_api_key="key", ) def _make_resource_actions(count: int = 2) -> list[ResourceAction]: return [ ResourceAction( resource_id=str(i), attributes={}, action="view", resource_type="track", ) for i in range(count) ] def _make_resources(count: int = 3) -> list[Any]: from python_pdp_sdk import ResourceWithAttributes return [ ResourceWithAttributes(resource_id=str(i), attributes={}) for i in range(count) ] # --------------------------------------------------------------------------- # is_authorized # --------------------------------------------------------------------------- def test_is_authorized_returns_true_when_inner_allows( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """No metric emitted when inner allows.""" mock_inner.is_authorized.return_value = True result = backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_not_called() def test_is_authorized_returns_true_when_inner_denies( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Metric emitted with reason=pp_denied when inner denies.""" mock_inner.is_authorized.return_value = False result = backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_called_once() tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "reason:pp_denied" in tags def test_is_authorized_returns_true_on_unauthenticated_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Metric emitted with reason=unauthenticated on UnauthenticatedException.""" mock_inner.is_authorized.side_effect = UnauthenticatedException("auth error") result = backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_called_once() tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "reason:unauthenticated" in tags def test_is_authorized_returns_true_on_unexpected_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Metric emitted with reason=exception on any unexpected inner exception.""" mock_inner.is_authorized.side_effect = RuntimeError("boom") result = backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_called_once() tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "reason:exception" in tags def test_is_authorized_raise_when_unauthorized_is_noop( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """raise_when_unauthorized=True is accepted but never raises.""" mock_inner.is_authorized.return_value = False result = backend.is_authorized( "view", 1, "track", MagicMock(), raise_when_unauthorized=True ) assert result is True def test_is_authorized_inner_called_with_raise_when_unauthorized_false( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """Inner backend always receives raise_when_unauthorized=False.""" mock_inner.is_authorized.return_value = True resource_getter = MagicMock() backend.is_authorized( "view", 1, "track", resource_getter, raise_when_unauthorized=True ) mock_inner.is_authorized.assert_called_once_with( "view", 1, "track", resource_getter, False ) def test_is_authorized_metric_tags_include_required_fields( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Metric tags include environment, service_name, action, resource_type, reason.""" mock_inner.is_authorized.return_value = False backend.is_authorized("edit", 42, "album", MagicMock()) tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "environment:qa" in tags assert "service_name:ows-test" in tags assert "action:edit" in tags assert "resource_type:album" in tags assert "reason:pp_denied" in tags def test_is_authorized_on_would_deny_called_with_pp_denied( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny receives WouldDenyMetadata with reason=pp_denied when inner denies.""" mock_inner.is_authorized.return_value = False on_would_deny = MagicMock() backend.is_authorized("view", 1, "track", MagicMock(), on_would_deny=on_would_deny) on_would_deny.assert_called_once() metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert isinstance(metadata, WouldDenyMetadata) assert metadata.reason == "pp_denied" assert metadata.action == "view" assert metadata.resource_type == "track" assert metadata.environment == "qa" assert metadata.service_name == "ows-test" def test_is_authorized_on_would_deny_called_with_unauthenticated( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny receives reason=unauthenticated on UnauthenticatedException.""" mock_inner.is_authorized.side_effect = UnauthenticatedException("auth error") on_would_deny = MagicMock() backend.is_authorized("view", 1, "track", MagicMock(), on_would_deny=on_would_deny) metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "unauthenticated" def test_is_authorized_on_would_deny_called_with_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny receives reason=exception on any unexpected inner exception.""" mock_inner.is_authorized.side_effect = RuntimeError("boom") on_would_deny = MagicMock() backend.is_authorized("view", 1, "track", MagicMock(), on_would_deny=on_would_deny) metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "exception" def test_is_authorized_on_would_deny_not_called_when_allowed( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is not invoked when inner allows.""" mock_inner.is_authorized.return_value = True on_would_deny = MagicMock() backend.is_authorized("view", 1, "track", MagicMock(), on_would_deny=on_would_deny) on_would_deny.assert_not_called() def test_is_authorized_on_would_deny_exception_swallowed( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """A raising on_would_deny callback never propagates to the caller.""" mock_inner.is_authorized.return_value = False on_would_deny = MagicMock(side_effect=RuntimeError("callback boom")) result = backend.is_authorized( "view", 1, "track", MagicMock(), on_would_deny=on_would_deny ) assert result is True def test_is_authorized_on_would_deny_not_forwarded_to_inner( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is consumed by this backend, not passed to the inner backend.""" mock_inner.is_authorized.return_value = True resource_getter = MagicMock() backend.is_authorized( "view", 1, "track", resource_getter, on_would_deny=MagicMock() ) mock_inner.is_authorized.assert_called_once_with( "view", 1, "track", resource_getter, False ) def test_is_authorized_on_would_deny_closure_style_callback( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny supports a closure-capturing callback like ows-assets' example. `_log_would_deny_identity(action, tenant)` ignores WouldDenyMetadata entirely and instead reads a Flask request-scoped `g` for identity/logging context it already has from the call site. """ RESOURCE_TYPE = "product" # Stand-in for flask.g inside a request context: request_context.jwt_identity_id # and log are populated per-request by the Flask app, not by this SDK. g = MagicMock() g.request_context.jwt_identity_id = "identity-42" logged: list[str] = [] g.log.info.side_effect = logged.append def _log_would_deny_identity(action: str, tenant: Tenant | None) -> None: """Log the JWT identity PP would deny, to build the grant list.""" identity_id = g.request_context.jwt_identity_id if not identity_id: return tenant_note = ( f"tenant_type={tenant.tenant_type} tenant_uuid={tenant.tenant_uuid}" if tenant is not None else "tenant=none" ) message = ( f"pp_would_deny identity={identity_id} action={action} " f"resource={RESOURCE_TYPE} {tenant_note}" ) g.log.info(message) action = "view" tenant = Tenant(tenant_type=TenantType.ACCOUNT, tenant_uuid="uuid-123") mock_inner.is_authorized.return_value = False backend.is_authorized( action, 1, "track", MagicMock(), on_would_deny=lambda _metadata: _log_would_deny_identity(action, tenant), ) assert logged == [ "pp_would_deny identity=identity-42 action=view resource=product " "tenant_type=TenantType.ACCOUNT tenant_uuid=uuid-123" ] # --------------------------------------------------------------------------- # is_authorized_many # --------------------------------------------------------------------------- def test_is_authorized_many_returns_all_true( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """Always returns a list of True values.""" mock_inner.is_authorized_many.return_value = [True, False, True] result = backend.is_authorized_many("view", "track", _make_resources(3)) assert result == [True, True, True] def test_is_authorized_many_emits_one_metric_per_denied_resource( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """One metric per denied resource, not per call.""" mock_inner.is_authorized_many.return_value = [True, False, False] backend.is_authorized_many("view", "track", _make_resources(3)) assert mock_dd_api.Metric.send.call_count == 2 for c in mock_dd_api.Metric.send.call_args_list: assert "reason:pp_denied" in c.kwargs["tags"] def test_is_authorized_many_no_metric_when_all_allowed( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """No metric when inner allows everything.""" mock_inner.is_authorized_many.return_value = [True, True, True] backend.is_authorized_many("view", "track", _make_resources(3)) mock_dd_api.Metric.send.assert_not_called() def test_is_authorized_many_emits_per_resource_on_unauthenticated_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """One metric per resource when inner raises UnauthenticatedException.""" resources = _make_resources(3) mock_inner.is_authorized_many.side_effect = UnauthenticatedException("auth") result = backend.is_authorized_many("view", "track", resources) assert result == [True, True, True] assert mock_dd_api.Metric.send.call_count == 3 for c in mock_dd_api.Metric.send.call_args_list: assert "reason:unauthenticated" in c.kwargs["tags"] def test_is_authorized_many_emits_per_resource_on_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """One metric per resource when inner raises unexpected exception.""" resources = _make_resources(2) mock_inner.is_authorized_many.side_effect = RuntimeError("boom") result = backend.is_authorized_many("view", "track", resources) assert result == [True, True] assert mock_dd_api.Metric.send.call_count == 2 def test_is_authorized_many_raise_when_unauthorized_is_noop( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """raise_when_unauthorized=True never causes an exception.""" mock_inner.is_authorized_many.return_value = [False, False] result = backend.is_authorized_many( "view", "track", _make_resources(2), raise_when_unauthorized=True ) assert result == [True, True] def test_is_authorized_many_on_would_deny_called_once_per_denied_resource( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is invoked once per denied resource, with reason=pp_denied.""" mock_inner.is_authorized_many.return_value = [True, False, False] on_would_deny = MagicMock() backend.is_authorized_many( "view", "track", _make_resources(3), on_would_deny=on_would_deny ) assert on_would_deny.call_count == 2 metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "pp_denied" def test_is_authorized_many_on_would_deny_called_per_resource_on_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is invoked once per resource when inner raises.""" mock_inner.is_authorized_many.side_effect = RuntimeError("boom") on_would_deny = MagicMock() backend.is_authorized_many( "view", "track", _make_resources(2), on_would_deny=on_would_deny ) assert on_would_deny.call_count == 2 metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "exception" # --------------------------------------------------------------------------- # is_authorized_many_resources_and_actions # --------------------------------------------------------------------------- def test_is_authorized_many_resources_and_actions_returns_all_true( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """Always returns a list of True values.""" resource_actions = _make_resource_actions(3) mock_inner.is_authorized_many_resources_and_actions.return_value = [ True, False, True, ] result = backend.is_authorized_many_resources_and_actions(resource_actions) assert result == [True, True, True] def test_is_authorized_many_resources_and_actions_emits_per_resource_action_with_correct_tags( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Each denied resource-action gets its own metric with correct action/resource_type tags.""" resource_actions = [ ResourceAction( resource_id="1", attributes={}, action="view", resource_type="track" ), ResourceAction( resource_id="2", attributes={}, action="edit", resource_type="album" ), ] mock_inner.is_authorized_many_resources_and_actions.return_value = [ False, False, ] backend.is_authorized_many_resources_and_actions(resource_actions) assert mock_dd_api.Metric.send.call_count == 2 first_tags = mock_dd_api.Metric.send.call_args_list[0].kwargs["tags"] second_tags = mock_dd_api.Metric.send.call_args_list[1].kwargs["tags"] assert "action:view" in first_tags assert "resource_type:track" in first_tags assert "action:edit" in second_tags assert "resource_type:album" in second_tags def test_is_authorized_many_resources_and_actions_emits_per_resource_action_on_unauthenticated( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """One metric per resource-action on UnauthenticatedException.""" resource_actions = _make_resource_actions(2) mock_inner.is_authorized_many_resources_and_actions.side_effect = ( UnauthenticatedException("auth") ) result = backend.is_authorized_many_resources_and_actions(resource_actions) assert result == [True, True] assert mock_dd_api.Metric.send.call_count == 2 for c in mock_dd_api.Metric.send.call_args_list: assert "reason:unauthenticated" in c.kwargs["tags"] def test_is_authorized_many_resources_and_actions_emits_per_resource_action_on_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """One metric per resource-action on unexpected exception.""" resource_actions = _make_resource_actions(2) mock_inner.is_authorized_many_resources_and_actions.side_effect = RuntimeError( "boom" ) result = backend.is_authorized_many_resources_and_actions(resource_actions) assert result == [True, True] assert mock_dd_api.Metric.send.call_count == 2 def test_is_authorized_many_resources_and_actions_raise_when_unauthorized_is_noop( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """raise_when_unauthorized=True never causes an exception.""" resource_actions = _make_resource_actions(2) mock_inner.is_authorized_many_resources_and_actions.return_value = [ False, False, ] result = backend.is_authorized_many_resources_and_actions( resource_actions, raise_when_unauthorized=True ) assert result == [True, True] def test_is_authorized_many_resources_and_actions_on_would_deny_called_per_denied( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is invoked once per denied resource-action, with reason=pp_denied.""" resource_actions = _make_resource_actions(3) mock_inner.is_authorized_many_resources_and_actions.return_value = [ True, False, False, ] on_would_deny = MagicMock() backend.is_authorized_many_resources_and_actions( resource_actions, on_would_deny=on_would_deny ) assert on_would_deny.call_count == 2 metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "pp_denied" def test_is_authorized_many_resources_and_actions_on_would_deny_called_on_exception( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """on_would_deny is invoked once per resource-action when inner raises.""" resource_actions = _make_resource_actions(2) mock_inner.is_authorized_many_resources_and_actions.side_effect = RuntimeError( "boom" ) on_would_deny = MagicMock() backend.is_authorized_many_resources_and_actions( resource_actions, on_would_deny=on_would_deny ) assert on_would_deny.call_count == 2 metadata: WouldDenyMetadata = on_would_deny.call_args.args[0] assert metadata.reason == "exception" def test_is_authorized_many_resources_and_actions_logs_warning_on_length_mismatch( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """Mismatched inner result length logs a warning, emits no metrics, and still returns all-True.""" resource_actions = _make_resource_actions(2) mock_inner.is_authorized_many_resources_and_actions.return_value = [False] on_would_deny = MagicMock() with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): result = backend.is_authorized_many_resources_and_actions( resource_actions, on_would_deny=on_would_deny ) assert result == [True, True] assert any("MIGRATION_BACKEND_ERROR" in r.message for r in caplog.records) mock_dd_api.Metric.send.assert_not_called() on_would_deny.assert_not_called() # --------------------------------------------------------------------------- # get_authorized_tenants # --------------------------------------------------------------------------- def test_get_authorized_tenants_delegates_to_inner_and_returns_result( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, ) -> None: """Return value from inner backend is passed through unchanged.""" expected: list[AllowedTenant] = [MagicMock(spec=AllowedTenant)] mock_inner.get_authorized_tenants.return_value = expected result = backend.get_authorized_tenants("view", "track") mock_inner.get_authorized_tenants.assert_called_once_with("view", "track") assert result is expected def test_get_authorized_tenants_no_metric_emitted( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """get_authorized_tenants never emits a metric on success.""" mock_inner.get_authorized_tenants.return_value = [] backend.get_authorized_tenants("view", "track") mock_dd_api.Metric.send.assert_not_called() @pytest.mark.parametrize( ("exc", "expected_reason"), [ pytest.param( UnauthenticatedException("no auth"), "reason:unauthenticated", id="unauthenticated", ), pytest.param(RuntimeError("boom"), "reason:exception", id="exception"), ], ) def test_get_authorized_tenants_emits_metric_and_returns_empty_list_on_error( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, exc: Exception, expected_reason: str, ) -> None: """Emits metric and returns [] instead of propagating the error.""" mock_inner.get_authorized_tenants.side_effect = exc result = backend.get_authorized_tenants("view", "track") assert result == [] mock_dd_api.Metric.send.assert_called_once() assert expected_reason in mock_dd_api.Metric.send.call_args.kwargs["tags"] # --------------------------------------------------------------------------- # Metrics disabled paths # --------------------------------------------------------------------------- def test_metrics_disabled_no_metric_when_key_absent( mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """No metric emitted when dd_api_key is not provided.""" no_keys_backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", ) mock_inner.is_authorized.return_value = False result = no_keys_backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_not_called() def test_metrics_disabled_no_metric_when_key_absent_logs_warning( mock_inner: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """A warning is logged when dd_api_key is not provided.""" with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", ) assert any("api key not provided" in r.message.lower() for r in caplog.records) def test_metrics_disabled_no_metric_when_metrics_enabled_is_false( mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """No metric emitted when metrics_enabled=False, even with a valid dd_api_key.""" disabled_backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", metrics_enabled=False, ) mock_inner.is_authorized.return_value = False result = disabled_backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_not_called() def test_metrics_enabled_false_skips_dd_credential_resolution( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: """metrics_enabled=False short-circuits before resolving DD credentials at all.""" try_enable_metrics = MagicMock() monkeypatch.setattr( MigrationAuthorizationBackend, "_try_enable_metrics", try_enable_metrics, ) backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", metrics_enabled=False, ) try_enable_metrics.assert_not_called() assert backend._metrics_enabled is False def test_metrics_disabled_no_metric_when_datadog_unavailable( mock_inner: MagicMock, mock_dd_api: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: """No metric emitted when the datadog package is not installed.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend._DATADOG_AVAILABLE", False, ) no_dd_backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) mock_inner.is_authorized.return_value = False result = no_dd_backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_not_called() def test_metrics_disabled_no_metric_when_datadog_unavailable_logs_warning( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """A warning is logged when the datadog package is not installed.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend._DATADOG_AVAILABLE", False, ) with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert any("datadog" in r.message.lower() for r in caplog.records) # --------------------------------------------------------------------------- # Metric send exception swallowed # --------------------------------------------------------------------------- def test_metric_exception_swallowed_metric_send_exception_is_swallowed( backend: MigrationAuthorizationBackend, mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Exception from datadog.api.Metric.send does not propagate to caller.""" mock_dd_api.Metric.send.side_effect = Exception("datadog down") mock_inner.is_authorized.return_value = False result = backend.is_authorized("view", 1, "track", MagicMock()) assert result is True # --------------------------------------------------------------------------- # extra_tags_getter # --------------------------------------------------------------------------- def test_extra_tags_getter_extra_tags_appended_to_metric( mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """Tags from extra_tags_getter are appended to the metric.""" extra_backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", extra_tags_getter=lambda: ["method:GET", "endpoint:/api/v1/tracks"], ) mock_inner.is_authorized.return_value = False extra_backend.is_authorized("view", 1, "track", MagicMock()) tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "method:GET" in tags assert "endpoint:/api/v1/tracks" in tags def test_extra_tags_getter_extra_tags_getter_exception_swallowed_metric_still_sent( mock_inner: MagicMock, mock_dd_api: MagicMock, ) -> None: """extra_tags_getter exception is swallowed; metric is still sent with base tags.""" def bad_getter() -> list[str]: raise RuntimeError("context unavailable") extra_backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", extra_tags_getter=bad_getter, ) mock_inner.is_authorized.return_value = False result = extra_backend.is_authorized("view", 1, "track", MagicMock()) assert result is True mock_dd_api.Metric.send.assert_called_once() tags = mock_dd_api.Metric.send.call_args.kwargs["tags"] assert "reason:pp_denied" in tags # --------------------------------------------------------------------------- # Protocol compliance # --------------------------------------------------------------------------- def test_protocol_compliance_isinstance_check( backend: MigrationAuthorizationBackend, ) -> None: """MigrationAuthorizationBackend is an instance of AuthorizationBackend protocol.""" assert isinstance(backend, AuthorizationBackend) def test_protocol_compliance_importable_from_python_pdp_sdk() -> None: """MigrationAuthorizationBackend is importable directly from python_pdp_sdk.""" from python_pdp_sdk import MigrationAuthorizationBackend as MAB # noqa: F401 assert MAB is MigrationAuthorizationBackend # --------------------------------------------------------------------------- # _try_enable_metrics # --------------------------------------------------------------------------- def test_try_enable_metrics_returns_true_when_api_key_present_and_init_succeeds( mock_inner: MagicMock, ) -> None: """Returns True and enables metrics when all conditions are met.""" backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert backend._metrics_enabled is True def test_try_enable_metrics_returns_false_when_api_key_missing( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Returns False when dd_api_key is absent and secrets fallback yields None.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.MigrationAuthorizationBackend" "._use_provided_dd_api_key_or_fetch", lambda self, k: None, ) backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", ) assert backend._metrics_enabled is False def test_try_enable_metrics_returns_false_when_datadog_unavailable( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Returns False when the datadog package is not installed.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend._DATADOG_AVAILABLE", False ) backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert backend._metrics_enabled is False def test_try_enable_metrics_returns_false_when_initialize_raises( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Returns False when datadog.initialize raises.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.datadog.initialize", MagicMock(side_effect=Exception("init failed")), ) backend = MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert backend._metrics_enabled is False def test_try_enable_metrics_logs_warning_when_key_missing( mock_inner: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """Logs a warning when key is missing.""" with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", ) assert any("api key not provided" in r.message.lower() for r in caplog.records) def test_try_enable_metrics_logs_warning_when_datadog_unavailable( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Logs a warning when the datadog package is not installed.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend._DATADOG_AVAILABLE", False ) with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert any("datadog" in r.message.lower() for r in caplog.records) def test_try_enable_metrics_logs_warning_when_initialize_raises( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Logs a warning when datadog.initialize raises.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.datadog.initialize", MagicMock(side_effect=Exception("init failed")), ) with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", dd_api_key="key", ) assert any("failed to initialize" in r.message.lower() for r in caplog.records) # --------------------------------------------------------------------------- # _use_provided_dd_api_key_or_fetch # --------------------------------------------------------------------------- @pytest.fixture() def backend_no_keys( mock_inner: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> MigrationAuthorizationBackend: """Backend instance with metrics init short-circuited so we can call the method directly.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.MigrationAuthorizationBackend._try_enable_metrics", lambda self, k: False, ) return MigrationAuthorizationBackend( inner_backend=mock_inner, service_name="svc", environment="qa", ) def test_use_provided_dd_api_key_or_fetch_returns_provided_key_without_fetching( backend_no_keys: MigrationAuthorizationBackend, monkeypatch: pytest.MonkeyPatch, ) -> None: """Short-circuits immediately when api_key is supplied; no SecretsManager created.""" mock_sm = MagicMock() monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.DatadogSecretsManager", mock_sm ) api_key = backend_no_keys._use_provided_dd_api_key_or_fetch("my-api") assert api_key == "my-api" mock_sm.assert_not_called() def test_use_provided_dd_api_key_or_fetch_fetches_from_secrets_manager_when_absent( backend_no_keys: MigrationAuthorizationBackend, monkeypatch: pytest.MonkeyPatch, ) -> None: """Fetches DD_API_KEY from Secrets Manager when api_key is not provided.""" mock_sm_instance = MagicMock() mock_sm_instance.get_secret.return_value = "fetched-api-key" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.DatadogSecretsManager", MagicMock(return_value=mock_sm_instance), ) api_key = backend_no_keys._use_provided_dd_api_key_or_fetch(None) assert api_key == "fetched-api-key" mock_sm_instance.get_secret.assert_called_once_with("DD_API_KEY") def test_use_provided_dd_api_key_or_fetch_returns_none_on_secrets_manager_error( backend_no_keys: MigrationAuthorizationBackend, monkeypatch: pytest.MonkeyPatch, ) -> None: """Returns None and logs a warning when Secrets Manager raises.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.DatadogSecretsManager", MagicMock(side_effect=Exception("AWS error")), ) api_key = backend_no_keys._use_provided_dd_api_key_or_fetch(None) assert api_key is None def test_use_provided_dd_api_key_or_fetch_logs_warning_on_secrets_manager_error( backend_no_keys: MigrationAuthorizationBackend, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Logs a warning when Secrets Manager raises.""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.DatadogSecretsManager", MagicMock(side_effect=Exception("AWS error")), ) with caplog.at_level( logging.WARNING, logger="python_pdp_sdk.backends.migration_backend" ): backend_no_keys._use_provided_dd_api_key_or_fetch(None) assert any("secrets manager" in r.message.lower() for r in caplog.records) def test_use_provided_dd_api_key_or_fetch_returns_none_on_boto3_import_error( backend_no_keys: MigrationAuthorizationBackend, monkeypatch: pytest.MonkeyPatch, ) -> None: """Returns None when boto3 is not installed (ImportError from DatadogSecretsManager).""" monkeypatch.setattr( "python_pdp_sdk.backends.migration_backend.DatadogSecretsManager", MagicMock(side_effect=ImportError("No module named 'boto3'")), ) api_key = backend_no_keys._use_provided_dd_api_key_or_fetch(None) assert api_key is None