summaryrefslogtreecommitdiffstats
path: root/test/units/module_utils/basic/test_argument_spec.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/units/module_utils/basic/test_argument_spec.py')
-rw-r--r--test/units/module_utils/basic/test_argument_spec.py724
1 files changed, 724 insertions, 0 deletions
diff --git a/test/units/module_utils/basic/test_argument_spec.py b/test/units/module_utils/basic/test_argument_spec.py
new file mode 100644
index 0000000..211d65a
--- /dev/null
+++ b/test/units/module_utils/basic/test_argument_spec.py
@@ -0,0 +1,724 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# Copyright: Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+import os
+
+import pytest
+
+from units.compat.mock import MagicMock
+from ansible.module_utils import basic
+from ansible.module_utils.api import basic_auth_argument_spec, rate_limit_argument_spec, retry_argument_spec
+from ansible.module_utils.common import warnings
+from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages
+from ansible.module_utils.six import integer_types, string_types
+from ansible.module_utils.six.moves import builtins
+
+
+MOCK_VALIDATOR_FAIL = MagicMock(side_effect=TypeError("bad conversion"))
+# Data is argspec, argument, expected
+VALID_SPECS = (
+ # Simple type=int
+ ({'arg': {'type': 'int'}}, {'arg': 42}, 42),
+ # Simple type=int with a large value (will be of type long under Python 2)
+ ({'arg': {'type': 'int'}}, {'arg': 18765432109876543210}, 18765432109876543210),
+ # Simple type=list, elements=int
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42, 32]}, [42, 32]),
+ # Type=int with conversion from string
+ ({'arg': {'type': 'int'}}, {'arg': '42'}, 42),
+ # Type=list elements=int with conversion from string
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': ['42', '32']}, [42, 32]),
+ # Simple type=float
+ ({'arg': {'type': 'float'}}, {'arg': 42.0}, 42.0),
+ # Simple type=list, elements=float
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42.1, 32.2]}, [42.1, 32.2]),
+ # Type=float conversion from int
+ ({'arg': {'type': 'float'}}, {'arg': 42}, 42.0),
+ # type=list, elements=float conversion from int
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42, 32]}, [42.0, 32.0]),
+ # Type=float conversion from string
+ ({'arg': {'type': 'float'}}, {'arg': '42.0'}, 42.0),
+ # type=list, elements=float conversion from string
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42.1', '32.2']}, [42.1, 32.2]),
+ # Type=float conversion from string without decimal point
+ ({'arg': {'type': 'float'}}, {'arg': '42'}, 42.0),
+ # Type=list elements=float conversion from string without decimal point
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42', '32.2']}, [42.0, 32.2]),
+ # Simple type=bool
+ ({'arg': {'type': 'bool'}}, {'arg': True}, True),
+ # Simple type=list elements=bool
+ ({'arg': {'type': 'list', 'elements': 'bool'}}, {'arg': [True, 'true', 1, 'yes', False, 'false', 'no', 0]},
+ [True, True, True, True, False, False, False, False]),
+ # Type=int with conversion from string
+ ({'arg': {'type': 'bool'}}, {'arg': 'yes'}, True),
+ # Type=str converts to string
+ ({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
+ # Type=list elements=str simple converts to string
+ ({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': ['42', '32']}, ['42', '32']),
+ # Type is implicit, converts to string
+ ({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
+ # Type=list elements=str implicit converts to string
+ ({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': [42, 32]}, ['42', '32']),
+ # parameter is required
+ ({'arg': {'required': True}}, {'arg': 42}, '42'),
+)
+
+INVALID_SPECS = (
+ # Type is int; unable to convert this string
+ ({'arg': {'type': 'int'}}, {'arg': "wolf"}, "is of type {0} and we were unable to convert to int: {0} cannot be converted to an int".format(type('bad'))),
+ # Type is list elements is int; unable to convert this string
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [1, "bad"]}, "is of type {0} and we were unable to convert to int: {0} cannot be converted to "
+ "an int".format(type('int'))),
+ # Type is int; unable to convert float
+ ({'arg': {'type': 'int'}}, {'arg': 42.1}, "'float'> cannot be converted to an int"),
+ # Type is list, elements is int; unable to convert float
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42.1, 32, 2]}, "'float'> cannot be converted to an int"),
+ # type is a callable that fails to convert
+ ({'arg': {'type': MOCK_VALIDATOR_FAIL}}, {'arg': "bad"}, "bad conversion"),
+ # type is a list, elements is callable that fails to convert
+ ({'arg': {'type': 'list', 'elements': MOCK_VALIDATOR_FAIL}}, {'arg': [1, "bad"]}, "bad conversion"),
+ # unknown parameter
+ ({'arg': {'type': 'int'}}, {'other': 'bad', '_ansible_module_name': 'ansible_unittest'},
+ 'Unsupported parameters for (ansible_unittest) module: other. Supported parameters include: arg.'),
+ ({'arg': {'type': 'int', 'aliases': ['argument']}}, {'other': 'bad', '_ansible_module_name': 'ansible_unittest'},
+ 'Unsupported parameters for (ansible_unittest) module: other. Supported parameters include: arg (argument).'),
+ # parameter is required
+ ({'arg': {'required': True}}, {}, 'missing required arguments: arg'),
+)
+
+BASIC_AUTH_VALID_ARGS = [
+ {'api_username': 'user1', 'api_password': 'password1', 'api_url': 'http://example.com', 'validate_certs': False},
+ {'api_username': 'user1', 'api_password': 'password1', 'api_url': 'http://example.com', 'validate_certs': True},
+]
+
+RATE_LIMIT_VALID_ARGS = [
+ {'rate': 1, 'rate_limit': 1},
+ {'rate': '1', 'rate_limit': 1},
+ {'rate': 1, 'rate_limit': '1'},
+ {'rate': '1', 'rate_limit': '1'},
+]
+
+RETRY_VALID_ARGS = [
+ {'retries': 1, 'retry_pause': 1.5},
+ {'retries': '1', 'retry_pause': '1.5'},
+ {'retries': 1, 'retry_pause': '1.5'},
+ {'retries': '1', 'retry_pause': 1.5},
+]
+
+
+@pytest.fixture
+def complex_argspec():
+ arg_spec = dict(
+ foo=dict(required=True, aliases=['dup']),
+ bar=dict(),
+ bam=dict(),
+ bing=dict(),
+ bang=dict(),
+ bong=dict(),
+ baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
+ bar1=dict(type='bool'),
+ bar3=dict(type='list', elements='path'),
+ bar_str=dict(type='list', elements=str),
+ zardoz=dict(choices=['one', 'two']),
+ zardoz2=dict(type='list', choices=['one', 'two', 'three']),
+ zardoz3=dict(type='str', aliases=['zodraz'], deprecated_aliases=[dict(name='zodraz', version='9.99')]),
+ )
+ mut_ex = (('bar', 'bam'), ('bing', 'bang', 'bong'))
+ req_to = (('bam', 'baz'),)
+
+ kwargs = dict(
+ argument_spec=arg_spec,
+ mutually_exclusive=mut_ex,
+ required_together=req_to,
+ no_log=True,
+ add_file_common_args=True,
+ supports_check_mode=True,
+ )
+ return kwargs
+
+
+@pytest.fixture
+def options_argspec_list():
+ options_spec = dict(
+ foo=dict(required=True, aliases=['dup']),
+ bar=dict(),
+ bar1=dict(type='list', elements='str'),
+ bar2=dict(type='list', elements='int'),
+ bar3=dict(type='list', elements='float'),
+ bar4=dict(type='list', elements='path'),
+ bam=dict(),
+ baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
+ bam1=dict(),
+ bam2=dict(default='test'),
+ bam3=dict(type='bool'),
+ bam4=dict(type='str'),
+ )
+
+ arg_spec = dict(
+ foobar=dict(
+ type='list',
+ elements='dict',
+ options=options_spec,
+ mutually_exclusive=[
+ ['bam', 'bam1'],
+ ],
+ required_if=[
+ ['foo', 'hello', ['bam']],
+ ['foo', 'bam2', ['bam2']]
+ ],
+ required_one_of=[
+ ['bar', 'bam']
+ ],
+ required_together=[
+ ['bam1', 'baz']
+ ],
+ required_by={
+ 'bam4': ('bam1', 'bam3'),
+ },
+ )
+ )
+
+ kwargs = dict(
+ argument_spec=arg_spec,
+ no_log=True,
+ add_file_common_args=True,
+ supports_check_mode=True
+ )
+ return kwargs
+
+
+@pytest.fixture
+def options_argspec_dict(options_argspec_list):
+ # should test ok, for options in dict format.
+ kwargs = options_argspec_list
+ kwargs['argument_spec']['foobar']['type'] = 'dict'
+ kwargs['argument_spec']['foobar']['elements'] = None
+
+ return kwargs
+
+
+#
+# Tests for one aspect of arg_spec
+#
+
+@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in VALID_SPECS],
+ indirect=['stdin'])
+def test_validator_basic_types(argspec, expected, stdin):
+
+ am = basic.AnsibleModule(argspec)
+
+ if 'type' in argspec['arg']:
+ if argspec['arg']['type'] == 'int':
+ type_ = integer_types
+ else:
+ type_ = getattr(builtins, argspec['arg']['type'])
+ else:
+ type_ = str
+
+ assert isinstance(am.params['arg'], type_)
+ assert am.params['arg'] == expected
+
+
+@pytest.mark.parametrize('stdin', [{'arg': 42}, {'arg': 18765432109876543210}], indirect=['stdin'])
+def test_validator_function(mocker, stdin):
+ # Type is a callable
+ MOCK_VALIDATOR_SUCCESS = mocker.MagicMock(return_value=27)
+ argspec = {'arg': {'type': MOCK_VALIDATOR_SUCCESS}}
+ am = basic.AnsibleModule(argspec)
+
+ assert isinstance(am.params['arg'], integer_types)
+ assert am.params['arg'] == 27
+
+
+@pytest.mark.parametrize('stdin', BASIC_AUTH_VALID_ARGS, indirect=['stdin'])
+def test_validate_basic_auth_arg(mocker, stdin):
+ kwargs = dict(
+ argument_spec=basic_auth_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['api_username'], string_types)
+ assert isinstance(am.params['api_password'], string_types)
+ assert isinstance(am.params['api_url'], string_types)
+ assert isinstance(am.params['validate_certs'], bool)
+
+
+@pytest.mark.parametrize('stdin', RATE_LIMIT_VALID_ARGS, indirect=['stdin'])
+def test_validate_rate_limit_argument_spec(mocker, stdin):
+ kwargs = dict(
+ argument_spec=rate_limit_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['rate'], integer_types)
+ assert isinstance(am.params['rate_limit'], integer_types)
+
+
+@pytest.mark.parametrize('stdin', RETRY_VALID_ARGS, indirect=['stdin'])
+def test_validate_retry_argument_spec(mocker, stdin):
+ kwargs = dict(
+ argument_spec=retry_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['retries'], integer_types)
+ assert isinstance(am.params['retry_pause'], float)
+
+
+@pytest.mark.parametrize('stdin', [{'arg': '123'}, {'arg': 123}], indirect=['stdin'])
+def test_validator_string_type(mocker, stdin):
+ # Custom callable that is 'str'
+ argspec = {'arg': {'type': str}}
+ am = basic.AnsibleModule(argspec)
+
+ assert isinstance(am.params['arg'], string_types)
+ assert am.params['arg'] == '123'
+
+
+@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in INVALID_SPECS],
+ indirect=['stdin'])
+def test_validator_fail(stdin, capfd, argspec, expected):
+ with pytest.raises(SystemExit):
+ basic.AnsibleModule(argument_spec=argspec)
+
+ out, err = capfd.readouterr()
+ assert not err
+ assert expected in json.loads(out)['msg']
+ assert json.loads(out)['failed']
+
+
+class TestComplexArgSpecs:
+ """Test with a more complex arg_spec"""
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello'}, {'dup': 'hello'}], indirect=['stdin'])
+ def test_complex_required(self, stdin, complex_argspec):
+ """Test that the complex argspec works if we give it its required param as either the canonical or aliased name"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['foo'], str)
+ assert am.params['foo'] == 'hello'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello1', 'dup': 'hello2'}], indirect=['stdin'])
+ def test_complex_duplicate_warning(self, stdin, complex_argspec):
+ """Test that the complex argspec issues a warning if we specify an option both with its canonical name and its alias"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['foo'], str)
+ assert 'Both option foo and its alias dup are set.' in get_warning_messages()
+ assert am.params['foo'] == 'hello2'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'test'}], indirect=['stdin'])
+ def test_complex_type_fallback(self, mocker, stdin, complex_argspec):
+ """Test that the complex argspec works if we get a required parameter via fallback"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ am = basic.AnsibleModule(**complex_argspec)
+
+ assert isinstance(am.params['baz'], str)
+ assert am.params['baz'] == 'test data'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'bad', 'bam': 'bad2', 'bing': 'a', 'bang': 'b', 'bong': 'c'}], indirect=['stdin'])
+ def test_fail_mutually_exclusive(self, capfd, stdin, complex_argspec):
+ """Fail because of mutually exclusive parameters"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are mutually exclusive: bar|bam, bing|bang|bong"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'bad2'}], indirect=['stdin'])
+ def test_fail_required_together(self, capfd, stdin, complex_argspec):
+ """Fail because only one of a required_together pair of parameters was specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'hi'}], indirect=['stdin'])
+ def test_fail_required_together_and_default(self, capfd, stdin, complex_argspec):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ complex_argspec['argument_spec']['baz'] = {'default': 42}
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello'}], indirect=['stdin'])
+ def test_fail_required_together_and_fallback(self, capfd, mocker, stdin, complex_argspec):
+ """Fail because one of a required_together pair of parameters has a fallback and the other was not specified"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'four', 'five']}], indirect=['stdin'])
+ def test_fail_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
+ """Fail because one of the items is not in the choice"""
+ with pytest.raises(SystemExit):
+ basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "value of zardoz2 must be one or more of: one, two, three. Got no match for: four, five"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'three']}], indirect=['stdin'])
+ def test_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['zardoz2'], list)
+ assert am.params['zardoz2'] == ['one', 'three']
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar3': ['~/test', 'test/']}], indirect=['stdin'])
+ def test_list_with_elements_path(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['bar3'], list)
+ assert am.params['bar3'][0].startswith('/')
+ assert am.params['bar3'][1] == 'test/'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zodraz': 'one'}], indirect=['stdin'])
+ def test_deprecated_alias(self, capfd, mocker, stdin, complex_argspec, monkeypatch):
+ """Test a deprecated alias"""
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ am = basic.AnsibleModule(**complex_argspec)
+
+ assert "Alias 'zodraz' is deprecated." in get_deprecation_messages()[0]['msg']
+ assert get_deprecation_messages()[0]['version'] == '9.99'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar_str': [867, '5309']}], indirect=['stdin'])
+ def test_list_with_elements_callable_str(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['bar_str'], list)
+ assert isinstance(am.params['bar_str'][0], string_types)
+ assert isinstance(am.params['bar_str'][1], string_types)
+ assert am.params['bar_str'][0] == '867'
+ assert am.params['bar_str'][1] == '5309'
+
+
+class TestComplexOptions:
+ """Test arg spec options"""
+
+ # (Parameters, expected value of module.params['foobar'])
+ OPTIONS_PARAMS_LIST = (
+ ({'foobar': [{"foo": "hello", "bam": "good"}, {"foo": "test", "bar": "good"}]},
+ [{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None},
+ {'foo': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Alias for required param
+ ({'foobar': [{"dup": "test", "bar": "good"}]},
+ [{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Required_if utilizing default value of the requirement
+ ({'foobar': [{"foo": "bam2", "bar": "required_one_of"}]},
+ [{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check that a bool option is converted
+ ({"foobar": [{"foo": "required", "bam": "good", "bam3": "yes"}]},
+ [{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check required_by options
+ ({"foobar": [{"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}]},
+ [{'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check for elements in sub-options
+ ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3":['1.3', 1.3, 1]}]},
+ [{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None, 'baz': None, 'bam': 'required_one_of',
+ 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}]
+ ),
+ )
+
+ # (Parameters, expected value of module.params['foobar'])
+ OPTIONS_PARAMS_DICT = (
+ ({'foobar': {"foo": "hello", "bam": "good"}},
+ {'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Alias for required param
+ ({'foobar': {"dup": "test", "bar": "good"}},
+ {'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Required_if utilizing default value of the requirement
+ ({'foobar': {"foo": "bam2", "bar": "required_one_of"}},
+ {'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Check that a bool option is converted
+ ({"foobar": {"foo": "required", "bam": "good", "bam3": "yes"}},
+ {'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Check required_by options
+ ({"foobar": {"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}},
+ {'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None,
+ 'foo': 'required', 'bar1': None, 'bar3': None, 'bar2': None, 'bar4': None}
+ ),
+ # Check for elements in sub-options
+ ({"foobar": {"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"],
+ "bar2": ['1', 1], "bar3": ['1.3', 1.3, 1]}},
+ {'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None,
+ 'baz': None, 'bam': 'required_one_of',
+ 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}
+ ),
+ )
+
+ # (Parameters, failure message)
+ FAILING_PARAMS_LIST = (
+ # Missing required option
+ ({'foobar': [{}]}, 'missing required arguments: foo found in foobar'),
+ # Invalid option
+ ({'foobar': [{"foo": "hello", "bam": "good", "invalid": "bad"}]}, 'module: foobar.invalid. Supported parameters include'),
+ # Mutually exclusive options found
+ ({'foobar': [{"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}]},
+ 'parameters are mutually exclusive: bam|bam1 found in foobar'),
+ # required_if fails
+ ({'foobar': [{"foo": "hello", "bar": "bad"}]},
+ 'foo is hello but all of the following are missing: bam found in foobar'),
+ # Missing required_one_of option
+ ({'foobar': [{"foo": "test"}]},
+ 'one of the following is required: bar, bam found in foobar'),
+ # Missing required_together option
+ ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam1": "bad"}]},
+ 'parameters are required together: bam1, baz found in foobar'),
+ # Missing required_by options
+ ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam4": "required_by"}]},
+ "missing parameter(s) required by 'bam4': bam1, bam3"),
+ )
+
+ # (Parameters, failure message)
+ FAILING_PARAMS_DICT = (
+ # Missing required option
+ ({'foobar': {}}, 'missing required arguments: foo found in foobar'),
+ # Invalid option
+ ({'foobar': {"foo": "hello", "bam": "good", "invalid": "bad"}},
+ 'module: foobar.invalid. Supported parameters include'),
+ # Mutually exclusive options found
+ ({'foobar': {"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}},
+ 'parameters are mutually exclusive: bam|bam1 found in foobar'),
+ # required_if fails
+ ({'foobar': {"foo": "hello", "bar": "bad"}},
+ 'foo is hello but all of the following are missing: bam found in foobar'),
+ # Missing required_one_of option
+ ({'foobar': {"foo": "test"}},
+ 'one of the following is required: bar, bam found in foobar'),
+ # Missing required_together option
+ ({'foobar': {"foo": "test", "bar": "required_one_of", "bam1": "bad"}},
+ 'parameters are required together: bam1, baz found in foobar'),
+ # Missing required_by options
+ ({'foobar': {"foo": "test", "bar": "required_one_of", "bam4": "required_by"}},
+ "missing parameter(s) required by 'bam4': bam1, bam3"),
+ )
+
+ @pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_DICT, indirect=['stdin'])
+ def test_options_type_dict(self, stdin, options_argspec_dict, expected):
+ """Test that a basic creation with required and required_if works"""
+ # should test ok, tests basic foo requirement and required_if
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar'], dict)
+ assert am.params['foobar'] == expected
+
+ @pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_LIST, indirect=['stdin'])
+ def test_options_type_list(self, stdin, options_argspec_list, expected):
+ """Test that a basic creation with required and required_if works"""
+ # should test ok, tests basic foo requirement and required_if
+ am = basic.AnsibleModule(**options_argspec_list)
+
+ assert isinstance(am.params['foobar'], list)
+ assert am.params['foobar'] == expected
+
+ @pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_DICT, indirect=['stdin'])
+ def test_fail_validate_options_dict(self, capfd, stdin, options_argspec_dict, expected):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert expected in results['msg']
+
+ @pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_LIST, indirect=['stdin'])
+ def test_fail_validate_options_list(self, capfd, stdin, options_argspec_list, expected):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**options_argspec_list)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert expected in results['msg']
+
+ @pytest.mark.parametrize('stdin', [{'foobar': {'foo': 'required', 'bam1': 'test', 'bar': 'case'}}], indirect=['stdin'])
+ def test_fallback_in_option(self, mocker, stdin, options_argspec_dict):
+ """Test that the complex argspec works if we get a required parameter via fallback"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar']['baz'], str)
+ assert am.params['foobar']['baz'] == 'test data'
+
+ @pytest.mark.parametrize('stdin',
+ [{'foobar': {'foo': 'required', 'bam1': 'test', 'baz': 'data', 'bar': 'case', 'bar4': '~/test'}}],
+ indirect=['stdin'])
+ def test_elements_path_in_option(self, mocker, stdin, options_argspec_dict):
+ """Test that the complex argspec works with elements path type"""
+
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar']['bar4'][0], str)
+ assert am.params['foobar']['bar4'][0].startswith('/')
+
+ @pytest.mark.parametrize('stdin,spec,expected', [
+ ({},
+ {'one': {'type': 'dict', 'apply_defaults': True, 'options': {'two': {'default': True, 'type': 'bool'}}}},
+ {'two': True}),
+ ({},
+ {'one': {'type': 'dict', 'options': {'two': {'default': True, 'type': 'bool'}}}},
+ None),
+ ], indirect=['stdin'])
+ def test_subspec_not_required_defaults(self, stdin, spec, expected):
+ # Check that top level not required, processed subspec defaults
+ am = basic.AnsibleModule(spec)
+ assert am.params['one'] == expected
+
+
+class TestLoadFileCommonArguments:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_smoketest_load_file_common_args(self, am):
+ """With no file arguments, an empty dict is returned"""
+ am.selinux_mls_enabled = MagicMock()
+ am.selinux_mls_enabled.return_value = True
+ am.selinux_default_context = MagicMock()
+ am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
+
+ assert am.load_file_common_arguments(params={}) == {}
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_load_file_common_args(self, am, mocker):
+ am.selinux_mls_enabled = MagicMock()
+ am.selinux_mls_enabled.return_value = True
+ am.selinux_default_context = MagicMock()
+ am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
+
+ base_params = dict(
+ path='/path/to/file',
+ mode=0o600,
+ owner='root',
+ group='root',
+ seuser='_default',
+ serole='_default',
+ setype='_default',
+ selevel='_default',
+ )
+
+ extended_params = base_params.copy()
+ extended_params.update(dict(
+ follow=True,
+ foo='bar',
+ ))
+
+ final_params = base_params.copy()
+ final_params.update(dict(
+ path='/path/to/real_file',
+ secontext=['unconfined_u', 'object_r', 'default_t', 's0'],
+ attributes=None,
+ ))
+
+ # with the proper params specified, the returned dictionary should represent
+ # only those params which have something to do with the file arguments, excluding
+ # other params and updated as required with proper values which may have been
+ # massaged by the method
+ mocker.patch('os.path.islink', return_value=True)
+ mocker.patch('os.path.realpath', return_value='/path/to/real_file')
+
+ res = am.load_file_common_arguments(params=extended_params)
+
+ assert res == final_params
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_true(stdin, capfd):
+ """Explicitly mask an argument (no_log=True)."""
+ arg_spec = {
+ "arg_pass": {"no_log": True}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ # no_log=True is picked up by both am._log_invocation and list_no_log_values
+ # (called by am._handle_no_log_values). As a result, we can check for the
+ # value in am.no_log_values.
+ assert "testing" in am.no_log_values
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_false(stdin, capfd):
+ """Explicitly log and display an argument (no_log=False)."""
+ arg_spec = {
+ "arg_pass": {"no_log": False}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ assert "testing" not in am.no_log_values and not get_warning_messages()
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_none(stdin, capfd):
+ """Allow Ansible to make the decision by matching the argument name
+ against PASSWORD_MATCH."""
+ arg_spec = {
+ "arg_pass": {}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ # Omitting no_log is only picked up by _log_invocation, so the value never
+ # makes it into am.no_log_values. Instead we can check for the warning
+ # emitted by am._log_invocation.
+ assert len(get_warning_messages()) > 0
+
+
+@pytest.mark.parametrize("stdin", [{"pass": "testing"}], indirect=["stdin"])
+def test_no_log_alias(stdin, capfd):
+ """Given module parameters that use an alias for a parameter that matches
+ PASSWORD_MATCH and has no_log=True set, a warning should not be issued.
+ """
+ arg_spec = {
+ "other_pass": {"no_log": True, "aliases": ["pass"]},
+ }
+ am = basic.AnsibleModule(arg_spec)
+
+ assert len(get_warning_messages()) == 0