diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/community/hashi_vault/tests/unit | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip |
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/hashi_vault/tests/unit')
43 files changed, 3428 insertions, 279 deletions
diff --git a/ansible_collections/community/hashi_vault/tests/unit/conftest.py b/ansible_collections/community/hashi_vault/tests/unit/conftest.py index 862e93cf6..7b07c1b1e 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/conftest.py +++ b/ansible_collections/community/hashi_vault/tests/unit/conftest.py @@ -78,5 +78,15 @@ def patch_get_vault_client(vault_client): def requests_unparseable_response(): r = mock.MagicMock() r.json.side_effect = json.JSONDecodeError + return r + +# https://github.com/hvac/hvac/issues/797 +@pytest.fixture +def empty_response(requests_unparseable_response): + r = requests_unparseable_response + r.status_code = 204 + r._content = b"" + r.content = b"" + r.text = "" return r diff --git a/ansible_collections/community/hashi_vault/tests/unit/constraints.txt b/ansible_collections/community/hashi_vault/tests/unit/constraints.txt new file mode 100644 index 000000000..f347921c3 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/constraints.txt @@ -0,0 +1,64 @@ +coverage >= 4.2, < 5.0.0, != 4.3.2 ; python_version <= '3.7' # features in 4.2+ required, avoid known bug in 4.3.2 on python 2.6, coverage 5.0+ incompatible +coverage >= 4.5.4, < 5.0.0 ; python_version > '3.7' # coverage had a bug in < 4.5.4 that would cause unit tests to hang in Python 3.8, coverage 5.0+ incompatible +cryptography < 2.2 ; python_version < '2.7' # cryptography 2.2 drops support for python 2.6 +deepdiff < 4.0.0 ; python_version < '3' # deepdiff 4.0.0 and later require python 3 +jinja2 < 2.11 ; python_version < '2.7' # jinja2 2.11 and later require python 2.7 or later +urllib3 < 1.24 ; python_version < '2.7' # urllib3 1.24 and later require python 2.7 or later +pywinrm >= 0.3.0 # message encryption support +sphinx < 1.6 ; python_version < '2.7' # sphinx 1.6 and later require python 2.7 or later +sphinx < 1.8 ; python_version >= '2.7' # sphinx 1.8 and later are currently incompatible with rstcheck 3.3 +pygments >= 2.4.0 # Pygments 2.4.0 includes bugfixes for YAML and YAML+Jinja lexers +wheel < 0.30.0 ; python_version < '2.7' # wheel 0.30.0 and later require python 2.7 or later +yamllint != 1.8.0, < 1.14.0 ; python_version < '2.7' # yamllint 1.8.0 and 1.14.0+ require python 2.7+ +pycrypto >= 2.6 # Need features found in 2.6 and greater +ncclient >= 0.5.2 # Need features added in 0.5.2 and greater +idna < 2.6, >= 2.5 # linode requires idna < 2.9, >= 2.5, requests requires idna < 2.6, but cryptography will cause the latest version to be installed instead +paramiko < 2.4.0 ; python_version < '2.7' # paramiko 2.4.0 drops support for python 2.6 +pytest < 3.3.0 ; python_version < '2.7' # pytest 3.3.0 drops support for python 2.6 +pytest < 5.0.0 ; python_version == '2.7' # pytest 5.0.0 and later will no longer support python 2.7 +pytest-forked < 1.0.2 ; python_version < '2.7' # pytest-forked 1.0.2 and later require python 2.7 or later +pytest-forked >= 1.0.2 ; python_version >= '2.7' # pytest-forked before 1.0.2 does not work with pytest 4.2.0+ (which requires python 2.7+) +ntlm-auth >= 1.3.0 # message encryption support using cryptography +requests < 2.20.0 ; python_version < '2.7' # requests 2.20.0 drops support for python 2.6 +requests-ntlm >= 1.1.0 # message encryption support +requests-credssp >= 0.1.0 # message encryption support +voluptuous >= 0.11.0 # Schema recursion via Self +openshift >= 0.6.2, < 0.9.0 # merge_type support +virtualenv < 16.0.0 ; python_version < '2.7' # virtualenv 16.0.0 and later require python 2.7 or later +pathspec < 0.6.0 ; python_version < '2.7' # pathspec 0.6.0 and later require python 2.7 or later +pyopenssl < 18.0.0 ; python_version < '2.7' # pyOpenSSL 18.0.0 and later require python 2.7 or later +pyfmg == 0.6.1 # newer versions do not pass current unit tests +pyyaml < 5.1 ; python_version < '2.7' # pyyaml 5.1 and later require python 2.7 or later +pycparser < 2.19 ; python_version < '2.7' # pycparser 2.19 and later require python 2.7 or later +mock >= 2.0.0 # needed for features backported from Python 3.6 unittest.mock (assert_called, assert_called_once...) +pytest-mock >= 1.4.0 # needed for mock_use_standalone_module pytest option +xmltodict < 0.12.0 ; python_version < '2.7' # xmltodict 0.12.0 and later require python 2.7 or later +lxml < 4.3.0 ; python_version < '2.7' # lxml 4.3.0 and later require python 2.7 or later +pyvmomi < 6.0.0 ; python_version < '2.7' # pyvmomi 6.0.0 and later require python 2.7 or later +pyone == 1.1.9 # newer versions do not pass current integration tests +boto3 < 1.11 ; python_version < '2.7' # boto3 1.11 drops Python 2.6 support +botocore >= 1.10.0, < 1.14 ; python_version < '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca; botocore 1.14 drops Python 2.6 support +botocore >= 1.10.0 ; python_version >= '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca +setuptools < 45 ; python_version <= '2.7' # setuptools 45 and later require python 3.5 or later +cffi >= 1.14.2, != 1.14.3 # Yanked version which older versions of pip will still install: + +# freeze pylint and its requirements for consistent test results +astroid == 2.2.5 +isort == 4.3.15 +lazy-object-proxy == 1.3.1 +mccabe == 0.6.1 +pylint == 2.3.1 +typed-ast == 1.4.0 # 1.4.0 is required to compile on Python 3.8 +wrapt == 1.11.1 + +# hvac +hvac >= 1.2.1 ; python_version >= '3.6' + +# urllib3 +# these should be satisfied naturally by the requests versions required by hvac anyway +urllib3 >= 1.15 ; python_version >= '3.6' # we need raise_on_status for retry support to raise the correct exceptions https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#115-2016-04-06 + +# requests +# https://github.com/psf/requests/pull/6356 +requests >= 2.29 ; python_version >= '3.7' +requests < 2.28 ; python_version < '3.7' diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connection_read_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connection_read_response.json new file mode 100644 index 000000000..36c4429a8 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connection_read_response.json @@ -0,0 +1,20 @@ +{ + "auth": null, + "data": { + "allowed_roles": [], + "connection_details": { + "connection_url": "postgresql://{{username}}:{{password}}@postgres:5432/postgres?sslmode=disable", + "username": "UserName" + }, + "password_policy": "", + "plugin_name": "postgresql-database-plugin", + "plugin_version": "", + "root_credentials_rotate_statements": [] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connections_list_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connections_list_response.json new file mode 100644 index 000000000..c5abad4ce --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_connections_list_response.json @@ -0,0 +1,21 @@ +{ + "auth": null, + "data": { + "keys": [ + "con1", + "con2", + "con3" + ] + }, + "connections": [ + "con1", + "con2", + "con3" + ], + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_role_read_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_role_read_response.json new file mode 100644 index 000000000..c840ad1cb --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_role_read_response.json @@ -0,0 +1,22 @@ +{ + "auth": null, + "data": { + "creation_statements": [ + "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';", + "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" + ], + "credential_type": "password", + "db_name": "SomeConnection", + "default_ttl": 3600, + "max_ttl": 86400, + "renew_statements": [], + "revocation_statements": [], + "rollback_statements": [] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_roles_list_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_roles_list_response.json new file mode 100644 index 000000000..00062fa20 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_roles_list_response.json @@ -0,0 +1,21 @@ +{ + "auth": null, + "data": { + "keys": [ + "dyn_role1", + "dyn_role2", + "dyn_role3" + ] + }, + "roles": [ + "dyn_role1", + "dyn_role2", + "dyn_role3" + ], + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_get_credentials_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_get_credentials_response.json new file mode 100644 index 000000000..34e901a12 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_get_credentials_response.json @@ -0,0 +1,25 @@ +{ + "data": { + "last_vault_rotation": "2024-01-01T09:00:00+01:00", + "password": "Th3_$3cr3t_P@ss!", + "rotation_period": 86400, + "ttl": 123456, + "username": "SomeUser" + }, + "raw": { + "auth": null, + "data": { + "last_vault_rotation": "2024-01-01T09:00:00+01:00", + "password": "Th3_$3cr3t_P@ss!", + "rotation_period": 86400, + "ttl": 123456, + "username": "SomeUser" + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null + } +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_read_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_read_response.json new file mode 100644 index 000000000..cd4cbdc63 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_role_read_response.json @@ -0,0 +1,18 @@ +{ + "auth": null, + "data": { + "credential_type": "password", + "db_name": "SomeConnection", + "last_vault_rotation": "2024-01-01T09:00:00 +01:00", + "rotation_period": 86400, + "rotation_statements": [ + "ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';" + ] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_roles_list_response.json b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_roles_list_response.json new file mode 100644 index 000000000..be8700112 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/fixtures/database_static_roles_list_response.json @@ -0,0 +1,21 @@ +{ + "auth": null, + "data": { + "keys": [ + "role1", + "role2", + "role3" + ] + }, + "roles": [ + "role1", + "role2", + "role3" + ], + "lease_duration": 0, + "lease_id": "", + "renewable": false, + "request_id": "91909ec0-cd89-489c-a7cf-2a82d2258b4d", + "warnings": null, + "wrap_info": null +} diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_token_create.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_token_create.py index 05a74f8b4..37404d9a2 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_token_create.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_token_create.py @@ -6,7 +6,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import sys import pytest from ansible.plugins.loader import lookup_loader @@ -150,11 +149,7 @@ class TestVaultTokenCreateLookup(object): "lookup result did not match expected result:\nlookup: %r\nexpected: %r" % (result, token_create_response) ) - if sys.version_info < (3, 8): - # TODO: remove when python < 3.8 is dropped - assert pass_thru_options.items() <= client.auth.token.create.call_args[1].items() - else: - assert pass_thru_options.items() <= client.auth.token.create.call_args.kwargs.items() + assert pass_thru_options.items() <= client.auth.token.create.call_args.kwargs.items() def test_vault_token_create_orphan_options( self, vault_token_create_lookup, authenticator, minimal_vars, pass_thru_options, orphan_option_translation, token_create_response @@ -175,11 +170,7 @@ class TestVaultTokenCreateLookup(object): "lookup result did not match expected result:\nlookup: %r\nexpected: %r" % (result, token_create_response) ) - if sys.version_info < (3, 8): - # TODO: remove when python < 3.8 is dropped - call_kwargs = client.auth.token.create_orphan.call_args[1] - else: - call_kwargs = client.auth.token.create_orphan.call_args.kwargs + call_kwargs = client.auth.token.create_orphan.call_args.kwargs for name, orphan in orphan_option_translation.items(): assert name not in call_kwargs, ( diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_write.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_write.py index c3c325228..eaac0ff08 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_write.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/lookup/test_vault_write.py @@ -68,18 +68,18 @@ class TestVaultWriteLookup(object): def test_vault_write_return_data(self, vault_write_lookup, minimal_vars, approle_secret_id_write_response, vault_client, paths, data, wrap_ttl): client = vault_client - expected_calls = [mock.call(path=p, wrap_ttl=wrap_ttl, **data) for p in paths] + expected_calls = [mock.call(path=p, wrap_ttl=wrap_ttl, data=data) for p in paths] - def _fake_write(path, wrap_ttl, **data): + def _fake_write(path, wrap_ttl, data=None): r = approle_secret_id_write_response.copy() r.update({'path': path}) return r - client.write = mock.Mock(wraps=_fake_write) + client.write_data = mock.Mock(wraps=_fake_write) response = vault_write_lookup.run(terms=paths, variables=minimal_vars, wrap_ttl=wrap_ttl, data=data) - client.write.assert_has_calls(expected_calls) + client.write_data.assert_has_calls(expected_calls) assert len(response) == len(paths), "%i paths processed but got %i responses" % (len(paths), len(response)) @@ -96,7 +96,7 @@ class TestVaultWriteLookup(object): requests_unparseable_response.status_code = 204 - client.write.return_value = requests_unparseable_response + client.write_data.return_value = requests_unparseable_response response = vault_write_lookup.run(terms=['fake'], variables=minimal_vars) @@ -108,7 +108,7 @@ class TestVaultWriteLookup(object): requests_unparseable_response.status_code = 200 requests_unparseable_response.content = '﷽' - client.write.return_value = requests_unparseable_response + client.write_data.return_value = requests_unparseable_response with mock.patch('ansible_collections.community.hashi_vault.plugins.lookup.vault_write.display.warning') as warning: response = vault_write_lookup.run(terms=['fake'], variables=minimal_vars) @@ -127,7 +127,40 @@ class TestVaultWriteLookup(object): def test_vault_write_exceptions(self, vault_write_lookup, minimal_vars, vault_client, exc): client = vault_client - client.write.side_effect = exc[0] + client.write_data.side_effect = exc[0] with pytest.raises(AnsibleError, match=exc[1]): vault_write_lookup.run(terms=['fake'], variables=minimal_vars) + + @pytest.mark.parametrize( + 'data', + [ + {"path": mock.sentinel.path_value}, + {"wrap_ttl": mock.sentinel.wrap_ttl_value}, + {"path": mock.sentinel.data_value, "wrap_ttl": mock.sentinel.write_ttl_value}, + ], + ) + def test_vault_write_data_fallback_bad_params(self, vault_write_lookup, minimal_vars, vault_client, data): + client = vault_client + client.mock_add_spec(['write']) + + with pytest.raises(AnsibleError, match=r"To use 'path' or 'wrap_ttl' as data keys, use hvac >= 1\.2"): + vault_write_lookup.run(terms=['fake'], variables=minimal_vars, data=data) + + client.write.assert_not_called() + + @pytest.mark.parametrize( + 'data', + [ + {"item1": mock.sentinel.item1_value}, + {"item2": mock.sentinel.item2_value}, + {"item1": mock.sentinel.item1_value, "item2": mock.sentinel.item2_value}, + ], + ) + def test_vault_write_data_fallback_write(self, vault_write_lookup, minimal_vars, vault_client, data): + client = vault_client + client.mock_add_spec(['write']) + + vault_write_lookup.run(terms=['fake'], variables=minimal_vars, data=data) + + client.write.assert_called_once_with(path='fake', wrap_ttl=None, **data) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_aws_iam.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_aws_iam.py index 678146b92..ac867dcd6 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_aws_iam.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_aws_iam.py @@ -163,7 +163,7 @@ class TestAuthAwsIam(object): params = auth_aws_iam._auth_aws_iam_login_params - assert boto3.session.Session.called_once_with(profile_name=profile) + boto3.session.Session.assert_called_once_with(profile_name=profile) assert params['access_key'] == aws_access_key assert params['secret_key'] == aws_secret_key diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index 747a432df..f0aabd78a 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -157,10 +157,10 @@ class TestAuthAzure(object): credential.get_token.return_value.token = jwt auth_azure.validate() - assert mocked_credential_class.called_once_with( + mocked_credential_class.assert_called_once_with( azure_tenant_id, azure_client_id, azure_client_secret ) - assert credential.get_token.called_once_with( + credential.get_token.assert_called_once_with( 'https://management.azure.com//.default' ) @@ -194,8 +194,8 @@ class TestAuthAzure(object): credential.get_token.return_value.token = jwt auth_azure.validate() - assert mocked_credential_class.called_once_with(azure_client_id) - assert credential.get_token.called_once_with( + mocked_credential_class.assert_called_once_with(client_id=azure_client_id) + credential.get_token.assert_called_once_with( 'https://management.azure.com//.default' ) @@ -215,8 +215,8 @@ class TestAuthAzure(object): credential.get_token.return_value.token = jwt auth_azure.validate() - assert mocked_credential_class.called_once_with() - assert credential.get_token.called_once_with( + mocked_credential_class.assert_called_once_with() + credential.get_token.assert_called_once_with( 'https://management.azure.com//.default' ) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_token.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_token.py index d8a13435b..5003082a5 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_token.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_auth_token.py @@ -11,11 +11,7 @@ import pytest from ......tests.unit.compat import mock -try: - import hvac -except ImportError: - # python 2.6, which isn't supported anyway - hvac = mock.MagicMock() +import hvac from ......plugins.module_utils._auth_method_token import ( HashiVaultAuthMethodToken, diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_hashi_vault_auth_method_base.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_hashi_vault_auth_method_base.py index 828cbdefc..36dfbcec0 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_hashi_vault_auth_method_base.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/authentication/test_hashi_vault_auth_method_base.py @@ -8,13 +8,10 @@ __metaclass__ = type import pytest -from ansible_collections.community.hashi_vault.tests.unit.compat import mock - -from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( +from ......plugins.module_utils._hashi_vault_common import ( HashiVaultAuthMethodBase, HashiVaultOptionGroupBase, HashiVaultValueError, - _stringify, ) @@ -75,12 +72,3 @@ class TestHashiVaultAuthMethodBase(object): auth_base.deprecate(msg, version, date, collection_name) deprecator.assert_called_once_with(msg, version=version, date=date, collection_name=collection_name) - - def test_has_stringify(self, auth_base): - v = 'X' - wrapper = mock.Mock(wraps=_stringify) - with mock.patch('ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common._stringify', wrapper): - r = auth_base._stringify(v) - - wrapper.assert_called_once_with(v) - assert r == v diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/option_adapter/test_hashi_vault_option_adapter.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/option_adapter/test_hashi_vault_option_adapter.py index d3e205d68..9226de003 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/option_adapter/test_hashi_vault_option_adapter.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/option_adapter/test_hashi_vault_option_adapter.py @@ -8,7 +8,7 @@ __metaclass__ = type import pytest -from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter +from ......plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter SAMPLE_DICT = { @@ -103,18 +103,32 @@ class TestHashiVaultOptionAdapter(object): def test_has_option(self, adapter, option, expected): assert adapter.has_option(option) == expected - @pytest.mark.parametrize('value', ['__VALUE']) - @pytest.mark.parametrize('option', (SAMPLE_KEYS + MISSING_KEYS)) - def test_set_option(self, adapter, option, value, sample_dict): + @pytest.mark.parametrize('option', SAMPLE_KEYS) + def test_set_option_existing(self, adapter, option, sample_dict): + value = type(sample_dict.get(option, ""))() adapter.set_option(option, value) - # first check the underlying data, then ensure the adapter refelcts the change too assert sample_dict[option] == value assert adapter.get_option(option) == value + @pytest.mark.parametrize('option', MISSING_KEYS) + def test_set_option_missing(self, request, adapter, option, sample_dict): + value = MARKER + + for mark in request.node.own_markers: + if mark.name == 'option_adapter_raise_on_missing': + from ansible.errors import AnsibleError + with pytest.raises(AnsibleError, match=rf"^Requested entry.*?setting: {option}.*?was not defined in configuration"): + adapter.set_option(option, value) + break + else: + adapter.set_option(option, value) + assert sample_dict[option] == value + assert adapter.get_option(option) == value + @pytest.mark.parametrize('default', [MARKER]) - @pytest.mark.parametrize('option,expected', [(o, SAMPLE_DICT[o]) for o in SAMPLE_KEYS] + [(o, MARKER) for o in MISSING_KEYS]) - def test_set_option_default(self, adapter, option, default, expected, sample_dict): + @pytest.mark.parametrize('option,expected', [(o, SAMPLE_DICT[o]) for o in SAMPLE_KEYS]) + def test_set_option_default_existing(self, adapter, option, default, expected, sample_dict): value = adapter.set_option_default(option, default) # check return data, underlying data structure, and adapter retrieval @@ -122,25 +136,47 @@ class TestHashiVaultOptionAdapter(object): assert sample_dict[option] == expected assert adapter.get_option(option) == expected - @pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]]) - def test_set_options(self, adapter, options, sample_dict): - update = dict([(o, '__VALUE_%i' % i) for i, o in enumerate(options)]) + @pytest.mark.parametrize('default', [MARKER]) + @pytest.mark.parametrize('option,expected', [(o, MARKER) for o in MISSING_KEYS]) + def test_set_option_default_missing(self, request, adapter, option, default, expected, sample_dict): + for mark in request.node.own_markers: + if mark.name == 'option_adapter_raise_on_missing': + from ansible.errors import AnsibleError + with pytest.raises(AnsibleError, match=rf"^Requested entry.*?setting: {option}.*?was not defined in configuration"): + adapter.set_option_default(option, default) + break + else: + value = adapter.set_option_default(option, default) + # check return data, underlying data structure, and adapter retrieval + assert value == expected + assert sample_dict[option] == expected + assert adapter.get_option(option) == expected - adapter.set_options(**update) + @pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]]) + def test_set_options(self, request, adapter, options, sample_dict): + update = dict([(o, type(sample_dict.get(o, ""))(i)) for i, o in enumerate(options)]) + + for mark in request.node.own_markers: + if mark.name == 'option_adapter_raise_on_missing': + from ansible.errors import AnsibleError + with pytest.raises(AnsibleError, match=r"^Requested entry.*?setting:.*?was not defined in configuration"): + adapter.set_options(**update) + break + else: + adapter.set_options(**update) + for k in MISSING_KEYS: + if k in update: + assert sample_dict[k] == update[k] + assert adapter.get_option(k) == update[k] + else: + assert k not in sample_dict + assert not adapter.has_option(k) for k in SAMPLE_KEYS: expected = update[k] if k in update else SAMPLE_DICT[k] assert sample_dict[k] == expected assert adapter.get_option(k) == expected - for k in MISSING_KEYS: - if k in update: - assert sample_dict[k] == update[k] - assert adapter.get_option(k) == update[k] - else: - assert k not in sample_dict - assert not adapter.has_option(k) - @pytest.mark.parametrize('options', [[SAMPLE_KEYS[0], MISSING_KEYS[0]]]) def test_get_options_mixed(self, adapter, options): with pytest.raises(KeyError): diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_connection_options.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_connection_options.py index 8766388e9..6a45b9859 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_connection_options.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_connection_options.py @@ -101,7 +101,7 @@ class TestHashiVaultConnectionOptions(object): with mock.patch.dict(os.environ, envpatch): connection_options._boolean_or_cacert() - assert predefined_options['ca_cert'] == expected + assert connection_options._conopt_verify == expected # _process_option_proxies # proxies can be specified as a dictionary where key is protocol/scheme @@ -248,7 +248,7 @@ class TestHashiVaultConnectionOptions(object): # these should always be returned assert 'url' in opts and opts['url'] == predefined_options['url'] - assert 'verify' in opts and opts['verify'] == predefined_options['ca_cert'] + assert 'verify' in opts and opts['verify'] == connection_options._conopt_verify # these are optional assert 'proxies' not in opts or opts['proxies'] == predefined_options['proxies'] diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_helper.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_helper.py index 6a5c6002b..14db5d1c8 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_helper.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/module_utils/test_hashi_vault_helper.py @@ -12,7 +12,6 @@ import pytest from .....tests.unit.compat import mock from .....plugins.module_utils._hashi_vault_common import ( HashiVaultHelper, - _stringify, ) @@ -48,12 +47,3 @@ class TestHashiVaultHelper(object): client = hashi_vault_helper.get_vault_client(hashi_vault_logout_inferred_token=True) assert client.token is None - - def test_has_stringify(self, hashi_vault_helper): - v = 'X' - wrapper = mock.Mock(wraps=_stringify) - with mock.patch('ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common._stringify', wrapper): - r = hashi_vault_helper._stringify(v) - - wrapper.assert_called_once_with(v) - assert r == v, '%r != %r' % (r, v) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_configure.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_configure.py new file mode 100644 index 000000000..17a4e063e --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_configure.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_connection_configure +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_connection(): + return { + "connection_name": "foo", + "plugin_name": "bar", + "allowed_roles": [ + "baz", + ], + "connection_url": "postgresql://{{'{{username}}'}}:{{'{{password}}'}}@postgres:5432/postgres?sslmode=disable", + "connection_username": "SomeUser", + "connection_password": "SomePass", + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_connection()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseConnectionConfigure: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_configure_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_configure.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_configure_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_configure.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_configure_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_connection_configure, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_connection_configure.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/config/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_connection_configure_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.configure.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_connection_configure.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_configure_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.configure.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_connection_configure.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.configure.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["connection_name"], + plugin_name=patch_ansible_module["plugin_name"], + allowed_roles=patch_ansible_module["allowed_roles"], + connection_url=patch_ansible_module["connection_url"], + username=patch_ansible_module["connection_username"], + password=patch_ansible_module["connection_password"], + ) + + assert result["changed"] is True diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_delete.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_delete.py new file mode 100644 index 000000000..5a24f85fa --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_delete.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_connection_delete +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_connection(): + return {"connection_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_connection()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseConnectionDelete: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_delete_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_delete_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_delete_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.delete_connection.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_connection_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.delete_connection.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["connection_name"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_delete_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_connection_delete, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_connection_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/config/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_connection_delete_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.delete_connection.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_connection_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_read.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_read.py new file mode 100644 index 000000000..062ff7689 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_read.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_connection_read +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_connection(): + return {"connection_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_connection()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_connection_read_response.json") + + +class TestModuleVaultDatabaseConnectionRead: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_read_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_read_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_read_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.read_connection.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_connection_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.read_connection.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["connection_name"], + ) + + raw = list_response.copy() + data = raw["data"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_read_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_connection_read, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_connection_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/config/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_connection_read_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.read_connection.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_connection_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_reset.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_reset.py new file mode 100644 index 000000000..bddb858fd --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connection_reset.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_connection_reset +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_connection(): + return {"connection_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_connection()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseConnectionReset: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_reset_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_reset.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connection_reset_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connection_reset.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_reset_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.reset_connection.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_connection_reset.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.reset_connection.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["connection_name"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connection_reset_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_connection_reset, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_connection_reset.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/reset/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_connection_reset_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.reset_connection.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_connection_reset.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connections_list.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connections_list.py new file mode 100644 index 000000000..057adc6f6 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_connections_list.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Martin Chmielewski (@M4rt1nCh) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_connections_list +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_connections_list_response.json") + + +class TestModuleVaultDatabaseConnectionsList: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connections_list_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_connections_list_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connections_list_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.list_connections.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_connections.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + raw = list_response.copy() + data = raw["data"] + roles = data["keys"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["connections"] == roles + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connections_list_no_data( + self, patch_ansible_module, vault_client, capfd + ): + client = vault_client + raw = {"errors": []} + client.secrets.database.list_connections.return_value = raw + + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_connections.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + empty_data = {"keys": []} + assert result["raw"] == raw + assert result["data"] == empty_data + assert result["connections"] == empty_data["keys"] + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_connections_list_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_connections_list, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/config'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_connections_list_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.list_connections.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_connections_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_create.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_create.py new file mode 100644 index 000000000..f8c752a00 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_create.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_role_create +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return { + "role_name": "foo", + "connection_name": "bar", + "creation_statements": [ + "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';", + 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";', + ], + "default_ttl": 3600, + "max_ttl": 86400, + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseRoleCreate: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_create_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_create_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_create_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.create_role.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.create_role.assert_called_once_with( + name=patch_ansible_module["role_name"], + db_name=patch_ansible_module["connection_name"], + creation_statements=patch_ansible_module["creation_statements"], + revocation_statements=patch_ansible_module.get("revocation_statements"), + rollback_statements=patch_ansible_module.get("rollback_statements"), + renew_statements=patch_ansible_module.get("renew_statements"), + default_ttl=patch_ansible_module["default_ttl"], + max_ttl=patch_ansible_module["max_ttl"], + mount_point=patch_ansible_module["engine_mount_point"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_create_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_role_create, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_role_create_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.create_role.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_delete.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_delete.py new file mode 100644 index 000000000..e0b254bd1 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_delete.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_role_delete +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return {"role_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseRoleDelete: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_delete_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_delete_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_delete_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.delete_role.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_role_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.delete_role.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_delete_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_role_delete, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_role_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_role_delete_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.delete_role.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_role_delete.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_read.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_read.py new file mode 100644 index 000000000..9dfcd4117 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_role_read.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_role_read +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return {"role_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_role_read_response.json") + + +class TestModuleVaultDatabaseRoleRead: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_read_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_role_read_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_read_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.read_role.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.read_role.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + ) + + raw = list_response.copy() + data = raw["data"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_role_read_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_role_read, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/roles/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_role_read_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.read_role.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_roles_list.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_roles_list.py new file mode 100644 index 000000000..bcfadb062 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_roles_list.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Martin Chmielewski (@M4rt1nCh) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_roles_list +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_roles_list_response.json") + + +class TestModuleVaultDatabaseRolesList: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_roles_list_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_roles_list_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_roles_list_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.list_roles.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_roles.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + raw = list_response.copy() + data = raw["data"] + roles = data["keys"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["roles"] == roles + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_roles_list_no_data( + self, patch_ansible_module, vault_client, capfd + ): + client = vault_client + raw = {"errors": []} + client.secrets.database.list_roles.return_value = raw + + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_roles.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + empty_data = {"keys": []} + assert result["raw"] == raw + assert result["data"] == empty_data + assert result["roles"] == empty_data["keys"] + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_roles_list_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_roles_list, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/roles'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_roles_list_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.list_roles.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_rotate_root_credentials.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_rotate_root_credentials.py new file mode 100644 index 000000000..1b22aaa0c --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_rotate_root_credentials.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_rotate_root_credentials +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_connection(): + return {"connection_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_connection()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseRotateRootCredentials: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_rotate_root_credentials_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_rotate_root_credentials_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_rotate_root_credentials_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.rotate_root_credentials.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.rotate_root_credentials.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["connection_name"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_rotate_root_credentials_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_rotate_root_credentials, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_rotate_root_credentials_old_hvac(self, vault_client, capfd): + client = vault_client + client.secrets.database.rotate_root_credentials.side_effect = AttributeError + + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "hvac>=2.0.0 is required" + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/rotate-root/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_rotate_root_credentials_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.rotate_root_credentials.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_rotate_root_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_create.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_create.py new file mode 100644 index 000000000..eab22477b --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_create.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_static_role_create +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return { + "role_name": "foo", + "connection_name": "bar", + "db_username": "baz", + "rotation_statements": [ + "ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';" + ], + "rotation_period": 86400, + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseStaticRoleCreate: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_create_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_create_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_create_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.create_static_role.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_static_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.create_static_role.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + db_name=patch_ansible_module["connection_name"], + username=patch_ansible_module["db_username"], + rotation_statements=patch_ansible_module["rotation_statements"], + rotation_period=patch_ansible_module["rotation_period"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_create_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_static_role_create, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_static_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/static-roles/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_static_role_create_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.create_static_role.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_static_role_create.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_get_credentials.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_get_credentials.py new file mode 100644 index 000000000..b15364ab6 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_get_credentials.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_static_role_get_credentials +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return {"role_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_static_role_get_credentials_response.json") + + +class TestModuleVaultDatabaseStaticRoleGetCredentials: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_get_credentials_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_get_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_get_credentials_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_get_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_get_credentials_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.get_static_credentials.return_value = ( + list_response.copy() + ) + + with pytest.raises(SystemExit) as e: + vault_database_static_role_get_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.get_static_credentials.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + ) + + raw = list_response.copy() + data = raw["data"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_get_credentials_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_static_role_get_credentials, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_static_role_get_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/static-creds/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_static_role_get_credentials_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.get_static_credentials.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_static_role_get_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_read.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_read.py new file mode 100644 index 000000000..6a0b087b4 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_read.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_static_role_read +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return {"role_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_static_role_read_response.json") + + +class TestModuleVaultDatabaseStaticRoleRead: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_read_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_read_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_read_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.read_static_role.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_static_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.read_static_role.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + ) + + raw = list_response.copy() + data = raw["data"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_read_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_static_role_read, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_static_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/static-roles/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_static_role_read_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.read_static_role.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_static_role_read.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_rotate_credentials.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_rotate_credentials.py new file mode 100644 index 000000000..b6e266755 --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_role_rotate_credentials.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_static_role_rotate_credentials +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _sample_role(): + return {"role_name": "foo"} + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(_sample_role()) + opt.update(kwargs) + return opt + + +class TestModuleVaultDatabaseStaticRoleRotateCredentials: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_rotate_credentials_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_role_rotate_credentials_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_rotate_credentials_success( + self, patch_ansible_module, empty_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.rotate_static_role_credentials.return_value = empty_response + + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.rotate_static_role_credentials.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"], + name=patch_ansible_module["role_name"], + ) + + assert result["changed"] is True + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_rotate_credentials_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_static_role_rotate_credentials, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_role_rotate_credentials_old_hvac(self, vault_client, capfd): + client = vault_client + client.secrets.database.rotate_static_role_credentials.side_effect = AttributeError + + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "hvac>=2.0.0 is required" + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/rotate-role/([^']+)'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_static_role_rotate_credentials_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.rotate_static_role_credentials.side_effect = exc[0]( + exc[1] + ) + + with pytest.raises(SystemExit) as e: + vault_database_static_role_rotate_credentials.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_roles_list.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_roles_list.py new file mode 100644 index 000000000..7a028e76d --- /dev/null +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_database_static_roles_list.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Brian Scholer (@briantist) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +import re +import json + +from ansible.module_utils.basic import missing_required_lib + +from ...compat import mock +from .....plugins.modules import vault_database_static_roles_list +from .....plugins.module_utils._hashi_vault_common import HashiVaultValueError + + +hvac = pytest.importorskip("hvac") + + +pytestmark = pytest.mark.usefixtures( + "patch_ansible_module", + "patch_authenticator", + "patch_get_vault_client", +) + + +def _connection_options(): + return { + "auth_method": "token", + "url": "http://myvault", + "token": "beep-boop", + } + + +def _sample_options(): + return { + "engine_mount_point": "dbmount", + } + + +def _combined_options(**kwargs): + opt = _connection_options() + opt.update(_sample_options()) + opt.update(kwargs) + return opt + + +@pytest.fixture +def list_response(fixture_loader): + return fixture_loader("database_static_roles_list_response.json") + + +class TestModuleVaultDatabaseStaticRolesList: + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_roles_list_authentication_error( + self, authenticator, exc, capfd + ): + authenticator.authenticate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg", "result: %r" % result + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + @pytest.mark.parametrize( + "exc", + [HashiVaultValueError("throwaway msg"), NotImplementedError("throwaway msg")], + ) + def test_vault_database_static_roles_list_auth_validation_error( + self, authenticator, exc, capfd + ): + authenticator.validate.side_effect = exc + + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == "throwaway msg" + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_roles_list_return_data( + self, patch_ansible_module, list_response, vault_client, capfd + ): + client = vault_client + client.secrets.database.list_static_roles.return_value = list_response.copy() + + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_static_roles.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + raw = list_response.copy() + data = raw["data"] + roles = data["keys"] + + assert ( + result["raw"] == raw + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["data"] == data + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + assert ( + result["roles"] == roles + ), "module result did not match expected result:\nexpected: %r\ngot: %r" % ( + list_response, + result, + ) + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_roles_list_no_data( + self, patch_ansible_module, vault_client, capfd + ): + client = vault_client + raw = {"errors": []} + client.secrets.database.list_static_roles.return_value = raw + + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.secrets.database.list_static_roles.assert_called_once_with( + mount_point=patch_ansible_module["engine_mount_point"] + ) + + empty_data = {"keys": []} + assert result["raw"] == raw + assert result["data"] == empty_data + assert result["roles"] == empty_data["keys"] + + @pytest.mark.parametrize( + "patch_ansible_module", [_combined_options()], indirect=True + ) + def test_vault_database_static_roles_list_no_hvac(self, capfd): + with mock.patch.multiple( + vault_database_static_roles_list, + HAS_HVAC=False, + HVAC_IMPORT_ERROR=None, + create=True, + ): + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert result["msg"] == missing_required_lib("hvac") + + @pytest.mark.parametrize( + "exc", + [ + ( + hvac.exceptions.Forbidden, + "", + r"^Forbidden: Permission Denied to path \['([^']+)'\]", + ), + ( + hvac.exceptions.InvalidPath, + "", + r"^Invalid or missing path \['([^']+)/static-roles'\]", + ), + ], + ) + @pytest.mark.parametrize( + "patch_ansible_module", + [[_combined_options(), "engine_mount_point"]], + indirect=True, + ) + @pytest.mark.parametrize("opt_engine_mount_point", ["path/1", "second/path"]) + def test_vault_database_static_roles_list_vault_exception( + self, vault_client, exc, opt_engine_mount_point, capfd + ): + + client = vault_client + client.secrets.database.list_static_roles.side_effect = exc[0](exc[1]) + + with pytest.raises(SystemExit) as e: + vault_database_static_roles_list.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + match = re.search(exc[2], result["msg"]) + assert match is not None, "result: %r\ndid not match: %s" % (result, exc[2]) + + assert opt_engine_mount_point == match.group(1) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_pki_generate_certificate.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_pki_generate_certificate.py index 29bba2165..c5374b537 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_pki_generate_certificate.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_pki_generate_certificate.py @@ -71,7 +71,7 @@ def translated_options(sample_options): if k in toplevel: opt[toplevel[k]] = v else: - if type(v) is list: + if isinstance(v, list): val = ','.join(v) else: val = v diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_write.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_write.py index afe877e8d..59d2e38a1 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_write.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/modules/test_vault_write.py @@ -88,7 +88,7 @@ class TestModuleVaultWrite(): @pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data', 'wrap_ttl']], indirect=True) def test_vault_write_return_data(self, patch_ansible_module, approle_secret_id_write_response, vault_client, opt_wrap_ttl, opt_data, capfd): client = vault_client - client.write.return_value = approle_secret_id_write_response + client.write_data.return_value = approle_secret_id_write_response with pytest.raises(SystemExit) as e: vault_write.main() @@ -98,7 +98,7 @@ class TestModuleVaultWrite(): assert e.value.code == 0, "result: %r" % (result,) - client.write.assert_called_once_with(path=patch_ansible_module['path'], wrap_ttl=opt_wrap_ttl, **opt_data) + client.write_data.assert_called_once_with(path=patch_ansible_module['path'], wrap_ttl=opt_wrap_ttl, data=opt_data) assert result['data'] == approle_secret_id_write_response, ( "module result did not match expected result:\nmodule: %r\nexpected: %r" % (result['data'], approle_secret_id_write_response) @@ -110,7 +110,7 @@ class TestModuleVaultWrite(): requests_unparseable_response.status_code = 204 - client.write.return_value = requests_unparseable_response + client.write_data.return_value = requests_unparseable_response with pytest.raises(SystemExit) as e: vault_write.main() @@ -129,7 +129,7 @@ class TestModuleVaultWrite(): requests_unparseable_response.status_code = 200 requests_unparseable_response.content = '﷽' - client.write.return_value = requests_unparseable_response + client.write_data.return_value = requests_unparseable_response with pytest.raises(SystemExit) as e: vault_write.main() @@ -166,7 +166,7 @@ class TestModuleVaultWrite(): def test_vault_write_vault_exception(self, vault_client, exc, capfd): client = vault_client - client.write.side_effect = exc[0] + client.write_data.side_effect = exc[0] with pytest.raises(SystemExit) as e: vault_write.main() @@ -176,3 +176,51 @@ class TestModuleVaultWrite(): assert e.value.code != 0, "result: %r" % (result,) assert re.search(exc[1], result['msg']) is not None + + @pytest.mark.parametrize( + 'opt_data', + [ + {"path": 'path_value'}, + {"wrap_ttl": 'wrap_ttl_value'}, + {"path": 'data_value', "wrap_ttl": 'write_ttl_value'}, + ], + ) + @pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data']], indirect=True) + def test_vault_write_data_fallback_bad_params(self, vault_client, opt_data, capfd): + client = vault_client + client.mock_add_spec(['write']) + + with pytest.raises(SystemExit) as e: + vault_write.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code != 0, "result: %r" % (result,) + assert re.search(r"To use 'path' or 'wrap_ttl' as data keys, use hvac >= 1\.2", result['msg']) is not None + + client.write.assert_not_called() + + @pytest.mark.parametrize( + 'opt_data', + [ + {"item1": 'item1_value'}, + {"item2": 'item2_value'}, + {"item1": 'item1_value', "item2": 'item2_value'}, + ], + ) + @pytest.mark.parametrize('patch_ansible_module', [[_combined_options(), 'data']], indirect=True) + def test_vault_write_data_fallback_write(self, vault_client, opt_data, patch_ansible_module, approle_secret_id_write_response, capfd): + client = vault_client + client.mock_add_spec(['write']) + client.write.return_value = approle_secret_id_write_response + + with pytest.raises(SystemExit) as e: + vault_write.main() + + out, err = capfd.readouterr() + result = json.loads(out) + + assert e.value.code == 0, "result: %r" % (result,) + + client.write.assert_called_once_with(path=patch_ansible_module['path'], wrap_ttl=None, **opt_data) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/conftest.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/conftest.py deleted file mode 100644 index bf577a2d0..000000000 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022 Brian Scholer (@briantist) -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -# pylint: disable=wildcard-import,unused-wildcard-import -from ...module_utils.authentication.conftest import * diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/test_auth_token.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/test_auth_token.py deleted file mode 100644 index 3dbc4c7fc..000000000 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/authentication/test_auth_token.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022 Brian Scholer (@briantist) -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest - -from ansible.utils.unsafe_proxy import AnsibleUnsafe, AnsibleUnsafeBytes, AnsibleUnsafeText - -from ansible_collections.community.hashi_vault.tests.unit.compat import mock - -from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_token import ( - HashiVaultAuthMethodToken, -) - - -@pytest.fixture -def option_dict(): - return { - 'auth_method': 'fake', - 'token': None, - 'token_path': None, - 'token_file': '.vault-token', - 'token_validate': True, - } - - -@pytest.fixture(params=[AnsibleUnsafeBytes(b'ub_opaque'), AnsibleUnsafeText(u'ut_opaque'), b'b_opaque', u't_opaque']) -def stringy(request): - return request.param - - -@pytest.fixture -def auth_token(adapter, warner, deprecator): - return HashiVaultAuthMethodToken(adapter, warner, deprecator) - - -class TestAuthToken(object): - def test_auth_token_unsafes(self, auth_token, client, adapter, stringy): - adapter.set_option('token', stringy) - adapter.set_option('token_validate', False) - - wrapper = mock.Mock(wraps=auth_token._stringify) - - with mock.patch.object(auth_token, '_stringify', wrapper): - response = auth_token.authenticate(client, use_token=True, lookup_self=False) - - assert isinstance(response['auth']['client_token'], (bytes, type(u''))), repr(response['auth']['client_token']) - assert isinstance(client.token, (bytes, type(u''))), repr(client.token) - assert not isinstance(response['auth']['client_token'], AnsibleUnsafe), repr(response['auth']['client_token']) - assert not isinstance(client.token, AnsibleUnsafe), repr(client.token) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/base/test_hashi_vault_lookup_base.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/base/test_hashi_vault_lookup_base.py index 620583b41..aa71687b4 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/base/test_hashi_vault_lookup_base.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/base/test_hashi_vault_lookup_base.py @@ -8,10 +8,11 @@ __metaclass__ = type import pytest -from ansible.errors import AnsibleError +from re import escape as re_escape + +from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.plugins.lookup import LookupBase -from ....compat import mock from ......plugins.plugin_utils._hashi_vault_plugin import HashiVaultPlugin from ......plugins.plugin_utils._hashi_vault_lookup_base import HashiVaultLookupBase @@ -72,7 +73,6 @@ class TestHashiVaultLookupBase(object): with pytest.raises(TypeError): parsed = hashi_vault_lookup_module.parse_kev_term('key1=value1', first_unqualified='fake') - # TODO: v5.0.0 - should raise not warn: https://github.com/ansible-collections/community.hashi_vault/pull/350 @pytest.mark.parametrize('term', [ 'one secret=two a=1 b=2', 'a=1 secret=one b=2 secret=two', @@ -80,10 +80,10 @@ class TestHashiVaultLookupBase(object): ]) def test_parse_kev_term_duplicate_option(self, term, hashi_vault_lookup_module): dup_key = 'secret' - removed_in = '5.0.0' - expected_template = "Duplicate key '%s' in the term string '%s'.\nIn version %s of the collection, this will raise an exception." - expected_msg = expected_template % (dup_key, term, removed_in) + expected_template = "Duplicate key '%s' in the term string '%s'." + expected_msg = expected_template % (dup_key, term) + expected_re = re_escape(expected_msg) + expected_match = "^%s$" % (expected_re,) - with mock.patch('ansible_collections.community.hashi_vault.plugins.plugin_utils._hashi_vault_lookup_base.display') as display: + with pytest.raises(AnsibleOptionsError, match=expected_match): hashi_vault_lookup_module.parse_kev_term(term, plugin_name='fake', first_unqualified=dup_key) - display.deprecated.assert_called_once_with(expected_msg, removed_in) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/conftest.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/conftest.py index 5c1aa7ed6..dd292cf10 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/conftest.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/conftest.py @@ -15,18 +15,33 @@ __metaclass__ = type import pytest +from packaging import version +from ansible.release import __version__ as ansible_version from ansible.plugins import AnsiblePlugin -from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter +from ......plugins.module_utils._hashi_vault_common import HashiVaultOptionAdapter -class FakePlugin(AnsiblePlugin): - _load_name = 'community.hashi_vault.fake' +ver = version.parse(ansible_version) +cutoff = version.parse('2.17') +option_adapter_from_plugin_marks = pytest.mark.option_adapter_raise_on_missing if ver.release >= cutoff.release else [] +# https://github.com/ansible-collections/community.hashi_vault/issues/417 + + +def _generate_options(opts: dict) -> dict: + return {k: {"type": type(v).__name__} if v is not None else {} for k, v in opts.items()} @pytest.fixture def ansible_plugin(sample_dict): - plugin = FakePlugin() + optdef = _generate_options(opts=sample_dict) + + class LookupModule(AnsiblePlugin): + _load_name = 'community.hashi_vault.fake' + + import ansible.constants as C + C.config.initialize_plugin_configuration_definitions("lookup", LookupModule._load_name, optdef) + plugin = LookupModule() plugin._options = sample_dict return plugin @@ -39,7 +54,11 @@ def adapter_from_ansible_plugin(ansible_plugin): return _create_adapter_from_ansible_plugin -@pytest.fixture(params=['dict', 'dict_defaults', 'ansible_plugin']) +@pytest.fixture(params=[ + 'dict', + 'dict_defaults', + pytest.param('ansible_plugin', marks=option_adapter_from_plugin_marks), +]) def adapter(request, adapter_from_dict, adapter_from_dict_defaults, adapter_from_ansible_plugin): return { 'dict': adapter_from_dict, diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/test_hashi_vault_option_adapter.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/test_hashi_vault_option_adapter.py index e78feec17..2d6d16ae7 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/test_hashi_vault_option_adapter.py +++ b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/option_adapter/test_hashi_vault_option_adapter.py @@ -12,4 +12,4 @@ __metaclass__ = type # So we really do want to import * and so we disable lint failure on wildcard imports. # # pylint: disable=wildcard-import,unused-wildcard-import -from ansible_collections.community.hashi_vault.tests.unit.plugins.module_utils.option_adapter.test_hashi_vault_option_adapter import * +from ...module_utils.option_adapter.test_hashi_vault_option_adapter import * diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_common_stringify.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_common_stringify.py deleted file mode 100644 index 1fcd3fd5e..000000000 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_common_stringify.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022 Brian Scholer (@briantist) -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest - -from ansible.utils.unsafe_proxy import AnsibleUnsafe, AnsibleUnsafeBytes, AnsibleUnsafeText - -from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import _stringify - - -@pytest.fixture -def uvalue(): - return u'fake123' - - -@pytest.fixture -def bvalue(): - return b'fake456' - - -class TestHashiVaultCommonStringify(object): - @pytest.mark.parametrize('unsafe', [True, False]) - def test_stringify_bytes(self, unsafe, bvalue): - token = bvalue - if unsafe: - token = AnsibleUnsafeBytes(token) - - r = _stringify(token) - - assert isinstance(r, bytes) - assert not isinstance(r, AnsibleUnsafe) - - @pytest.mark.parametrize('unsafe', [True, False]) - def test_stringify_unicode(self, unsafe, uvalue): - token = uvalue - utype = type(token) - if unsafe: - token = AnsibleUnsafeText(token) - - r = _stringify(token) - - assert isinstance(r, utype) - assert not isinstance(r, AnsibleUnsafe) diff --git a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_helper.py b/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_helper.py deleted file mode 100644 index e71e625c0..000000000 --- a/ansible_collections/community/hashi_vault/tests/unit/plugins/plugin_utils/test_hashi_vault_helper.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022 Brian Scholer (@briantist) -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest - -from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes, AnsibleUnsafeText - -from ansible_collections.community.hashi_vault.tests.unit.compat import mock -from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import HashiVaultHelper - - -@pytest.fixture -def hashi_vault_helper(): - return HashiVaultHelper() - - -@pytest.fixture -def expected_stringify_candidates(): - return set([ - 'token', - 'namespace', - ]) - - -class TestHashiVaultHelper(object): - def test_expected_stringify_candidates(self, hashi_vault_helper, expected_stringify_candidates): - # If we add more candidates to the set without updating the tests, - # this will help us catch that. The purpose is not to simply update - # the set in the fixture, but to also add specific tests where appropriate. - assert hashi_vault_helper.STRINGIFY_CANDIDATES == expected_stringify_candidates, '%r' % ( - hashi_vault_helper.STRINGIFY_CANDIDATES ^ expected_stringify_candidates - ) - - @pytest.mark.parametrize('input', [b'one', u'two', AnsibleUnsafeBytes(b'three'), AnsibleUnsafeText(u'four')]) - @pytest.mark.parametrize('stringify', [True, False]) - def test_get_vault_client_stringify(self, hashi_vault_helper, expected_stringify_candidates, input, stringify): - kwargs = { - '__no_candidate': AnsibleUnsafeText(u'value'), - } - expected_calls = [] - for k in expected_stringify_candidates: - v = '%s_%s' % (k, input) - kwargs[k] = v - if stringify: - expected_calls.append(mock.call(v)) - - wrapper = mock.Mock(wraps=hashi_vault_helper._stringify) - with mock.patch('hvac.Client'): - with mock.patch.object(hashi_vault_helper, '_stringify', wrapper): - hashi_vault_helper.get_vault_client(hashi_vault_stringify_args=stringify, **kwargs) - - assert wrapper.call_count == len(expected_calls) - wrapper.assert_has_calls(expected_calls) diff --git a/ansible_collections/community/hashi_vault/tests/unit/requirements.txt b/ansible_collections/community/hashi_vault/tests/unit/requirements.txt index d8844e35b..af9366e75 100644 --- a/ansible_collections/community/hashi_vault/tests/unit/requirements.txt +++ b/ansible_collections/community/hashi_vault/tests/unit/requirements.txt @@ -1,15 +1,3 @@ -# the collection supports python 3.6 and higher, however the constraints for -# earlier python versions are still needed for Ansible < 2.12 which doesn't -# support tests/config.yml, so that unit tests (which will be skipped) won't -# choke on installing requirements. -hvac >= 0.10.6, != 0.10.12, != 0.10.13, < 1.0.0 ; python_version == '2.7' # bugs in 0.10.12 and 0.10.13 prevent it from working in Python 2 -hvac >= 0.10.6, < 1.0.0 ; python_version == '3.5' # py3.5 support will be dropped in 1.0.0 -hvac >= 0.10.6 ; python_version >= '3.6' - -# these should be satisfied naturally by the requests versions required by hvac anyway -urllib3 >= 1.15 ; python_version >= '3.6' # we need raise_on_status for retry support to raise the correct exceptions https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#115-2016-04-06 -urllib3 >= 1.15, <2.0.0 ; python_version < '3.6' # https://urllib3.readthedocs.io/en/latest/v2-roadmap.html#optimized-for-python-3-6 - -# azure-identity 1.7.0 depends on cryptography 2.5 which drops python 2.6 support -azure-identity < 1.7.0; python_version < '2.7' -azure-identity; python_version >= '2.7' +hvac +urllib3 +azure-identity |