From c472ae542a7024b58df417d4143c5cadceb6e59c Mon Sep 17 00:00:00 2001 From: Christian Siegel Date: Mon, 8 Jun 2020 21:48:02 +0200 Subject: [PATCH 1/2] test(deploy): add tests for deploy command --- Makefile | 2 +- gitopscli/commands/deploy.py | 2 +- tests/commands/test_deploy.py | 301 ++++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 tests/commands/test_deploy.py diff --git a/Makefile b/Makefile index 710d6927..2c67a908 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ lint: pylint gitopscli test: - python3 -m pytest -v -s + python3 -m pytest -vv -s image: docker build -t gitopscli:latest . diff --git a/gitopscli/commands/deploy.py b/gitopscli/commands/deploy.py index 018017fc..2efd4196 100644 --- a/gitopscli/commands/deploy.py +++ b/gitopscli/commands/deploy.py @@ -105,7 +105,7 @@ def __create_pr(git, branch, file, updated_values, auto_merge): ``` """ pull_request = git.create_pull_request(branch, "master", title, description) - logging.info("Pull request created: %s", {git.get_pull_request_url(pull_request)}) + logging.info("Pull request created: %s", git.get_pull_request_url(pull_request)) if auto_merge: git.merge_pull_request(pull_request) diff --git a/tests/commands/test_deploy.py b/tests/commands/test_deploy.py new file mode 100644 index 00000000..8cae04ba --- /dev/null +++ b/tests/commands/test_deploy.py @@ -0,0 +1,301 @@ +import unittest +from uuid import UUID +from unittest.mock import patch, MagicMock, Mock, call +import pytest +from gitopscli.gitops_exception import GitOpsException +from gitopscli.commands.deploy import deploy_command + + +class DeployCommandTest(unittest.TestCase): + def setUp(self): + def add_patch(target): + patcher = patch(target) + self.addCleanup(patcher.stop) + return patcher.start() + + # Monkey patch all external functions the command is using: + self.os_path_isfile_mock = add_patch("gitopscli.commands.deploy.os.path.isfile") + self.update_yaml_file_mock = add_patch("gitopscli.commands.deploy.update_yaml_file") + self.create_tmp_dir_mock = add_patch("gitopscli.commands.deploy.create_tmp_dir") + self.delete_tmp_dir_mock = add_patch("gitopscli.commands.deploy.delete_tmp_dir") + self.logging_mock = add_patch("gitopscli.commands.deploy.logging") + self.uuid_mock = add_patch("gitopscli.commands.deploy.uuid") + + self.git_util_mock = MagicMock() + create_git_mock = add_patch("gitopscli.commands.deploy.create_git") + create_git_mock.return_value = self.git_util_mock + + # Attach all mocks to a single mock manager + self.mock_manager = Mock() + self.mock_manager.attach_mock(self.create_tmp_dir_mock, "create_tmp_dir") + self.mock_manager.attach_mock(self.git_util_mock, "git_util") + self.mock_manager.attach_mock(self.delete_tmp_dir_mock, "delete_tmp_dir") + self.mock_manager.attach_mock(self.update_yaml_file_mock, "update_yaml_file") + self.mock_manager.attach_mock(self.os_path_isfile_mock, "os.path.isfile") + self.mock_manager.attach_mock(self.logging_mock, "logging") + + # Define some common default return values + self.create_tmp_dir_mock.return_value = "/tmp/created-tmp-dir" + self.git_util_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}" + self.git_util_mock.create_pull_request.return_value = "" + self.git_util_mock.get_pull_request_url.side_effect = lambda x: f"" + self.os_path_isfile_mock.return_value = True + self.update_yaml_file_mock.return_value = True + self.uuid_mock.uuid4.return_value = UUID("b973b5bb-64a6-4735-a840-3113d531b41c") + + def test_happy_flow(self): + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=False, + auto_merge=False, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), + call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), + call.git_util.commit("changed 'a.b.c' to 'foo' in test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), + call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), + call.git_util.commit("changed 'a.b.d' to 'bar' in test/file.yml"), + call.git_util.push("master"), + call.logging.info("Pushed branch %s", "master"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + ] + + def test_create_pr_happy_flow(self): + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=True, + auto_merge=False, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.new_branch("gitopscli-deploy-b973b5bb"), + call.logging.info("Created branch %s", "gitopscli-deploy-b973b5bb"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), + call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), + call.git_util.commit("changed 'a.b.c' to 'foo' in test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), + call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), + call.git_util.commit("changed 'a.b.d' to 'bar' in test/file.yml"), + call.git_util.push("gitopscli-deploy-b973b5bb"), + call.logging.info("Pushed branch %s", "gitopscli-deploy-b973b5bb"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + call.git_util.create_pull_request( + "gitopscli-deploy-b973b5bb", + "master", + "Updated values in test/file.yml", + "Updated 2 values in `test/file.yml`:\n```yaml\na.b.c: foo\na.b.d: bar\n```\n", + ), + call.git_util.get_pull_request_url(""), + call.logging.info("Pull request created: %s", ">"), + ] + + def test_create_pr_and_merge_happy_flow(self): + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=True, + auto_merge=True, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.new_branch("gitopscli-deploy-b973b5bb"), + call.logging.info("Created branch %s", "gitopscli-deploy-b973b5bb"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), + call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), + call.git_util.commit("changed 'a.b.c' to 'foo' in test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), + call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), + call.git_util.commit("changed 'a.b.d' to 'bar' in test/file.yml"), + call.git_util.push("gitopscli-deploy-b973b5bb"), + call.logging.info("Pushed branch %s", "gitopscli-deploy-b973b5bb"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + call.git_util.create_pull_request( + "gitopscli-deploy-b973b5bb", + "master", + "Updated values in test/file.yml", + "Updated 2 values in `test/file.yml`:\n```yaml\na.b.c: foo\na.b.d: bar\n```\n", + ), + call.git_util.get_pull_request_url(""), + call.logging.info("Pull request created: %s", ">"), + call.git_util.merge_pull_request(""), + call.logging.info("Pull request merged"), + call.git_util.delete_branch("gitopscli-deploy-b973b5bb"), + call.logging.info("Branch '%s' deleted", "gitopscli-deploy-b973b5bb"), + ] + + def test_single_commit_happy_flow(self): + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=False, + auto_merge=False, + single_commit=True, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), + call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), + call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"), + call.git_util.commit("updated 2 values in test/file.yml\n\na.b.c: foo\na.b.d: bar"), + call.git_util.push("master"), + call.logging.info("Pushed branch %s", "master"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + ] + + def test_checkout_error(self): + checkout_exception = GitOpsException("dummy checkout error") + self.git_util_mock.checkout.side_effect = checkout_exception + + with pytest.raises(GitOpsException) as ex: + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=False, + auto_merge=False, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + self.assertEqual(ex.value, checkout_exception) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + ] + + def test_file_not_found(self): + self.os_path_isfile_mock.return_value = False + + with pytest.raises(GitOpsException) as ex: + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=False, + auto_merge=False, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + self.assertEqual(str(ex.value), "No such file: test/file.yml") + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + ] + + def test_nothing_to_update(self): + self.update_yaml_file_mock.return_value = False + + deploy_command( + command="deploy", + file="test/file.yml", + values={"a.b.c": "foo", "a.b.d": "bar"}, + username="USERNAME", + password="PASSWORD", + git_user="GIT_USER", + git_email="GIT_EMAIL", + create_pr=False, + auto_merge=False, + single_commit=False, + organisation="ORGA", + repository_name="REPO", + git_provider="github", + git_provider_url=None, + ) + + assert self.mock_manager.mock_calls == [ + call.create_tmp_dir(), + call.git_util.checkout("master"), + call.logging.info("Master checkout successful"), + call.git_util.get_full_file_path("test/file.yml"), + call.os.path.isfile("/tmp/created-tmp-dir/test/file.yml"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"), + call.logging.info("Yaml property %s already up-to-date", "a.b.c"), + call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"), + call.logging.info("Yaml property %s already up-to-date", "a.b.d"), + call.logging.info("All values already up-to-date. I'm done here"), + call.delete_tmp_dir("/tmp/created-tmp-dir"), + ] From 708b06596450280539c06d9559255c18bf58c07e Mon Sep 17 00:00:00 2001 From: Christian Siegel Date: Tue, 9 Jun 2020 06:51:10 +0200 Subject: [PATCH 2/2] test(deploy): add create_git() to captured mock calls --- tests/commands/test_deploy.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/commands/test_deploy.py b/tests/commands/test_deploy.py index 8cae04ba..4998a131 100644 --- a/tests/commands/test_deploy.py +++ b/tests/commands/test_deploy.py @@ -20,14 +20,13 @@ def add_patch(target): self.delete_tmp_dir_mock = add_patch("gitopscli.commands.deploy.delete_tmp_dir") self.logging_mock = add_patch("gitopscli.commands.deploy.logging") self.uuid_mock = add_patch("gitopscli.commands.deploy.uuid") - + self.create_git_mock = add_patch("gitopscli.commands.deploy.create_git") self.git_util_mock = MagicMock() - create_git_mock = add_patch("gitopscli.commands.deploy.create_git") - create_git_mock.return_value = self.git_util_mock # Attach all mocks to a single mock manager self.mock_manager = Mock() self.mock_manager.attach_mock(self.create_tmp_dir_mock, "create_tmp_dir") + self.mock_manager.attach_mock(self.create_git_mock, "create_git") self.mock_manager.attach_mock(self.git_util_mock, "git_util") self.mock_manager.attach_mock(self.delete_tmp_dir_mock, "delete_tmp_dir") self.mock_manager.attach_mock(self.update_yaml_file_mock, "update_yaml_file") @@ -35,6 +34,7 @@ def add_patch(target): self.mock_manager.attach_mock(self.logging_mock, "logging") # Define some common default return values + self.create_git_mock.return_value = self.git_util_mock self.create_tmp_dir_mock.return_value = "/tmp/created-tmp-dir" self.git_util_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}" self.git_util_mock.create_pull_request.return_value = "" @@ -63,6 +63,9 @@ def test_happy_flow(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.get_full_file_path("test/file.yml"), @@ -98,6 +101,9 @@ def test_create_pr_happy_flow(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.new_branch("gitopscli-deploy-b973b5bb"), @@ -143,6 +149,9 @@ def test_create_pr_and_merge_happy_flow(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.new_branch("gitopscli-deploy-b973b5bb"), @@ -192,6 +201,9 @@ def test_single_commit_happy_flow(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.get_full_file_path("test/file.yml"), @@ -231,6 +243,9 @@ def test_checkout_error(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.delete_tmp_dir("/tmp/created-tmp-dir"), ] @@ -259,6 +274,9 @@ def test_file_not_found(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.get_full_file_path("test/file.yml"), @@ -288,6 +306,9 @@ def test_nothing_to_update(self): assert self.mock_manager.mock_calls == [ call.create_tmp_dir(), + call.create_git( + "USERNAME", "PASSWORD", "GIT_USER", "GIT_EMAIL", "ORGA", "REPO", "github", None, "/tmp/created-tmp-dir" + ), call.git_util.checkout("master"), call.logging.info("Master checkout successful"), call.git_util.get_full_file_path("test/file.yml"),