feat: add AWS Secrets Manager Integration

This commit is contained in:
tomoya-kawaguchi
2025-05-29 12:42:05 +09:00
parent a17100e512
commit 1955157e9a
13 changed files with 608 additions and 3 deletions

View File

@ -0,0 +1,102 @@
import json
import pytest
from unittest.mock import MagicMock, patch
from botocore.exceptions import ClientError
from pr_agent.secret_providers.aws_secrets_manager_provider import AWSSecretsManagerProvider
class TestAWSSecretsManagerProvider:
def _provider(self):
"""Create provider following existing pattern"""
with patch('pr_agent.secret_providers.aws_secrets_manager_provider.get_settings') as mock_get_settings, \
patch('pr_agent.secret_providers.aws_secrets_manager_provider.boto3.client') as mock_boto3_client:
settings = MagicMock()
settings.get.side_effect = lambda k, d=None: {
'aws_secrets_manager.secret_arn': 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret',
'aws_secrets_manager.region_name': 'us-east-1',
'aws.AWS_REGION_NAME': 'us-east-1'
}.get(k, d)
settings.aws_secrets_manager.secret_arn = 'arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret'
mock_get_settings.return_value = settings
# Mock boto3 client
mock_client = MagicMock()
mock_boto3_client.return_value = mock_client
provider = AWSSecretsManagerProvider()
provider.client = mock_client # Set client directly for testing
return provider, mock_client
# Positive test cases
def test_get_secret_success(self):
provider, mock_client = self._provider()
mock_client.get_secret_value.return_value = {'SecretString': 'test-secret-value'}
result = provider.get_secret('test-secret-name')
assert result == 'test-secret-value'
mock_client.get_secret_value.assert_called_once_with(SecretId='test-secret-name')
def test_get_all_secrets_success(self):
provider, mock_client = self._provider()
secret_data = {'openai.key': 'sk-test', 'github.webhook_secret': 'webhook-secret'}
mock_client.get_secret_value.return_value = {'SecretString': json.dumps(secret_data)}
result = provider.get_all_secrets()
assert result == secret_data
# Negative test cases (following Google Cloud Storage pattern)
def test_get_secret_failure(self):
provider, mock_client = self._provider()
mock_client.get_secret_value.side_effect = Exception("AWS error")
result = provider.get_secret('nonexistent-secret')
assert result == "" # Confirm empty string is returned
def test_get_all_secrets_failure(self):
provider, mock_client = self._provider()
mock_client.get_secret_value.side_effect = Exception("AWS error")
result = provider.get_all_secrets()
assert result == {} # Confirm empty dictionary is returned
def test_store_secret_update_existing(self):
provider, mock_client = self._provider()
mock_client.update_secret.return_value = {}
provider.store_secret('test-secret', 'test-value')
mock_client.update_secret.assert_called_once_with(
SecretId='test-secret',
SecretString='test-value'
)
def test_store_secret_create_new(self):
provider, mock_client = self._provider()
mock_client.update_secret.side_effect = ClientError(
{'Error': {'Code': 'ResourceNotFoundException'}}, 'UpdateSecret'
)
mock_client.create_secret.return_value = {}
provider.store_secret('new-secret', 'test-value')
mock_client.create_secret.assert_called_once_with(
Name='new-secret',
SecretString='test-value'
)
def test_init_failure_invalid_config(self):
with patch('pr_agent.secret_providers.aws_secrets_manager_provider.get_settings') as mock_get_settings:
settings = MagicMock()
settings.aws_secrets_manager.secret_arn = None # Configuration error
mock_get_settings.return_value = settings
with pytest.raises(Exception):
AWSSecretsManagerProvider()
def test_store_secret_failure(self):
provider, mock_client = self._provider()
mock_client.update_secret.side_effect = Exception("AWS error")
with pytest.raises(Exception):
provider.store_secret('test-secret', 'test-value')

View File

@ -0,0 +1,120 @@
import pytest
from unittest.mock import MagicMock, patch
from pr_agent.config_loader import apply_secrets_manager_config, apply_secrets_to_config
class TestConfigLoaderSecrets:
def test_apply_secrets_manager_config_success(self):
with patch('pr_agent.config_loader.get_secret_provider') as mock_get_provider, \
patch('pr_agent.config_loader.apply_secrets_to_config') as mock_apply_secrets, \
patch('pr_agent.config_loader.get_settings') as mock_get_settings:
# Mock secret provider
mock_provider = MagicMock()
mock_provider.get_all_secrets.return_value = {'openai.key': 'sk-test'}
mock_get_provider.return_value = mock_provider
# Mock settings
settings = MagicMock()
settings.get.return_value = "aws_secrets_manager"
mock_get_settings.return_value = settings
apply_secrets_manager_config()
mock_apply_secrets.assert_called_once_with({'openai.key': 'sk-test'})
def test_apply_secrets_manager_config_no_provider(self):
with patch('pr_agent.config_loader.get_secret_provider') as mock_get_provider:
mock_get_provider.return_value = None
# Confirm no exception is raised
apply_secrets_manager_config()
def test_apply_secrets_manager_config_not_aws(self):
with patch('pr_agent.config_loader.get_secret_provider') as mock_get_provider, \
patch('pr_agent.config_loader.get_settings') as mock_get_settings:
# Mock Google Cloud Storage provider
mock_provider = MagicMock()
mock_get_provider.return_value = mock_provider
# Mock settings (Google Cloud Storage)
settings = MagicMock()
settings.get.return_value = "google_cloud_storage"
mock_get_settings.return_value = settings
# Confirm execution is skipped for non-AWS Secrets Manager
apply_secrets_manager_config()
# Confirm get_all_secrets is not called
assert not hasattr(mock_provider, 'get_all_secrets') or \
not mock_provider.get_all_secrets.called
def test_apply_secrets_to_config_nested_keys(self):
with patch('pr_agent.config_loader.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = None # No existing value
settings.set = MagicMock()
mock_get_settings.return_value = settings
secrets = {
'openai.key': 'sk-test',
'github.webhook_secret': 'webhook-secret'
}
apply_secrets_to_config(secrets)
# Confirm settings are applied correctly
settings.set.assert_any_call('OPENAI.KEY', 'sk-test')
settings.set.assert_any_call('GITHUB.WEBHOOK_SECRET', 'webhook-secret')
def test_apply_secrets_to_config_existing_value_preserved(self):
with patch('pr_agent.config_loader.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = "existing-value" # Existing value present
settings.set = MagicMock()
mock_get_settings.return_value = settings
secrets = {'openai.key': 'sk-test'}
apply_secrets_to_config(secrets)
# Confirm settings are not overridden when existing value present
settings.set.assert_not_called()
def test_apply_secrets_to_config_single_key(self):
with patch('pr_agent.config_loader.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = None
settings.set = MagicMock()
mock_get_settings.return_value = settings
secrets = {'simple_key': 'simple_value'}
apply_secrets_to_config(secrets)
# Confirm non-dot notation keys are ignored
settings.set.assert_not_called()
def test_apply_secrets_to_config_multiple_dots(self):
with patch('pr_agent.config_loader.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = None
settings.set = MagicMock()
mock_get_settings.return_value = settings
secrets = {'section.subsection.key': 'value'}
apply_secrets_to_config(secrets)
# Confirm keys with multiple dots are ignored
settings.set.assert_not_called()
def test_apply_secrets_manager_config_exception_handling(self):
with patch('pr_agent.config_loader.get_secret_provider') as mock_get_provider:
mock_get_provider.side_effect = Exception("Provider error")
# Confirm processing continues even when exception occurs
apply_secrets_manager_config() # Confirm no exception is raised

View File

@ -0,0 +1,69 @@
import pytest
from unittest.mock import MagicMock, patch
from pr_agent.secret_providers import get_secret_provider
class TestSecretProviderFactory:
def test_get_secret_provider_none_when_not_configured(self):
with patch('pr_agent.secret_providers.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = None
mock_get_settings.return_value = settings
result = get_secret_provider()
assert result is None
def test_get_secret_provider_google_cloud_storage(self):
with patch('pr_agent.secret_providers.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = "google_cloud_storage"
settings.config.secret_provider = "google_cloud_storage"
mock_get_settings.return_value = settings
with patch('pr_agent.secret_providers.google_cloud_storage_secret_provider.GoogleCloudStorageSecretProvider') as MockProvider:
mock_instance = MagicMock()
MockProvider.return_value = mock_instance
result = get_secret_provider()
assert result is mock_instance
MockProvider.assert_called_once()
def test_get_secret_provider_aws_secrets_manager(self):
with patch('pr_agent.secret_providers.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = "aws_secrets_manager"
settings.config.secret_provider = "aws_secrets_manager"
mock_get_settings.return_value = settings
with patch('pr_agent.secret_providers.aws_secrets_manager_provider.AWSSecretsManagerProvider') as MockProvider:
mock_instance = MagicMock()
MockProvider.return_value = mock_instance
result = get_secret_provider()
assert result is mock_instance
MockProvider.assert_called_once()
def test_get_secret_provider_unknown_provider(self):
with patch('pr_agent.secret_providers.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = "unknown_provider"
settings.config.secret_provider = "unknown_provider"
mock_get_settings.return_value = settings
with pytest.raises(ValueError, match="Unknown SECRET_PROVIDER"):
get_secret_provider()
def test_get_secret_provider_initialization_error(self):
with patch('pr_agent.secret_providers.get_settings') as mock_get_settings:
settings = MagicMock()
settings.get.return_value = "aws_secrets_manager"
settings.config.secret_provider = "aws_secrets_manager"
mock_get_settings.return_value = settings
with patch('pr_agent.secret_providers.aws_secrets_manager_provider.AWSSecretsManagerProvider') as MockProvider:
MockProvider.side_effect = Exception("Initialization failed")
with pytest.raises(ValueError, match="Failed to initialize aws_secrets_manager secret provider"):
get_secret_provider()