From 9c4015977de8005901b645b1ea54bd7d3f153270 Mon Sep 17 00:00:00 2001 From: Wissem BEN CHAABANE Date: Sat, 4 Sep 2021 19:14:34 +0200 Subject: [PATCH] Add unit test for the Ansible module --- tests/unit/modules/test_module_helm.py | 252 +++++++++++++++++++++--- tests/unit/utils/ansible_module_mock.py | 49 +++++ 2 files changed, 273 insertions(+), 28 deletions(-) create mode 100644 tests/unit/utils/ansible_module_mock.py diff --git a/tests/unit/modules/test_module_helm.py b/tests/unit/modules/test_module_helm.py index 3cf3f1376c7..f84bdd4c5ad 100644 --- a/tests/unit/modules/test_module_helm.py +++ b/tests/unit/modules/test_module_helm.py @@ -1,40 +1,236 @@ -# -*- coding: utf-8 -*- -# Copyright: (c) 2020, 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 -from ansible_collections.kubernetes.core.plugins.modules.helm import ( - run_dep_update +import unittest + +from unittest.mock import MagicMock, patch, call + +from ansible.module_utils import basic +from ansible_collections.kubernetes.core.plugins.modules import helm +from ansible_collections.kubernetes.core.tests.unit.utils.ansible_module_mock import ( + AnsibleFailJson, + AnsibleExitJson, + exit_json, + fail_json, + get_bin_path, + set_module_args ) -class MockedModule: - def __init__(self): - self.params = { - "api_key": None, - "ca_cert": None, - "host": None, - "kube_context": None, - "kubeconfig": None, - "release_namespace": None, - "validate_certs": None, +class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + self.chart_info_without_dep = { + 'apiVersion': 'v2', + 'appVersion': 'default', + 'description': 'A chart used in molecule tests', + 'name': 'test-chart', + 'type': 'application', + 'version': '0.1.0' + } + + self.chart_info_with_dep = { + 'apiVersion': 'v2', + 'appVersion': 'default', + 'description': 'A chart used in molecule tests', + 'name': 'test-chart', + 'type': 'application', + 'version': '0.1.0', + 'dependencies': [ + { + 'name': 'test', + 'version': '0.1.0', + 'repository': 'file://../test-chart' + } + ] + } + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + helm.main() + + def test_dependency_update_option_not_defined(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': '/tmp/path' + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + helm.run_dep_update = MagicMock() + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + helm.run_dep_update.assert_not_called() + mock_run_command.assert_called_once_with('/usr/bin/helm upgrade -i --reset-values test /tmp/path', + environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['command'] == '/usr/bin/helm upgrade -i --reset-values test /tmp/path' + + def test_dependency_update_option_false(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': '/tmp/path', + 'dependency_update': False + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + helm.run_dep_update = MagicMock() + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + helm.run_dep_update.assert_not_called() + mock_run_command.assert_called_once_with('/usr/bin/helm upgrade -i --reset-values test /tmp/path', + environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['command'] == '/usr/bin/helm upgrade -i --reset-values test /tmp/path' + + def test_dependency_update_option_true(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': '/tmp/path', + 'dependency_update': True + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep) + + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' + with patch.object(basic.AnsibleModule, 'warn') as mock_warn: + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + mock_warn.assert_not_called() + mock_run_command.assert_has_calls([ + call('/usr/bin/helm dependency update /tmp/path'), # Run commmand for dependency update + call('/usr/bin/helm upgrade -i --reset-values test /tmp/path', environ_update={'HELM_NAMESPACE': 'test'})]) + assert result.exception.args[0]['command'] == '/usr/bin/helm upgrade -i --reset-values test /tmp/path' + + def test_dependency_update_option_true_without_dependencies_block(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': '/tmp/path', + 'dependency_update': True + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with patch.object(basic.AnsibleModule, 'warn') as mock_warn: + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + mock_warn.assert_called_once() + mock_run_command.assert_has_calls([ + call('/usr/bin/helm dependency update /tmp/path'), # Run commmand for dependency update + call('/usr/bin/helm upgrade -i --reset-values test /tmp/path', environ_update={'HELM_NAMESPACE': 'test'})]) + assert result.exception.args[0]['command'] == '/usr/bin/helm upgrade -i --reset-values test /tmp/path' + + +class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + self.chart_info_without_dep = { + 'apiVersion': 'v2', + 'appVersion': 'default', + 'description': 'A chart used in molecule tests', + 'name': 'test-chart', + 'type': 'application', + 'version': '0.1.0' } - self.r = {} + def test_dependency_update_option_not_defined(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': 'chart1', + 'chart_repo_url': 'http://repo.example/charts' + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1', + environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['command'] == '/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1' - def run_command(self, command, environ_update=None): - self.r = {"command": command, "environ_update": environ_update} - return 0, "", "" + def test_dependency_update_option_False(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': 'chart1', + 'chart_repo_url': 'http://repo.example/charts', + 'dependency_update': False + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1', + environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['command'] == '/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1' + def test_dependency_update_option_True_and_replace_option_disabled(self): + set_module_args({ + 'release_name': 'test', + 'release_namespace': 'test', + 'chart_ref': 'chart1', + 'chart_repo_url': 'http://repo.example/charts', + 'dependency_update': True + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleFailJson) as result: + helm.main() + # mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1', + # environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['msg'] == ("'--dependency-update' hasn't been supported yet with 'helm upgrade'. " + "Please use 'helm install' instead by adding 'replace' option") + assert result.exception.args[0]['failed'] -def test_dependency_update_naked(): - module = MockedModule() - module.params = {} - common_cmd = "helm" - chart_ref = "/tmp/path/chart" - run_dep_update(module, common_cmd, chart_ref) - assert module.r["command"] == "helm dependency update /tmp/path/chart" - assert module.r["environ_update"] == {} + def test_dependency_update_option_True_and_replace_option_enabled(self): + set_module_args({ + 'release_name': "test", + 'release_namespace': 'test', + 'chart_ref': "chart1", + 'chart_repo_url': 'http://repo.example/charts', + 'dependency_update': True, + 'replace': True + }) + helm.get_release_status = MagicMock(return_value=None) + helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + mock_run_command.return_value = 0, 'configuration updated', '' # successful execution + with self.assertRaises(AnsibleExitJson) as result: + helm.main() + mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test chart1', + environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]['command'] == '/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test chart1' diff --git a/tests/unit/utils/ansible_module_mock.py b/tests/unit/utils/ansible_module_mock.py new file mode 100644 index 00000000000..0c2fc1c1340 --- /dev/null +++ b/tests/unit/utils/ansible_module_mock.py @@ -0,0 +1,49 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json + +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_bytes + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +def get_bin_path(self, arg, required=False): + """Mock AnsibleModule.get_bin_path""" + if arg.endswith('helm'): + return '/usr/bin/helm' + else: + if required: + fail_json(msg='%r not found !' % arg) + +# def warn(self,msg): +# return msg