"""Tests for DatadogSecretsManager.""" import logging from types import ModuleType from typing import Any from unittest.mock import MagicMock, patch import pytest from python_pdp_sdk.connectors.secretsmanager import DatadogSecretsManager # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def mock_boto3_client() -> MagicMock: """Mock boto client.""" return MagicMock() @pytest.fixture() def secrets_manager(mock_boto3_client: MagicMock) -> DatadogSecretsManager: """DatadogSecretsManager with boto3 session/client patched out.""" mock_session = MagicMock() mock_session.client.return_value = mock_boto3_client with patch("boto3.session.Session", return_value=mock_session): return DatadogSecretsManager(environment="qa") # --------------------------------------------------------------------------- # __init__ # --------------------------------------------------------------------------- def test_init_raises_import_error_when_boto3_unavailable( monkeypatch: pytest.MonkeyPatch, ) -> None: """Raises a clear ImportError when boto3 is not installed.""" import builtins real_import = builtins.__import__ def import_without_boto3(name: str, *args: Any, **kwargs: Any) -> ModuleType: if name == "boto3": raise ImportError("No module named 'boto3'") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", import_without_boto3) with pytest.raises(ImportError, match="boto3 is required"): DatadogSecretsManager(environment="qa") def test_init_creates_secretsmanager_client() -> None: """Constructs a secretsmanager boto3 client on init.""" mock_session = MagicMock() with patch("boto3.session.Session", return_value=mock_session) as mock_session_cls: DatadogSecretsManager(environment="qa") mock_session_cls.assert_called_once() mock_session.client.assert_called_once_with("secretsmanager") def test_init_default_service_name_is_datadog() -> None: """Default service_name is 'datadog'.""" mock_session = MagicMock() with patch("boto3.session.Session", return_value=mock_session): sm = DatadogSecretsManager(environment="qa") assert sm._service_name == "datadog" def test_init_custom_service_name() -> None: """Accepts a custom service_name.""" mock_session = MagicMock() with patch("boto3.session.Session", return_value=mock_session): sm = DatadogSecretsManager(environment="qa", service_name="custom") assert sm._service_name == "custom" # --------------------------------------------------------------------------- # get_secret # --------------------------------------------------------------------------- def test_get_secret_returns_secret_string( secrets_manager: DatadogSecretsManager, mock_boto3_client: MagicMock, ) -> None: """Returns the SecretString value from the AWS response.""" mock_boto3_client.get_secret_value.return_value = {"SecretString": "s3cr3t"} assert secrets_manager.get_secret("DD_API_KEY") == "s3cr3t" mock_boto3_client.get_secret_value.assert_called_once_with( SecretId="qa/datadog/DD_API_KEY" ) def test_get_secret_raises_value_error_when_secret_string_absent( secrets_manager: DatadogSecretsManager, mock_boto3_client: MagicMock, ) -> None: """Raises ValueError when response contains no SecretString (e.g. binary secret).""" mock_boto3_client.get_secret_value.return_value = {"SecretBinary": b"binary"} with pytest.raises(ValueError, match="SecretString"): secrets_manager.get_secret("DD_API_KEY") def test_get_secret_propagates_client_error( secrets_manager: DatadogSecretsManager, mock_boto3_client: MagicMock, ) -> None: """Propagates boto3 client errors (e.g. ResourceNotFoundException) to the caller.""" mock_boto3_client.get_secret_value.side_effect = Exception( "ResourceNotFoundException" ) with pytest.raises(Exception, match="ResourceNotFoundException"): secrets_manager.get_secret("DD_API_KEY") def test_get_secret_logs_secret_name_at_debug( secrets_manager: DatadogSecretsManager, mock_boto3_client: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """Logs the secret path being fetched at DEBUG level.""" mock_boto3_client.get_secret_value.return_value = {"SecretString": "val"} with caplog.at_level( logging.DEBUG, logger="python_pdp_sdk.connectors.secretsmanager" ): secrets_manager.get_secret("DD_API_KEY") assert any("qa/datadog/dd_api_key" in r.message.lower() for r in caplog.records) def test_get_secret_uses_environment_in_secret_name( mock_boto3_client: MagicMock, ) -> None: """Secret name includes the environment prefix.""" mock_session = MagicMock() mock_session.client.return_value = mock_boto3_client mock_boto3_client.get_secret_value.return_value = {"SecretString": "val"} with patch("boto3.session.Session", return_value=mock_session): sm = DatadogSecretsManager(environment="production") sm.get_secret("DD_APP_KEY") mock_boto3_client.get_secret_value.assert_called_once_with( SecretId="production/datadog/DD_APP_KEY" )