diff --git a/docs/usage/auth_methods/index.rst b/docs/usage/auth_methods/index.rst index 10d60c9fe..e553dd200 100644 --- a/docs/usage/auth_methods/index.rst +++ b/docs/usage/auth_methods/index.rst @@ -12,7 +12,7 @@ Auth Methods jwt-oidc kubernetes ldap - mfa + legacymfa okta token userpass diff --git a/docs/usage/auth_methods/mfa.rst b/docs/usage/auth_methods/legacymfa.rst similarity index 62% rename from docs/usage/auth_methods/mfa.rst rename to docs/usage/auth_methods/legacymfa.rst index ea241bb38..5c5b309fe 100644 --- a/docs/usage/auth_methods/mfa.rst +++ b/docs/usage/auth_methods/legacymfa.rst @@ -1,15 +1,15 @@ -MFA -=== +Legacy MFA +========== -Configure MFA Auth Method Settings ------------------------------------ +Configure Legacy MFA Auth Method Settings +----------------------------------------- -:py:meth:`hvac.api.auth_methods.Mfa.configure` +:py:meth:`hvac.api.auth_methods.LegacyMfa.configure` .. note:: - The legacy/unsupported MFA auth method covered by this class's configuration API route only supports integration with a subset of Vault auth methods. See the list of supported auth methods in this module's :py:attr:`"SUPPORTED_AUTH_METHODS" attribute` and/or the associated `Vault MFA documentation`_ for additional information. + The legacy/unsupported MFA auth method covered by this class's configuration API route only supports integration with a subset of Vault auth methods. See the list of supported auth methods in this module's :py:attr:`"SUPPORTED_AUTH_METHODS" attribute` and/or the associated `Vault LegacyMFA documentation`_ for additional information. -.. _Vault MFA documentation: https://www.vaultproject.io/docs/auth/mfa.html +.. _Vault LegacyMFA documentation: https://developer.hashicorp.com/vault/docs/v1.10.x/auth/mfa .. code:: python @@ -27,29 +27,29 @@ Configure MFA Auth Method Settings path=userpass_auth_path, ) - client.auth.mfa.configure( + client.auth.legacymfa.configure( mount_point=userpass_auth_path, ) -Reading the MFA Auth Method Configuration ------------------------------------------ +Reading the Legacy MFA Auth Method Configuration +------------------------------------------------ -:py:meth:`hvac.api.auth_methods.Mfa.read_configuration` +:py:meth:`hvac.api.auth_methods.LegacyMfa.read_configuration` .. code:: python import hvac client = hvac.Client() - mfa_configuration = client.auth.mfa.read_configuration() - print('The MFA auth method is configured with a MFA type of: {mfa_type}'.format( + mfa_configuration = client.auth.legacymfa.read_configuration() + print('The LegacyMFA auth method is configured with a MFA type of: {mfa_type}'.format( mfa_type=mfa_configuration['data']['type'] ) -Configure Duo MFA Type Access Credentials ------------------------------------------ +Configure Duo LegacyMFA Type Access Credentials +----------------------------------------------- -:py:meth:`hvac.api.auth_methods.Mfa.configure_duo_access` +:py:meth:`hvac.api.auth_methods.LegacyMfa.configure_duo_access` .. code:: python @@ -61,43 +61,43 @@ Configure Duo MFA Type Access Credentials secret_key_prompt = 'Please enter the Duo access secret key to configure: ' duo_access_secret_key = getpass(prompt=secret_key_prompt) - client.auth.mfa.configure_duo_access( + client.auth.legacymfa.configure_duo_access( mount_point=userpass_auth_path, host='api-1234abcd.duosecurity.com', integration_key='SOME_DUO_IKEY', secret_key=duo_access_secret_key, ) -Configure Duo MFA Type Behavior -------------------------------- +Configure Duo Legacy MFA Type Behavior +-------------------------------------- -:py:meth:`hvac.api.auth_methods.Mfa.configure_duo_behavior` +:py:meth:`hvac.api.auth_methods.LegacyMfa.configure_duo_behavior` .. code:: python import hvac client = hvac.Client() - client.auth.mfa.configure_duo_behavior( + client.auth.legacymfa.configure_duo_behavior( mount_point=userpass_auth_path, username_format='%s@hvac.network', ) -Read Duo MFA Type Behavior --------------------------- +Read Duo Legacy MFA Type Behavior +--------------------------------- -:py:meth:`hvac.api.auth_methods.Mfa.read_duo_behavior_configuration` +:py:meth:`hvac.api.auth_methods.LegacyMfa.read_duo_behavior_configuration` .. code:: python import hvac client = hvac.Client() - duo_behavior_config = client.auth.mfa.read_duo_behavior_configuration( + duo_behavior_config = client.auth.legacymfa.read_duo_behavior_configuration( mount_point=userpass_auth_path, ) - print('The Duo MFA behvaior is configured with a username_format of: {username_format}'.format( + print('The Duo LegacyMFA behavior is configured with a username_format of: {username_format}'.format( username_format=duo_behavior_config['data']['username_format'], ) @@ -119,7 +119,7 @@ Authentication / Login client = hvac.Client() # Here the mount_point parameter corresponds to the path provided when enabling the backend - client.auth.mfa.auth_userpass( + client.auth.legacymfa.auth_userpass( username=login_username, password=login_password, mount_point=userpass_auth_path, diff --git a/hvac/api/auth_methods/__init__.py b/hvac/api/auth_methods/__init__.py index c2034e868..9c06376b4 100644 --- a/hvac/api/auth_methods/__init__.py +++ b/hvac/api/auth_methods/__init__.py @@ -10,7 +10,7 @@ from hvac.api.auth_methods.kubernetes import Kubernetes from hvac.api.auth_methods.ldap import Ldap from hvac.api.auth_methods.userpass import Userpass -from hvac.api.auth_methods.mfa import Mfa +from hvac.api.auth_methods.legacy_mfa import LegacyMfa from hvac.api.auth_methods.oidc import OIDC from hvac.api.auth_methods.okta import Okta from hvac.api.auth_methods.radius import Radius @@ -30,7 +30,7 @@ "Kubernetes", "Ldap", "Userpass", - "Mfa", + "LegacyMfa", "OIDC", "Okta", "Radius", @@ -52,7 +52,7 @@ class AuthMethods(VaultApiCategory): Kubernetes, Ldap, Userpass, - Mfa, + LegacyMfa, OIDC, Okta, Radius, @@ -63,6 +63,7 @@ class AuthMethods(VaultApiCategory): unimplemented_classes = [ "AppId", "AliCloud", + "Mfa", ] def __call__(self, *args, **kwargs): diff --git a/hvac/api/auth_methods/legacy_mfa.py b/hvac/api/auth_methods/legacy_mfa.py index 47f32dbc0..ce4b4ed05 100644 --- a/hvac/api/auth_methods/legacy_mfa.py +++ b/hvac/api/auth_methods/legacy_mfa.py @@ -16,7 +16,7 @@ class LegacyMfa(VaultApiBase): This class's methods correspond to a legacy / unsupported set of Vault API routes. Please see the reference link for additional context. - Reference: https://www.vaultproject.io/docs/auth/mfa.html + Reference: https://developer.hashicorp.com/vault/docs/v1.10.x/auth/mfa """ def configure(self, mount_point, mfa_type="duo", force=False): diff --git a/hvac/api/auth_methods/mfa.py b/hvac/api/auth_methods/mfa.py deleted file mode 100644 index b2b0665d0..000000000 --- a/hvac/api/auth_methods/mfa.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python -"""Multi-factor authentication methods module.""" -from hvac.api.auth_methods.legacy_mfa import LegacyMfa -from hvac.api.vault_api_base import VaultApiBase -from hvac import exceptions, utils - -SUPPORTED_MFA_TYPES = [ - "duo", -] -SUPPORTED_AUTH_METHODS = ["ldap", "okta", "radius", "userpass"] - - -class Mfa(VaultApiBase): - """Multi-factor authentication Auth Method (API). - - .. warning:: - This class's methods correspond to a legacy / unsupported set of Vault API routes. Please see the reference link - for additional context. - - Reference: https://www.vaultproject.io/docs/auth/mfa.html - """ - - @utils.deprecated_method( - to_be_removed_in_version="2.0.0", - new_method=LegacyMfa.configure, - ) - def configure(self, mount_point, mfa_type="duo", force=False): - """Configure MFA for a supported method. - - This endpoint allows you to turn on multi-factor authentication with a given backend. - Currently only Duo is supported. - - Supported methods: - POST: /auth/{mount_point}/mfa_config. Produces: 204 (empty body) - - :param mount_point: The "path" the method/backend was mounted on. - :type mount_point: str | unicode - :param mfa_type: Enables MFA with given backend (available: duo) - :type mfa_type: str | unicode - :param force: If True, make the "mfa_config" request regardless of circumstance. If False (the default), verify - the provided mount_point is available and one of the types of methods supported by this feature. - :type force: bool - :return: The response of the configure MFA request. - :rtype: requests.Response - """ - if mfa_type != "duo" and not force: - # The situation described via this exception is not likely to change in the future. - # However we provided that flexibility here just in case. - error_msg = 'Unsupported mfa_type argument provided "{arg}", supported types: "{mfa_types}"' - raise exceptions.ParamValidationError( - error_msg.format( - mfa_types=",".join(SUPPORTED_MFA_TYPES), - arg=mfa_type, - ) - ) - params = { - "type": mfa_type, - } - - api_path = utils.format_url( - "/v1/auth/{mount_point}/mfa_config", mount_point=mount_point - ) - return self._adapter.post( - url=api_path, - json=params, - ) - - @utils.deprecated_method( - to_be_removed_in_version="2.0.0", - new_method=LegacyMfa.read_configuration, - ) - def read_configuration(self, mount_point): - """Read the MFA configuration. - - Supported methods: - GET: /auth/{mount_point}/mfa_config. Produces: 200 application/json - - - :param mount_point: The "path" the method/backend was mounted on. - :type mount_point: str | unicode - :return: The JSON response of the read_configuration request. - :rtype: dict - """ - api_path = utils.format_url( - "/v1/auth/{mount_point}/mfa_config", - mount_point=mount_point, - ) - return self._adapter.get(url=api_path) - - @utils.deprecated_method( - to_be_removed_in_version="2.0.0", - new_method=LegacyMfa.configure_duo_access, - ) - def configure_duo_access(self, mount_point, host, integration_key, secret_key): - """Configure the access keys and host for Duo API connections. - - To authenticate users with Duo, the backend needs to know what host to connect to and must authenticate with an - integration key and secret key. This endpoint is used to configure that information. - - Supported methods: - POST: /auth/{mount_point}/duo/access. Produces: 204 (empty body) - - :param mount_point: The "path" the method/backend was mounted on. - :type mount_point: str | unicode - :param host: Duo API host - :type host: str | unicode - :param integration_key: Duo integration key - :type integration_key: Duo secret key - :param secret_key: The "path" the method/backend was mounted on. - :type secret_key: str | unicode - :return: The response of the configure_duo_access request. - :rtype: requests.Response - """ - params = { - "host": host, - "ikey": integration_key, - "skey": secret_key, - } - api_path = utils.format_url( - "/v1/auth/{mount_point}/duo/access", - mount_point=mount_point, - ) - return self._adapter.post( - url=api_path, - json=params, - ) - - @utils.deprecated_method( - to_be_removed_in_version="2.0.0", - new_method=LegacyMfa.configure_duo_behavior, - ) - def configure_duo_behavior( - self, mount_point, push_info=None, user_agent=None, username_format="%s" - ): - """Configure Duo second factor behavior. - - This endpoint allows you to configure how the original auth method username maps to the Duo username by - providing a template format string. - - Supported methods: - POST: /auth/{mount_point}/duo/config. Produces: 204 (empty body) - - - :param mount_point: The "path" the method/backend was mounted on. - :type mount_point: str | unicode - :param push_info: A string of URL-encoded key/value pairs that provides additional context about the - authentication attempt in the Duo Mobile app - :type push_info: str | unicode - :param user_agent: User agent to connect to Duo (default "") - :type user_agent: str | unicode - :param username_format: Format string given auth method username as argument to create Duo username - (default '%s') - :type username_format: str | unicode - :return: The response of the configure_duo_behavior request. - :rtype: requests.Response - """ - params = { - "username_format": username_format, - } - if push_info is not None: - params["push_info"] = push_info - if user_agent is not None: - params["user_agent"] = user_agent - api_path = utils.format_url( - "/v1/auth/{mount_point}/duo/config", - mount_point=mount_point, - ) - return self._adapter.post( - url=api_path, - json=params, - ) - - @utils.deprecated_method( - to_be_removed_in_version="2.0.0", - new_method=LegacyMfa.read_duo_behavior_configuration, - ) - def read_duo_behavior_configuration(self, mount_point): - """Read the Duo second factor behavior configuration. - - Supported methods: - GET: /auth/{mount_point}/duo/config. Produces: 200 application/json - - - :param mount_point: The "path" the method/backend was mounted on. - :type mount_point: str | unicode - :return: The JSON response of the read_duo_behavior_configuration request. - :rtype: dict - """ - api_path = utils.format_url( - "/v1/auth/{mount_point}/duo/config", - mount_point=mount_point, - ) - return self._adapter.get(url=api_path) diff --git a/hvac/constants/client.py b/hvac/constants/client.py index 09c5766fe..19c1fb4fe 100644 --- a/hvac/constants/client.py +++ b/hvac/constants/client.py @@ -12,10 +12,6 @@ to_be_removed_in_version="0.9.0", client_property="auth", ), - "mfa": dict( - to_be_removed_in_version="0.9.0", - client_property="auth", - ), "kv": dict( to_be_removed_in_version="0.9.0", client_property="secrets", diff --git a/hvac/utils.py b/hvac/utils.py index 3f9a81750..60f2bec62 100644 --- a/hvac/utils.py +++ b/hvac/utils.py @@ -222,9 +222,11 @@ def getattr_with_deprecated_properties(obj, item, deprecated_properties): :type obj: object :param item: Name of the attribute being requested. :type item: str - :param deprecated_properties: List of deprecated properties. Each item in the list is a dict with at least a - "to_be_removed_in_version" and "client_property" key to be used in the displayed deprecation warning. - :type deprecated_properties: List[dict] + :param deprecated_properties: Dict of deprecated properties. Each key is the name of the old property. + Each value is a dict with at least a "to_be_removed_in_version" and "client_property" key to be + used in the displayed deprecation warning. An optional "new_property" key contains the name of + the new property within the "client_property", otherwise the original name is used. + :type deprecated_properties: Dict :return: The new property indicated where available. :rtype: object """ @@ -298,15 +300,14 @@ def new_func(*args, **kwargs): docstring_copy = ( new_method.__doc__ if new_method.__doc__ is not None else "N/A" ) - if new_method.__doc__ is not None: - new_func.__doc__ = """\ - {message} - Docstring content from this method's replacement copied below: - {docstring_copy} - """.format( - message=deprecation_message, - docstring_copy=dedent(docstring_copy), - ) + new_func.__doc__ = """\ + {message} + Docstring content from this method's replacement copied below: + {docstring_copy} + """.format( + message=deprecation_message, + docstring_copy=dedent(docstring_copy), + ) else: new_func.__doc__ = deprecation_message @@ -317,13 +318,12 @@ def new_func(*args, **kwargs): def validate_list_of_strings_param(param_name, param_argument): """Validate that an argument is a list of strings. + Returns nothing if valid, raises ParamValidationException if invalid. :param param_name: The name of the parameter being validated. Used in any resulting exception messages. :type param_name: str | unicode :param param_argument: The argument to validate. :type param_argument: list - :return: True if the argument is validated, False otherwise. - :rtype: bool """ if param_argument is None: param_argument = [] diff --git a/tests/integration_tests/api/auth_methods/test_legacy_mfa.py b/tests/integration_tests/api/auth_methods/test_legacy_mfa.py index d2c5a57da..756df7e76 100644 --- a/tests/integration_tests/api/auth_methods/test_legacy_mfa.py +++ b/tests/integration_tests/api/auth_methods/test_legacy_mfa.py @@ -90,7 +90,7 @@ def test_configure( ): if raises: with self.assertRaises(raises) as cm: - self.client.auth.mfa.configure( + self.client.auth.legacymfa.configure( mount_point=mount_point, mfa_type=mfa_type, force=force, @@ -101,7 +101,7 @@ def test_configure( ) else: expected_status_code = 204 - configure_response = self.client.auth.mfa.configure( + configure_response = self.client.auth.legacymfa.configure( mount_point=mount_point, mfa_type=mfa_type, force=force, @@ -110,7 +110,7 @@ def test_configure( first=expected_status_code, second=configure_response.status_code ) - read_config_response = self.client.auth.mfa.read_configuration( + read_config_response = self.client.auth.legacymfa.read_configuration( mount_point=mount_point, ) self.assertEqual( @@ -124,11 +124,11 @@ def test_configure( ) def test_read_configuration(self, test_label, mount_point, add_configuration=True): if add_configuration: - self.client.auth.mfa.configure( + self.client.auth.legacymfa.configure( mount_point=mount_point, ) - response = self.client.auth.mfa.read_configuration( + response = self.client.auth.legacymfa.read_configuration( mount_point=mount_point, ) self.assertIn( @@ -153,7 +153,7 @@ def test_configure_duo_access( ): if raises: with self.assertRaises(raises) as cm: - self.client.auth.mfa.configure_duo_access( + self.client.auth.legacymfa.configure_duo_access( mount_point=mount_point, host=host, integration_key=integration_key, @@ -165,7 +165,7 @@ def test_configure_duo_access( ) else: expected_status_code = 204 - configure_response = self.client.auth.mfa.configure_duo_access( + configure_response = self.client.auth.legacymfa.configure_duo_access( mount_point=mount_point, host=host, integration_key=integration_key, @@ -192,7 +192,7 @@ def test_configure_duo_behavior( ): if raises: with self.assertRaises(raises) as cm: - self.client.auth.mfa.configure_duo_behavior( + self.client.auth.legacymfa.configure_duo_behavior( mount_point=mount_point, push_info=push_info, user_agent=user_agent, @@ -204,7 +204,7 @@ def test_configure_duo_behavior( ) else: expected_status_code = 204 - configure_response = self.client.auth.mfa.configure_duo_behavior( + configure_response = self.client.auth.legacymfa.configure_duo_behavior( mount_point=mount_point, push_info=push_info, user_agent=user_agent, @@ -214,8 +214,10 @@ def test_configure_duo_behavior( first=expected_status_code, second=configure_response.status_code ) - read_config_response = self.client.auth.mfa.read_duo_behavior_configuration( - mount_point=mount_point, + read_config_response = ( + self.client.auth.legacymfa.read_duo_behavior_configuration( + mount_point=mount_point, + ) ) self.assertEqual( first=push_info, second=read_config_response["data"]["push_info"] @@ -230,11 +232,11 @@ def test_read_duo_behavior_configuration( self, test_label, mount_point, add_configuration=True ): if add_configuration: - self.client.auth.mfa.configure( + self.client.auth.legacymfa.configure( mount_point=mount_point, ) - response = self.client.auth.mfa.read_duo_behavior_configuration( + response = self.client.auth.legacymfa.read_duo_behavior_configuration( mount_point=mount_point, ) self.assertIn( @@ -258,11 +260,11 @@ def test_login_with_mfa( username = "somedude" password = "myverygoodpassword" - self.client.auth.mfa.configure( + self.client.auth.legacymfa.configure( mount_point=TEST_AUTH_PATH, ) if configure_access: - self.client.auth.mfa.configure_duo_access( + self.client.auth.legacymfa.configure_duo_access( mount_point=TEST_AUTH_PATH, host=f"localhost:{self.mock_server_port}", integration_key="an-integration-key", diff --git a/tests/unit_tests/api/auth_methods/test_mfa.py b/tests/unit_tests/api/auth_methods/test_legacy_mfa.py similarity index 94% rename from tests/unit_tests/api/auth_methods/test_mfa.py rename to tests/unit_tests/api/auth_methods/test_legacy_mfa.py index d6f9bbf01..9f3cd8afc 100644 --- a/tests/unit_tests/api/auth_methods/test_mfa.py +++ b/tests/unit_tests/api/auth_methods/test_legacy_mfa.py @@ -4,11 +4,11 @@ from parameterized import parameterized from hvac.adapters import JSONAdapter -from hvac.api.auth_methods import Mfa +from hvac.api.auth_methods import LegacyMfa from hvac.api.auth_methods.github import DEFAULT_MOUNT_POINT -class TestMfa(TestCase): +class TestLegacyMfa(TestCase): @parameterized.expand( [ ("default mount point", DEFAULT_MOUNT_POINT), @@ -26,7 +26,7 @@ def test_configure(self, test_label, mount_point, requests_mocker): url=mock_url, status_code=expected_status_code, ) - mfa = Mfa(adapter=JSONAdapter()) + mfa = LegacyMfa(adapter=JSONAdapter()) response = mfa.configure( mount_point=mount_point, ) @@ -64,7 +64,7 @@ def test_read_configuration(self, test_label, mount_point, requests_mocker): status_code=expected_status_code, json=mock_response, ) - mfa = Mfa(adapter=JSONAdapter()) + mfa = LegacyMfa(adapter=JSONAdapter()) response = mfa.read_configuration( mount_point=mount_point, ) @@ -90,7 +90,7 @@ def test_configure_duo_access(self, test_label, mount_point, requests_mocker): url=mock_url, status_code=expected_status_code, ) - mfa = Mfa(adapter=JSONAdapter()) + mfa = LegacyMfa(adapter=JSONAdapter()) response = mfa.configure_duo_access( mount_point=mount_point, host="someapisubdomain.python-hvac.org", @@ -119,7 +119,7 @@ def test_configure_duo_behavior(self, test_label, mount_point, requests_mocker): url=mock_url, status_code=expected_status_code, ) - mfa = Mfa(adapter=JSONAdapter()) + mfa = LegacyMfa(adapter=JSONAdapter()) response = mfa.configure_duo_behavior( mount_point=mount_point, push_info="howdy" ) @@ -159,7 +159,7 @@ def test_read_duo_behvaior_configuration( status_code=expected_status_code, json=mock_response, ) - mfa = Mfa(adapter=JSONAdapter()) + mfa = LegacyMfa(adapter=JSONAdapter()) response = mfa.read_duo_behavior_configuration( mount_point=mount_point, ) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index d145cb678..c4cdae006 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -1,7 +1,20 @@ import pytest import warnings -from hvac.utils import generate_parameter_deprecation_message, aliased_parameter +from unittest import mock + +from hvac import exceptions +from hvac.utils import ( + generate_method_deprecation_message, + generate_property_deprecation_message, + generate_parameter_deprecation_message, + aliased_parameter, + comma_delimited_to_list, + get_token_from_env, + validate_list_of_strings_param, + getattr_with_deprecated_properties, + deprecated_method, +) @pytest.fixture @@ -18,7 +31,282 @@ def _func(pos0, pos1=None, *, kw0, kw1="kwone", kw2=None): return _func +@pytest.fixture +def deprecatable_method(): + def _meth(): + """docstring""" + return mock.sentinel.dep_meth_retval + + return _meth + + +class ClassWithDeprecatedProperties: + class _Sub: + old1 = "new1" + new1 = "NG" + old2 = "old2" + new2 = "new2" + + sub = _Sub() + + DEPRECATED_PROPERTIES = { + "old1": dict( + to_be_removed_in_version="99.88.77", + client_property="sub", + ), + "old2": dict( + to_be_removed_in_version="99.99.99", + client_property="sub", + new_property="new2", + ), + } + + class TestUtils: + @pytest.mark.parametrize("removed_in_version", ["99.88.77", "99.99.99"]) + def test_deprecated_method_no_new(self, removed_in_version, deprecatable_method): + old_method = deprecatable_method + + with mock.patch( + "hvac.utils.generate_method_deprecation_message", + new=mock.Mock(return_value=mock.sentinel.dep_meth_msg), + ) as gen_msg: + wrapped = deprecated_method( + to_be_removed_in_version=removed_in_version, + )(old_method) + + gen_msg.assert_called_once_with( + to_be_removed_in_version=removed_in_version, + old_method_name=old_method.__name__, + method_name=None, + module_name=None, + ) + assert wrapped.__doc__ == mock.sentinel.dep_meth_msg + + with mock.patch("warnings.warn") as warn: + result = wrapped() + warn.assert_called_once_with( + message=mock.sentinel.dep_meth_msg, + category=DeprecationWarning, + stacklevel=2, + ) + assert result == mock.sentinel.dep_meth_retval + + @pytest.mark.parametrize("removed_in_version", ["99.88.77", "99.99.99"]) + @pytest.mark.parametrize("new_meth_doc", [None, "newdoc"]) + def test_deprecated_method_new( + self, removed_in_version, deprecatable_method, new_meth_doc + ): + old_method = deprecatable_method + dep_meth_msg = "depmsg" + + def new_method(): + pass + + new_method.__doc__ = new_meth_doc + + with mock.patch( + "hvac.utils.generate_method_deprecation_message", + new=mock.Mock(return_value=dep_meth_msg), + ) as gen_msg: + wrapped = deprecated_method( + to_be_removed_in_version=removed_in_version, + new_method=new_method, + )(old_method) + + gen_msg.assert_called_once_with( + to_be_removed_in_version=removed_in_version, + old_method_name=old_method.__name__, + method_name=new_method.__name__, + module_name=__name__, + ) + if new_meth_doc is not None: + assert new_meth_doc in wrapped.__doc__ + else: + assert "N/A" in wrapped.__doc__ + + assert dep_meth_msg in wrapped.__doc__ + assert ( + "Docstring content from this method's replacement copied below" + in wrapped.__doc__ + ) + + with mock.patch("warnings.warn") as warn: + result = wrapped() + warn.assert_called_once_with( + message=dep_meth_msg, + category=DeprecationWarning, + stacklevel=2, + ) + assert result == mock.sentinel.dep_meth_retval + + @pytest.mark.parametrize( + ["item", "expected_new_prop", "expected_value", "expected_version"], + [("old1", "old1", "new1", "99.88.77"), ("old2", "new2", "new2", "99.99.99")], + ) + def test_getattr_with_deprecated_properties( + self, item, expected_new_prop, expected_value, expected_version + ): + with mock.patch( + "hvac.utils.generate_property_deprecation_message", + new=mock.Mock(return_value=mock.sentinel.dep_prop_msg), + ) as gen_msg, mock.patch("warnings.warn") as warn: + result = getattr_with_deprecated_properties( + ClassWithDeprecatedProperties, + item, + ClassWithDeprecatedProperties.DEPRECATED_PROPERTIES, + ) + + gen_msg.assert_called_once_with( + to_be_removed_in_version=expected_version, + old_name=item, + new_name=expected_new_prop, + new_attribute="sub", + ) + warn.assert_called_once_with( + message=mock.sentinel.dep_prop_msg, + category=DeprecationWarning, + stacklevel=2, + ) + assert result == expected_value + + @pytest.mark.parametrize("item", ["old9", "old8"]) + def test_getattr_with_deprecated_properties_no_item(self, item): + with pytest.raises(AttributeError, match=rf"has no attribute '{item}'"): + getattr_with_deprecated_properties( + ClassWithDeprecatedProperties, + item, + ClassWithDeprecatedProperties.DEPRECATED_PROPERTIES, + ) + + @pytest.mark.parametrize("token", ["token", "token2 ", " ", "\n"]) + def test_get_token_from_env_env_var(self, token): + with mock.patch.dict("os.environ", {"VAULT_TOKEN": token}): + with mock.patch("builtins.open", mock.mock_open()) as mopen: + result = get_token_from_env() + + mopen.assert_not_called() + assert result == token + + @mock.patch.dict("os.environ", clear=True) + @mock.patch("os.path.expanduser", mock.Mock(return_value="/a/b/c/token")) + @pytest.mark.parametrize("token", ["token", "token2 ", "", " ", "\n"]) + @pytest.mark.parametrize("exists", [True, False]) + def test_get_token_from_env_token_sink(self, token, exists): + with mock.patch("os.path.exists", lambda x: exists): + with mock.patch("builtins.open", mock.mock_open(read_data=token)) as mopen: + result = get_token_from_env() + + if exists: + mopen.assert_called_once_with("/a/b/c/token") + if token.strip(): + assert result == token.strip() + else: + assert result is None + else: + mopen.assert_not_called() + assert result is None + + @pytest.mark.parametrize("param_name", ["PARAM1", "PARAM2"]) + @pytest.mark.parametrize( + "param_argument", [None, [], ["1", "2"], "1,2,3", "", ",,"] + ) + def test_validate_list_of_strings_param_pass(self, param_name, param_argument): + result = validate_list_of_strings_param(param_name, param_argument) + assert result is None + + @pytest.mark.parametrize("param_name", ["PARAM1", "PARAM2"]) + @pytest.mark.parametrize( + "param_argument", [[None], [1], ["1", 2], ("1,2,3",), [["a"]]] + ) + def test_validate_list_of_strings_param_fail(self, param_name, param_argument): + with pytest.raises(exceptions.ParamValidationError) as e: + validate_list_of_strings_param(param_name, param_argument) + + msg = str(e.value) + assert str(param_name) in msg + assert str(param_argument) in msg + assert str(type(param_argument)) in msg + + @pytest.mark.parametrize( + "list_param", + [[], ["one"], [1, "two"], [1, "2", None], ["1", None, ["!", "@"], {}]], + ) + def test_comma_delimited_to_list_from_list(self, list_param): + result = comma_delimited_to_list(list_param=list_param) + assert result == list_param + + @pytest.mark.parametrize( + "list_param", + [{}, {"a": 1}, None, 7, b"X,Y,Z"], + ) + def test_comma_delimited_to_list_from_other(self, list_param): + result = comma_delimited_to_list(list_param=list_param) + assert result == [] + + @pytest.mark.parametrize( + ("list_param", "expected"), + [ + ("", [""]), + ("a", ["a"]), + ("a,b,c", ["a", "b", "c"]), + ("a, b, c", ["a", " b", " c"]), + ("a,,c", ["a", "", "c"]), + ], + ) + def test_comma_delimited_to_list_from_str(self, list_param, expected): + result = comma_delimited_to_list(list_param=list_param) + assert result == expected + + @pytest.mark.parametrize("to_be_removed_in_version", ["99.0.0"]) + @pytest.mark.parametrize("old_name", ["old_one"]) + @pytest.mark.parametrize("new_name", ["new_one"]) + @pytest.mark.parametrize("new_attribute", ["new_attr"]) + @pytest.mark.parametrize("module_name", ["Client", "modulename"]) + def test_generate_property_deprecation_message( + self, + to_be_removed_in_version, + old_name, + new_name, + new_attribute, + module_name, + ): + result = generate_property_deprecation_message( + to_be_removed_in_version=to_be_removed_in_version, + old_name=old_name, + new_name=new_name, + new_attribute=new_attribute, + module_name=module_name, + ) + + assert to_be_removed_in_version in result + assert old_name in result + assert new_name in result + assert module_name in result + + @pytest.mark.parametrize("to_be_removed_in_version", ["99.0.0"]) + @pytest.mark.parametrize("old_method_name", ["old_one"]) + @pytest.mark.parametrize("method_name", [None, "new_one"]) + @pytest.mark.parametrize("module_name", [None, "modulename"]) + def test_generate_method_deprecation_message( + self, + to_be_removed_in_version, + old_method_name, + method_name, + module_name, + ): + result = generate_method_deprecation_message( + to_be_removed_in_version=to_be_removed_in_version, + old_method_name=old_method_name, + method_name=method_name, + module_name=module_name, + ) + + assert to_be_removed_in_version in result + assert old_method_name in result + if method_name is not None and module_name is not None: + assert method_name in result and module_name in result + @pytest.mark.parametrize("to_be_removed_in_version", ["99.0.0"]) @pytest.mark.parametrize("old_parameter_name", ["old_one"]) @pytest.mark.parametrize("new_parameter_name", [None, "new_one"])