Release v1.0.0

This commit is contained in:
2024-01-04 20:30:54 +01:00
commit 19c8d10645
45 changed files with 3745 additions and 0 deletions

0
tests/__init__.py Normal file
View File

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import pytest
from .fixtures import TestingSecrets
@pytest.fixture
def testing_secrets() -> TestingSecrets:
return TestingSecrets()

0
tests/ext/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import os
from unittest import mock
from keep_it_secret.ext import aws
@mock.patch.dict(
os.environ,
{
'AWS_ACCESS_KEY_ID': 'test_access_key_id',
'AWS_SECRET_ACCESS_KEY': 'test_secret_access_key',
'AWS_SESSION_TOKEN': 'test_aws_session_token',
'AWS_DEFAULT_REGION': 'test_aws_default_region',
},
)
def test_as_boto3_client_kwargs():
# Given
secrets = aws.AWSSecrets()
# When
result = secrets.as_boto3_client_kwargs()
# Then
assert result == {
'aws_access_key_id': 'test_access_key_id',
'aws_secret_access_key': 'test_secret_access_key',
'aws_session_token': 'test_aws_session_token',
'region_name': 'test_aws_default_region',
}
@mock.patch.dict(os.environ, {}, clear=True)
def test_as_boto3_client_kwargs_empty():
# Given
secrets = aws.AWSSecrets()
# When
result = secrets.as_boto3_client_kwargs()
# Then
assert result == {}

View File

@@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import datetime
import json
from unittest import mock
import boto3
import moto
import pytest
from pytest_mock import MockerFixture
from keep_it_secret.ext import aws
from keep_it_secret.secrets import Secrets
class TestingAWSSecrets(Secrets):
aws = aws.AWSSecrets()
@pytest.fixture
def aws_secrets_manager_client() -> mock.Mock:
return mock.Mock(spec=['get_secret_value'])
@pytest.fixture
def mock_boto3_client(mocker: MockerFixture) -> mock.Mock:
return mocker.patch.object(aws.boto3, 'client')
@pytest.fixture
def testing_aws_secrets() -> TestingAWSSecrets:
return TestingAWSSecrets()
def test_init():
# When
result = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
# Then
assert result.secret_id == 'keep_it_secret/tests/spam'
assert result.default is None
assert result.decoder == json.loads
assert result.client is None
def test_init_with_default():
# When
result = aws.AWSSecretsManagerField(
'keep_it_secret/tests/spam', default='eggs',
)
# Then
assert result.default == 'eggs'
def test_init_with_decoder():
# When
result = aws.AWSSecretsManagerField(
'keep_it_secret/tests/spam', decoder=int,
)
# Then
assert result.decoder == int
def test_init_with_field_options():
# When
result = aws.AWSSecretsManagerField(
'keep_it_secret/tests/spam',
as_type=int,
required=False,
description='spameggs',
)
# Then
assert result.as_type is None
assert result.required is False
assert result.description == 'spameggs'
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
aws.AWSSecretsManagerField, '__init__', return_value=None,
)
# When
_ = aws.AWSSecretsManagerField.new(
'keep_it_secret/tests/spam',
default='eggs',
decoder=json.dumps,
as_type=int,
required=False,
description='spameggs',
)
# Then
mock_init.assert_called_once_with(
'keep_it_secret/tests/spam',
default='eggs',
decoder=json.dumps,
as_type=int,
required=False,
description='spameggs',
)
def test_get_client_cache_miss(mock_boto3_client: mock.Mock,
aws_secrets_manager_client: mock.Mock,
testing_aws_secrets: TestingAWSSecrets):
# Given
mock_boto3_client.return_value = aws_secrets_manager_client
field = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
# When
result = field.get_client(testing_aws_secrets)
# Then
assert result == aws_secrets_manager_client
assert field.client == aws_secrets_manager_client
mock_boto3_client.assert_called_once_with(
'secretsmanager', **testing_aws_secrets.aws.as_boto3_client_kwargs(),
)
def test_get_client_cache_hit(mock_boto3_client: mock.Mock,
aws_secrets_manager_client: mock.Mock,
testing_aws_secrets: TestingAWSSecrets):
# Given
field = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
field.client = aws_secrets_manager_client
# When
result = field.get_client(testing_aws_secrets)
# Then
assert result == aws_secrets_manager_client
mock_boto3_client.assert_not_called()
def test_get_value_aws_dependency_missing(mocker: MockerFixture,
aws_secrets_manager_client: mock.Mock,
testing_secrets: Secrets):
# Given
field = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
mock_field_get_client = mocker.patch.object(
field, 'get_client', return_value=aws_secrets_manager_client,
)
with pytest.raises(field.DependencyMissing) as exception_info:
# When
_ = field(testing_secrets)
# Then
assert exception_info.value.args[0] == 'aws'
mock_field_get_client.assert_not_called()
aws_secrets_manager_client.get_secret_value.assert_not_called()
@moto.mock_secretsmanager
def test_get_value_required_value_not_found(testing_aws_secrets: Secrets):
# Given
field = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
with pytest.raises(field.RequiredValueMissing) as exception_info:
# When
_ = field(testing_aws_secrets)
# Then
assert exception_info.value.args[0] == 'keep_it_secret/tests/spam'
@moto.mock_secretsmanager
def test_get_value_not_found_not_required(testing_aws_secrets: Secrets):
# Given
field = aws.AWSSecretsManagerField(
'keep_it_secret/tests/spam', required=False,
)
result = field(testing_aws_secrets)
# Then
assert result is None
@moto.mock_secretsmanager
def test_get_value(testing_aws_secrets: Secrets):
# Given
aws_secrets_manager_client = boto3.client('secretsmanager')
aws_secrets_manager_client.create_secret(
Name='keep_it_secret/tests/spam',
SecretString='{"spam":true}',
)
field = aws.AWSSecretsManagerField('keep_it_secret/tests/spam')
# When
result = field(testing_aws_secrets)
# Then
assert result == {"spam": True}
@moto.mock_secretsmanager
def test_get_value_with_custom_decoder(testing_aws_secrets: Secrets):
# Given
aws_secrets_manager_client = boto3.client('secretsmanager')
aws_secrets_manager_client.create_secret(
Name='keep_it_secret/tests/spam',
SecretString='1987-10-03T08:00:00',
)
field = aws.AWSSecretsManagerField(
'keep_it_secret/tests/spam', decoder=datetime.datetime.fromisoformat,
)
# When
result = field(testing_aws_secrets)
# Then
assert result == datetime.datetime(1987, 10, 3, 8, 0, 0)

View File

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from keep_it_secret.ext import loader
from tests.fixtures import TestingSecrets
def test_load_secrets():
# When
result = loader.load_secrets('tests.fixtures', 'testing', 'example')
# Then
assert isinstance(result, TestingSecrets)

0
tests/fields/__init__.py Normal file
View File

View File

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import pytest
from pytest_mock import MockerFixture
from keep_it_secret import fields
from keep_it_secret.secrets import Secrets
def test_init():
# When
_ = fields.AbstractField()
# Then
assert True # Nothing to test here ;)
def test_init_with_field_options():
# When
result = fields.AbstractField(
as_type=int, required=False, description='spameggs',
)
# Then
assert result.as_type == int
assert result.required is False
assert result.description == 'spameggs'
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
fields.AbstractField, '__init__', return_value=None,
)
# When
_ = fields.AbstractField.new(
as_type=int,
required=False,
description='spameggs',
)
# Then
mock_init.assert_called_once_with(
as_type=int,
required=False,
description='spameggs',
)
def test_get_value(testing_secrets: Secrets):
# Given
field = fields.AbstractField()
with pytest.raises(NotImplementedError) as exception_info:
# When
_ = field.get_value(testing_secrets)
# Then
assert field.name in exception_info.value.args[0]

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import pytest
from pytest_mock import MockerFixture
from keep_it_secret import fields
from keep_it_secret.secrets import Secrets
def test_init():
# When
result = fields.EnvField('KEEP_IT_SECRET_TESTS_SPAM')
# Then
assert result.key == 'KEEP_IT_SECRET_TESTS_SPAM'
assert result.default is None
def test_init_with_default():
# When
result = fields.EnvField('KEEP_IT_SECRET_TESTS_SPAM', default='eggs')
# Then
assert result.default == 'eggs'
def test_init_with_field_options():
# When
result = fields.EnvField(
'KEEP_IT_SECRET_TESTS_SPAM',
as_type=int,
required=False,
description='spameggs',
)
# Then
assert result.as_type == int
assert result.required is False
assert result.description == 'spameggs'
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
fields.EnvField, '__init__', return_value=None,
)
# When
_ = fields.EnvField.new(
'KEEP_IT_SECRET_TESTS_SPAM',
default='eggs',
as_type=int,
required=False,
description='spameggs',
)
# Then
mock_init.assert_called_once_with(
'KEEP_IT_SECRET_TESTS_SPAM',
default='eggs',
as_type=int,
required=False,
description='spameggs',
)
def test_get_value(testing_secrets: Secrets):
# Given
field = fields.EnvField('KEEP_IT_SECRET_TESTS_SPAM')
# When
result = field.get_value(testing_secrets)
# Then
assert result == 'spam'
def test_get_value_required_value_missing(testing_secrets: Secrets):
# Given
field = fields.EnvField('KEEP_IT_SECRET_TESTS_IDONTEXIST', default=None)
with pytest.raises(field.RequiredValueMissing) as exception_info:
# When
_ = field.get_value(testing_secrets)
# Then
assert exception_info.value.args[0] == 'KEEP_IT_SECRET_TESTS_IDONTEXIST'
def test_get_value_missing_not_required(testing_secrets: Secrets):
# Given
field = fields.EnvField('KEEP_IT_SECRET_TESTS_IDONTEXIST', required=False)
# When
result = field.get_value(testing_secrets)
# Then
assert result == field.default

117
tests/fields/test_Field.py Normal file
View File

@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import pytest
from pytest_mock import MockerFixture
from keep_it_secret import fields
from keep_it_secret.secrets import Secrets
class TestingField(fields.Field):
def get_value(self, secrets):
return 'test'
def test_init():
# When
result = TestingField()
# Then
assert result.as_type == str
assert result.required is True
assert result.description is None
assert isinstance(result.name, str) is True
assert len(result.name) > 0
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
TestingField, '__init__', return_value=None,
)
# When
_ = TestingField.new(
as_type=int,
required=False,
description='spameggs',
)
# Then
mock_init.assert_called_once_with(
as_type=int,
required=False,
description='spameggs',
)
@pytest.mark.parametrize(
'argument,value',
[
('as_type', int),
('required', False),
('description', 'spam'),
],
)
def test_init_with_kwargs(argument, value):
# When
result = TestingField(**{argument: value})
# Then
assert getattr(result, argument) == value
def test_call(mocker: MockerFixture, testing_secrets: Secrets):
# Given
field = TestingField()
# When
result = field(testing_secrets)
# Then
assert result == 'test'
def test_call_get_value_call(mocker: MockerFixture, testing_secrets: Secrets):
# Given
field = TestingField()
mock_get_value = mocker.patch.object(
field, 'get_value', return_value='spam',
)
# When
result = field(testing_secrets)
# Then
assert result == 'spam'
mock_get_value.assert_called_once_with(testing_secrets)
def test_call_get_value_None(mocker: MockerFixture, testing_secrets: Secrets):
# Given
field = TestingField(as_type=int)
_ = mocker.patch.object(field, 'get_value', return_value=None)
# When
result = field(testing_secrets)
# Then
assert result is None
def test_call_with_as_type(mocker: MockerFixture, testing_secrets: Secrets):
# Given
field = TestingField(as_type=int)
_ = mocker.patch.object(field, 'get_value', return_value='5')
# When
result = field(testing_secrets)
# Then
assert result == 5

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from pytest_mock import MockerFixture
from keep_it_secret import fields
from keep_it_secret.secrets import Secrets
def test_init():
# When
result = fields.LiteralField('spam')
# Then
assert result.as_type is None
assert result.value == 'spam'
def test_init_with_field_options():
# When
result = fields.LiteralField(
'spam', as_type=int, required=False, description='eggs',
)
# Then
assert result.as_type is None
assert result.required is False
assert result.description == 'eggs'
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
fields.LiteralField, '__init__', return_value=None,
)
# When
_ = fields.LiteralField.new(
'spam', as_type=int, required=False, description='eggs',
)
# Then
mock_init.assert_called_once_with(
'spam', as_type=int, required=False, description='eggs',
)
def test_get_value(testing_secrets: Secrets):
# Given
field = fields.LiteralField('spam')
# When
result = field.get_value(testing_secrets)
# Then
assert result == 'spam'

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from unittest import mock
from pytest_mock import MockerFixture
from keep_it_secret import fields
from keep_it_secret.secrets import Secrets
def test_init():
# When
result = fields.SecretsField(Secrets)
# Then
assert result.as_type is None
assert result.klass == Secrets
def test_init_with_field_options():
# When
result = fields.SecretsField(
Secrets, as_type=int, required=False, description='eggs',
)
# Then
assert result.as_type is None
assert result.required is False
assert result.description == 'eggs'
def test_new(mocker: MockerFixture):
# Given
mock_init = mocker.patch.object(
fields.SecretsField, '__init__', return_value=None,
)
# When
_ = fields.SecretsField.new(
Secrets, as_type=int, required=False, description='eggs',
)
# Then
mock_init.assert_called_once_with(
Secrets, as_type=int, required=False, description='eggs',
)
def test_get_value(testing_secrets: Secrets):
# Given
wrapped_secrets = mock.Mock(spec=Secrets)
mock_secrets_class = mock.Mock(return_value=wrapped_secrets)
field = fields.SecretsField(mock_secrets_class)
# When
result = field.get_value(testing_secrets)
# Then
assert result == wrapped_secrets
mock_secrets_class.assert_called_once_with(parent=testing_secrets)

2
tests/fixtures/__init__.py vendored Normal file
View File

@@ -0,0 +1,2 @@
# type: ignore
from .models import TestingSecrets # noqa: F401

11
tests/fixtures/models.py vendored Normal file
View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from keep_it_secret.fields import EnvField, LiteralField
from keep_it_secret.secrets import Secrets
class TestingSecrets(Secrets):
spam: str = EnvField.new('KEEP_IT_SECRET_TESTS_SPAM')
eggs: str = LiteralField.new('eggs')

0
tests/fixtures/testing/__init__.py vendored Normal file
View File

7
tests/fixtures/testing/example.py vendored Normal file
View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from tests.fixtures.models import TestingSecrets
__secrets__ = TestingSecrets()

View File

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from keep_it_secret import secrets
from tests.fixtures import TestingSecrets
class ParentSecrets(secrets.Secrets):
pass
def test_init():
# When
result = TestingSecrets()
# Then
assert isinstance(result, TestingSecrets)
assert result.__secrets_parent__ is None
assert result.__secrets_data__ == {'spam': 'spam', 'eggs': 'eggs'}
def test_init_with_parent():
# Given
parent_secrets = ParentSecrets()
# When
result = TestingSecrets(parent=parent_secrets)
# Then
assert result.__secrets_parent__ is parent_secrets
def test_field_property(testing_secrets: TestingSecrets):
# When
result = testing_secrets.spam
# Then
assert result == testing_secrets.__secrets_data__['spam']

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import types
from keep_it_secret import secrets
from keep_it_secret.fields import EnvField, LiteralField
class TestingSecrets(secrets.Secrets):
spam: str = LiteralField.new('spam')
something = False
def do_something(self):
return True
class SubclassSecrets(TestingSecrets):
spam: str = EnvField.new('KEEP_IT_SECRET_TESTS_SPAM')
eggs: str = LiteralField.new('eggs')
def test_generic_attribute_initialization():
# Then
assert TestingSecrets.__class__ is secrets.SecretsBase
assert isinstance(TestingSecrets.__init__, types.FunctionType)
def test_secrets_dunder_attribute_initialization():
# Then
assert hasattr(TestingSecrets, '__secrets_fields__')
assert 'spam' in TestingSecrets.__secrets_fields__
assert isinstance(TestingSecrets.__secrets_fields__['spam'], LiteralField) is True
assert TestingSecrets.__secrets_fields__['spam'].name == 'TestingSecrets.spam'
def test_secret_properties_initialization():
# Then
assert hasattr(TestingSecrets, 'spam')
assert isinstance(TestingSecrets.spam, property)
def test_subclass_attribute_initialization():
# Then
assert hasattr(TestingSecrets, 'something')
assert TestingSecrets.something is False
assert hasattr(TestingSecrets, 'do_something')
assert isinstance(TestingSecrets.do_something, types.FunctionType)
def test_inheritance():
# Then
assert isinstance(SubclassSecrets.__secrets_fields__['spam'], EnvField) is True
assert isinstance(SubclassSecrets.__secrets_fields__['eggs'], LiteralField) is True
assert hasattr(SubclassSecrets, 'do_something')
assert isinstance(SubclassSecrets.do_something, types.FunctionType)
def test_multiple_inheritance():
# Given
class TestingMixin:
def do_something_else(self):
return False
class SubclassWithMixin(TestingMixin, SubclassSecrets):
spameggs = LiteralField.new('spameggs')
# Then
assert isinstance(SubclassWithMixin.__secrets_fields__['spam'], EnvField) is True
assert isinstance(SubclassWithMixin.__secrets_fields__['eggs'], LiteralField) is True
assert isinstance(SubclassWithMixin.__secrets_fields__['spameggs'], LiteralField) is True
assert hasattr(SubclassWithMixin, 'do_something')
assert isinstance(SubclassWithMixin.do_something, types.FunctionType)
assert hasattr(SubclassWithMixin, 'do_something_else')
assert isinstance(SubclassWithMixin.do_something_else, types.FunctionType)

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
from unittest import mock
import pytest
from pytest_mock import MockFixture
from keep_it_secret import secrets
from keep_it_secret.secrets import Secrets
class TestingSecrets(Secrets):
pass
@pytest.fixture
def mock_field(mocker: MockFixture) -> mock.Mock:
result = mocker.Mock(return_value='spam')
result.description = None
return result
@pytest.fixture
def testing_secrets() -> TestingSecrets:
yield TestingSecrets()
TestingSecrets.__secrets_fields__ = {}
def test_getter_cache_miss(mock_field: mock.Mock, testing_secrets: TestingSecrets):
# Given
testing_secrets.__secrets_fields__['spam'] = mock_field
# When
result = secrets.field_property_factory('spam', mock_field).fget(testing_secrets)
# Then
assert result == 'spam'
mock_field.assert_called_once_with(testing_secrets)
def test_getter_cache_hit(mock_field: mock.Mock, testing_secrets: TestingSecrets):
# Given
testing_secrets.__secrets_fields__['spam'] = mock_field
testing_secrets.__secrets_data__['spam'] = 'eggs'
# When
result = secrets.field_property_factory('spam', mock_field).fget(testing_secrets)
# Then
assert result == 'eggs'
mock_field.assert_not_called()