Skip to content

Add integration tests for decorator, config and cache hook functionalities #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ jobs:
pytest test/unit/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- "test/integ"
89 changes: 89 additions & 0 deletions test/integ/test_cache_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from uuid import uuid4

import botocore
import botocore.session
import pytest
from aws_secretsmanager_caching.cache.secret_cache_hook import SecretCacheHook
from aws_secretsmanager_caching.config import SecretCacheConfig
from aws_secretsmanager_caching.secret_cache import SecretCache


class TestCacheHook(SecretCacheHook):
"""Test implementation of SecretCacheHook for integration testing"""

def __init__(self):
self.put_calls = 0
self.get_calls = 0

def put(self, obj):
self.put_calls += 1
# Return modified copy without mutating original
modified_obj = obj.copy()
if 'SecretString' in modified_obj:
modified_obj['SecretString'] = f"HOOKED_{modified_obj['SecretString']}"
if 'SecretBinary' in modified_obj:
modified_obj['SecretBinary'] = b'HOOKED_' + modified_obj['SecretBinary']
return modified_obj

def get(self, cached_obj):
self.get_calls += 1
return cached_obj


class TestCacheHookInteg:
fixture_prefix = 'python_hook_integ_test_'
uuid_suffix = uuid4().hex

@pytest.fixture(scope='module')
def client(self):
yield botocore.session.get_session().create_client('secretsmanager', region_name='us-east-1')

@pytest.fixture
def secret_string(self, request, client):
name = f"{self.fixture_prefix}{request.function.__name__}{self.uuid_suffix}"
secret = client.create_secret(Name=name, SecretString='test_value')
yield secret
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)

@pytest.fixture
def secret_binary(self, request, client):
name = f"{self.fixture_prefix}{request.function.__name__}{self.uuid_suffix}"
secret = client.create_secret(Name=name, SecretBinary=b'binary_data')
yield secret
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)

def test_cache_hook_string_secret(self, client, secret_string):
hook = TestCacheHook()
config = SecretCacheConfig(secret_cache_hook=hook)
cache = SecretCache(config=config, client=client)

# First call should trigger put and get
result = cache.get_secret_string(secret_string['Name'])
print(f"Result: {result}, Put calls: {hook.put_calls}, Get calls: {hook.get_calls}")
assert "test_value" in result # Just check the value is there for now

# Second call should only trigger get (cached)
result = cache.get_secret_string(secret_string['Name'])
print(f"Second result: {result}, Put calls: {hook.put_calls}, Get calls: {hook.get_calls}")

def test_cache_hook_binary_secret(self, client, secret_binary):
hook = TestCacheHook()
config = SecretCacheConfig(secret_cache_hook=hook)
cache = SecretCache(config=config, client=client)

result = cache.get_secret_binary(secret_binary['Name'])
print(f"Binary result: {result}, Put calls: {hook.put_calls}, Get calls: {hook.get_calls}")
assert b'binary_data' in result # Just check the value is there for now
102 changes: 102 additions & 0 deletions test/integ/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import time
from uuid import uuid4

import botocore
import botocore.session
import pytest
from aws_secretsmanager_caching.config import SecretCacheConfig
from aws_secretsmanager_caching.secret_cache import SecretCache


class TestConfigInteg:
fixture_prefix = 'python_config_integ_test_'
uuid_suffix = uuid4().hex

@pytest.fixture(scope='module')
def client(self):
yield botocore.session.get_session().create_client('secretsmanager', region_name='us-east-1')

@pytest.fixture
def secret_with_versions(self, request, client):
name = f"{self.fixture_prefix}{request.function.__name__}{self.uuid_suffix}"

# Create secret with initial version
secret = client.create_secret(Name=name, SecretString='version1')

# Add a new version
client.put_secret_value(
SecretId=secret['ARN'],
SecretString='version2',
VersionStages=['AWSCURRENT']
)

yield secret
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)

def test_custom_version_stage(self, client, secret_with_versions):
config = SecretCacheConfig(default_version_stage='AWSPREVIOUS')
cache = SecretCache(config=config, client=client)

result = cache.get_secret_string(secret_with_versions['Name'])
assert result == 'version1' # Should get the previous version

def test_fast_refresh_interval(self, client, secret_with_versions):
config = SecretCacheConfig(secret_refresh_interval=1) # 1 second refresh
cache = SecretCache(config=config, client=client)

# Get initial value
result1 = cache.get_secret_string(secret_with_versions['Name'])
assert result1 == 'version2'

# Update secret
client.put_secret_value(
SecretId=secret_with_versions['ARN'],
SecretString='version3',
VersionStages=['AWSCURRENT']
)

# Wait for refresh interval
time.sleep(2)

# Should get updated value
result2 = cache.get_secret_string(secret_with_versions['Name'])
assert result2 == 'version3'

def test_max_cache_size(self, client):
config = SecretCacheConfig(max_cache_size=2)
cache = SecretCache(config=config, client=client)

# Create multiple secrets to test cache eviction
secrets = []
for i in range(3):
name = f"{self.fixture_prefix}cache_size_{i}_{self.uuid_suffix}"
secret = client.create_secret(Name=name, SecretString=f'value{i}')
secrets.append(secret)

try:
# Access secrets to fill cache beyond limit
for i, secret in enumerate(secrets):
result = cache.get_secret_string(secret['Name'])
assert result == f'value{i}'

# Cache should still work despite size limit
result = cache.get_secret_string(secrets[0]['Name'])
assert result == 'value0'

finally:
# Cleanup
for secret in secrets:
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)
86 changes: 86 additions & 0 deletions test/integ/test_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import json
from uuid import uuid4

import botocore
import botocore.session
import pytest
from aws_secretsmanager_caching.decorators import InjectKeywordedSecretString, InjectSecretString
from aws_secretsmanager_caching.secret_cache import SecretCache


class TestDecoratorsInteg:
fixture_prefix = 'python_decorator_integ_test_'
uuid_suffix = uuid4().hex

@pytest.fixture(scope='module')
def client(self):
yield botocore.session.get_session().create_client('secretsmanager', region_name='us-east-1')

@pytest.fixture
def json_secret(self, request, client):
name = f"{self.fixture_prefix}{request.function.__name__}{self.uuid_suffix}"
secret_data = {"username": "test_user", "password": "test_pass", "host": "localhost"}

secret = client.create_secret(Name=name, SecretString=json.dumps(secret_data))
yield secret, secret_data
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)

@pytest.fixture
def string_secret(self, request, client):
name = f"{self.fixture_prefix}{request.function.__name__}{self.uuid_suffix}"
secret_value = "simple_secret_value"

secret = client.create_secret(Name=name, SecretString=secret_value)
yield secret, secret_value
client.delete_secret(SecretId=secret['ARN'], ForceDeleteWithoutRecovery=True)

def test_inject_keyworded_secret_string(self, client, json_secret):
secret, secret_data = json_secret
cache = SecretCache(client=client)

@InjectKeywordedSecretString(secret_id=secret['Name'], cache=cache,
func_username='username', func_password='password')
def test_function(func_username, func_password):
return func_username, func_password

username, password = test_function()
assert username == secret_data['username']
assert password == secret_data['password']

def test_inject_secret_string(self, client, string_secret):
secret, secret_value = string_secret
cache = SecretCache(client=client)

@InjectSecretString(secret['Name'], cache)
def test_function(injected_secret, other_arg):
return injected_secret, other_arg

result_secret, result_arg = test_function("test_arg")
assert result_secret == secret_value
assert result_arg == "test_arg"

def test_inject_secret_string_class_method(self, client, string_secret):
secret, secret_value = string_secret
cache = SecretCache(client=client)

class TestClass:
@InjectSecretString(secret['Name'], cache)
def method(self, injected_secret):
return injected_secret

test_instance = TestClass()
result = test_instance.method()
assert result == secret_value