From 036dd51b79cac1b6e89150e45f939e9eec4e374b Mon Sep 17 00:00:00 2001 From: Roger Camargo Date: Fri, 12 Oct 2018 17:40:23 -0300 Subject: [PATCH 1/2] Added more detailed tests to explain the lib behaviour for all cases --- tests/test_basic_usage.py | 108 ++++++++++++++++++++++++++++++++++++-- tox.ini | 3 ++ 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/tests/test_basic_usage.py b/tests/test_basic_usage.py index 0f7903a..1058f76 100644 --- a/tests/test_basic_usage.py +++ b/tests/test_basic_usage.py @@ -1,10 +1,15 @@ import json import os import mock -from decouple_aws import get_config +import pytest +from botocore.exceptions import NoCredentialsError +from decouple import Config, AutoConfig, UndefinedValueError +from decouple_aws import get_config, RepositoryAwsSecretManager def test_should_return_secrets_from_aws_secrets_manager(): + """ Test if get_config returns Config (from AWS python decouple) """ + with mock.patch('decouple_aws.repository.boto3') as boto3: # Given secrets registered on AWS @@ -20,19 +25,112 @@ def test_should_return_secrets_from_aws_secrets_manager(): MY_EMAIL_USER = config('EMAIL_USER') # Then the secrets should be retrieved + is_aws_decouple = Config + + assert isinstance(config, is_aws_decouple) assert MY_EMAIL_USER == "user1" assert config('EMAIL_PASS') == '123456' -def test_should_use_environment_as_fall_back(): +def test_should_not_fail_when_the_aws_resource_is_not_found(): + """ Test if get_config returns AutoConfig (from python decouple) as fall back """ + + # Given an invalid resource name or + # no aws credentials + + # When + config = get_config('invalid/resource', 'us-west-2') + secret = config('EMAIL_USER', 'default-value') + + # Then + is_env_decouple = AutoConfig + + assert isinstance(config, is_env_decouple) + assert secret == 'default-value' + + +def test_should_return_secrets_from_aws_secrets_manager_repository(): + """ + Test if RepositoryAwsSecretManager can be instantiate direclty + """ + + with mock.patch('decouple_aws.repository.boto3') as boto3: + + # Given secrets registered on AWS + boto3.client().get_secret_value.return_value = { + 'SecretString': json.dumps({ + 'EMAIL_USER': 'user1', + 'EMAIL_PASS': '123456', + }) + } + + # When + repo = RepositoryAwsSecretManager('myproject/secrets1', 'ap-southeast-2') + config = Config(repo) + + # Then the secrets should be retrieved + assert config('EMAIL_USER') == "user1" + assert config('EMAIL_PASS') == '123456' + + +def test_should_raise_NoCredentialsError_with_no_credentials(): + """ Test if users can handle errors direclty when necessary """ + # Given no Credentials + + with pytest.raises(NoCredentialsError): + repo = RepositoryAwsSecretManager('myproject/secrets1', 'ap-southeast-2') + config = Config(repo) + + assert repo is not None + assert config is None + + +def test_should_use_environment_as_first_option(): + """ Test if ENVIRONMENT variable is the first option """ with mock.patch('decouple_aws.repository.boto3') as boto3: # Given an empty secrets manager and some OS environment boto3.client().get_secret_value.return_value = { - 'SecretString': json.dumps({}) + 'SecretString': json.dumps({ + 'MY_SECRET': 'secret-from-aws', + }) } - os.environ["EMAIL_USER"] = "user-test" + os.environ["MY_SECRET"] = "secret-from-environment" # When + config = get_config('myproject/secrets', 'ap-southeast-2') + assert config('MY_SECRET') == "secret-from-environment" + + +def test_should_use_environment_with_no_aws_credentials(): + """ Test if ENVIRONMENT variable is the first option """ + + # Given an empty secrets manager and some OS environment + os.environ["MY_SECRET"] = "secret-from-environment" + + # When + config = get_config('myproject/secrets', 'ap-southeast-2') + assert config('MY_SECRET') == "secret-from-environment" + + +def test_should_raise_UndefinedValueError_for_invalid_key(): + + # Given no secrets and no environment variables + + # When get_config used with no fail. Then an Exception is Raised + with pytest.raises(UndefinedValueError): config = get_config('myproject/secrets1', 'ap-southeast-2') - assert config('EMAIL_USER') == "user-test" + invalid_secret = config('MY_SECRET_NO_SET') + + assert invalid_secret is None + + +def test_should_use_default_value(): + + # Given no secrets and no environment variables + + # When get_config used with no fail. Then an Exception is Raised + config = get_config('myproject/secrets1', 'ap-southeast-2') + secret = config('MY_SECRET_NO_SET', 'my-default-value') + + assert secret == 'my-default-value' diff --git a/tox.ini b/tox.ini index 70c76e8..6463f7a 100644 --- a/tox.ini +++ b/tox.ini @@ -9,3 +9,6 @@ deps = boto3==1.9.16 python-decouple==3.1 commands=py.test + +[flake8] +max-line-length = 120 \ No newline at end of file From 2a1b9e47c51c3a6fcffbcb9c6570189dab06f5a2 Mon Sep 17 00:00:00 2001 From: Roger Camargo Date: Fri, 12 Oct 2018 17:55:25 -0300 Subject: [PATCH 2/2] Make it possible to use the same API whether you need to handle errors or not --- decouple_aws/__init__.py | 9 ++++++--- tests/test_basic_usage.py | 13 +++++++++++++ tox.ini | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/decouple_aws/__init__.py b/decouple_aws/__init__.py index 30462f6..d205613 100644 --- a/decouple_aws/__init__.py +++ b/decouple_aws/__init__.py @@ -1,7 +1,7 @@ import logging from decouple import AutoConfig, Config -from botocore.exceptions import BotoCoreError +from botocore.exceptions import ClientError, BotoCoreError from .repository import RepositoryAwsSecretManager @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) -def get_config(source, region): +def get_config(source, region, fail=False): """ Get config object but fallback to AutoConfig if AWS connection fails""" try: logger.debug( @@ -20,7 +20,10 @@ def get_config(source, region): logger.debug('Successfully queried for %s in region %s', source, region) return Config(repo) - except BotoCoreError as e: + + except (ClientError, BotoCoreError) as e: + if fail: + raise e logger.error( 'Failed retrieving secrets from AWS Secrets Manager: %s', e) return AutoConfig() diff --git a/tests/test_basic_usage.py b/tests/test_basic_usage.py index 1058f76..5d8589a 100644 --- a/tests/test_basic_usage.py +++ b/tests/test_basic_usage.py @@ -85,6 +85,19 @@ def test_should_raise_NoCredentialsError_with_no_credentials(): assert config is None +def test_should_raise_NoCredentialsError_using_the_default_api(): + """ + Test if users can handle errors direclty when necessary + using a default api rather than be forced to instantiate RepositoryAwsSecretManager + """ + # Given no Credentials + + with pytest.raises(NoCredentialsError): + config = get_config('myproject/secrets', 'ap-southeast-2', fail=True) + + assert config is None + + def test_should_use_environment_as_first_option(): """ Test if ENVIRONMENT variable is the first option """ with mock.patch('decouple_aws.repository.boto3') as boto3: diff --git a/tox.ini b/tox.ini index 6463f7a..4d2b63d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,4 @@ deps = commands=py.test [flake8] -max-line-length = 120 \ No newline at end of file +max-line-length = 120