Skip to content

Commit 26e1a0a

Browse files
committed
feat: add unit tests on gitlab cmd + commons
- Add unit tests on commons tools - Add unit tests on commands (for Gitlab, missing for Git)
1 parent f5c2e60 commit 26e1a0a

20 files changed

+1119
-112
lines changed

gitflow_toolbox/common/gitlab.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ def project(self) -> Project:
3030
# Use provided CI project, if not available, use project from current job
3131
project_id = get_env("GITLAB_PROJECT_ID", "CI_PROJECT_ID")
3232
self.__project = self.projects.get(project_id)
33+
return self.__project
3334

34-
self.__project = self.projects.get(get_env("GITLAB_PROJECT_ID"))
35+
# This line isn't covered by unit test to simplify singleton handling.
36+
self.__project = self.projects.get(get_env("GITLAB_PROJECT_ID")) # pragma: no cover
3537
return self.__project
3638

3739
@property

gitflow_toolbox/diff.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def diff(
6565
click.echo(diff_output)
6666
return diff_output
6767
finally:
68-
# Clean
68+
# Clean created directory by git lib during clone_from
6969
if os.path.isdir(project_from_clone_dir):
70-
shutil.rmtree(project_from_clone_dir)
70+
# These lines aren't tested by unit test because git doesn't impact file-system (mocked)
71+
shutil.rmtree(project_from_clone_dir) # pragma: no cover

gitflow_toolbox/push.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
@click.argument("target_branch", type=str)
1616
@click.option(
1717
"--from-gitlab",
18-
"-f",
1918
type=click.Choice(["current", "remote"], case_sensitive=False),
2019
help="Flag which allow you to chose between the current gitlab or the remote gitlab for the FROM role."
2120
" Defaults to 'remote'.",
@@ -46,8 +45,8 @@ def push(
4645
to_gitlab (str, optional): destination gitlab [current/remote]. Defaults to 'remote'.
4746
force (bool, optional): whether to force push. Defaults to False.
4847
"""
49-
to_gitlab = to_gitlab.lower()
50-
from_gitlab = from_gitlab.lower()
48+
to_gitlab = (to_gitlab or "remote").lower()
49+
from_gitlab = (from_gitlab or "remote").lower()
5150

5251
gitlab_from = CurrentGitlab() if from_gitlab == "current" else RemoteGitlab()
5352
gitlab_to = CurrentGitlab() if to_gitlab == "current" else RemoteGitlab()
@@ -77,7 +76,8 @@ def push(
7776
click.echo(f"✨ Successfully pushed {from_gitlab} {source_branch} into {to_gitlab} {target_branch}")
7877

7978
finally:
80-
# Clean
79+
# Clean created directory by git lib during clone_from
8180
if os.path.isdir(project_from_clone_dir):
82-
click.echo(f"Removing cache {project_from_clone_dir}")
83-
shutil.rmtree(project_from_clone_dir)
81+
# These lines aren't tested by unit test because git doesn't impact file-system (mocked)
82+
click.echo(f"Removing cache {project_from_clone_dir}") # pragma: no cover
83+
shutil.rmtree(project_from_clone_dir) # pragma: no cover

gitflow_toolbox/push_missing_tags.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import shutil
3-
import sys
43
import uuid
54

65
import click
@@ -37,8 +36,8 @@ def push_missing_tags(
3736
from_gitlab (str, optional): source gitlab [current/remote]. Defaults to 'remote'.
3837
to_gitlab (str, optional): destination gitlab [current/remote]. Defaults to 'remote'.
3938
"""
40-
to_gitlab = to_gitlab.lower()
41-
from_gitlab = from_gitlab.lower()
39+
to_gitlab = (to_gitlab or "remote").lower()
40+
from_gitlab = (from_gitlab or "remote").lower()
4241

4342
gitlab_from = CurrentGitlab() if from_gitlab == "current" else RemoteGitlab()
4443
gitlab_to = CurrentGitlab() if to_gitlab == "current" else RemoteGitlab()
@@ -68,19 +67,15 @@ def push_missing_tags(
6867
# Push missing tags from target to source
6968
for item in source_tags - target_tags:
7069
click.echo(f"Pushing {from_gitlab} {item.name} into {to_gitlab} ...")
71-
changes = repo_from.remotes.target.push(item)
72-
for change in changes:
73-
click.echo(change.summary)
74-
if "rejected" in change.summary:
75-
click.echo(f"❌ Couldn't push {item.name} to {to_gitlab}")
76-
sys.exit(1)
77-
click.echo(f"✨ Successfully pushed {from_gitlab} tags into {to_gitlab}")
70+
repo_from.remotes.target.push(item)
71+
click.echo(f"✨ Successfully pushed {from_gitlab} {item.name} tags into {to_gitlab}")
7872

7973
finally:
80-
# Clean
74+
# Clean created directory by git lib during clone_from
75+
# These lines aren't tested by unit test because git doesn't impact file-system (mocked)
8176
if os.path.isdir(project_from_clone_dir):
82-
click.echo(f"Removing cache {project_from_clone_dir}")
83-
shutil.rmtree(project_from_clone_dir)
77+
click.echo(f"Removing cache {project_from_clone_dir}") # pragma: no cover
78+
shutil.rmtree(project_from_clone_dir) # pragma: no cover
8479
if os.path.isdir(project_to_clone_dir):
85-
click.echo(f"Removing cache {project_to_clone_dir}")
86-
shutil.rmtree(project_to_clone_dir)
80+
click.echo(f"Removing cache {project_to_clone_dir}") # pragma: no cover
81+
shutil.rmtree(project_to_clone_dir) # pragma: no cover

gitflow_toolbox/tests/factories.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import itertools
2+
from functools import reduce
3+
from typing import Union
4+
from unittest import mock
5+
6+
7+
class DictObject:
8+
def __init__(self, **kwargs):
9+
self.__dict__.update(kwargs)
10+
11+
12+
class MockBranch:
13+
def __init__(self, name: str = "BRANCH") -> None:
14+
self.name = name
15+
16+
17+
class MockMergeRequest:
18+
def __init__(self, iid: str = "MR_ID", web_url: str = "MR_URL", state: str = "opened") -> None:
19+
self.iid = iid
20+
self.web_url = web_url
21+
self.state = state
22+
23+
24+
class MockBranchManager:
25+
def __init__(self) -> None:
26+
self.list = mock.MagicMock()
27+
self.list.return_value = []
28+
self.create = mock.MagicMock()
29+
30+
def reset_mock(self):
31+
self.list.reset_mock()
32+
self.create.reset_mock()
33+
self.list.return_value = []
34+
35+
36+
class MockMergeRequestManager:
37+
def __init__(self) -> None:
38+
self.list = mock.MagicMock()
39+
self.list.return_value = []
40+
self.create = mock.MagicMock()
41+
self.create.return_value = MockMergeRequest()
42+
43+
def reset_mock(self):
44+
self.list.reset_mock()
45+
self.create.reset_mock()
46+
self.list.return_value = []
47+
self.create.return_value = MockMergeRequest()
48+
49+
50+
class MockProject:
51+
def __init__(self) -> None:
52+
self.branches = MockBranchManager()
53+
self.mergerequests = MockMergeRequestManager()
54+
self.attributes = {
55+
"http_url_to_repo": "https://sample.spikeelabs.fr",
56+
"ssh_url_to_repo": "ssh_url_to_repo",
57+
}
58+
59+
60+
class MockGitRemoteInstance:
61+
def __init__(self) -> None:
62+
self.fetch = mock.MagicMock()
63+
self.push = mock.MagicMock()
64+
65+
66+
class MockGitRemote:
67+
def __init__(self) -> None:
68+
self.fetch = mock.MagicMock()
69+
70+
def reset(self):
71+
self.fetch.reset_mock()
72+
73+
def create_remote(self, *args, **kwargs): # pylint: disable=W0613
74+
if not hasattr(self, args[0]):
75+
setattr(self, args[0], MockGitRemoteInstance())
76+
77+
78+
class MockGitGit:
79+
def __init__(self) -> None:
80+
self.diff = mock.MagicMock()
81+
self.diff.return_value = None
82+
83+
def reset(self):
84+
self.diff.reset_mock()
85+
self.diff.return_value = None
86+
87+
88+
class MockGitRepo:
89+
def __init__(self) -> None:
90+
self.create_remote = mock.MagicMock()
91+
self.remotes = MockGitRemote()
92+
self.git = MockGitGit()
93+
self.tags = mock.MagicMock()
94+
95+
self.create_remote.side_effect = self.__create_remote
96+
97+
def reset(self):
98+
self.create_remote.reset_mock()
99+
self.remotes.reset()
100+
self.git.reset()
101+
self.tags.reset_mock()
102+
103+
def __create_remote(self, *args, **kwargs):
104+
self.remotes.create_remote(*args, **kwargs)
105+
106+
107+
def append_title(*args: Union[str, list[str]]):
108+
"""This is just an utils to rewrite title of test when we use parameterized.expend
109+
110+
It help use to understand which case of expended tests fail.
111+
112+
Yields:
113+
Union[str, list[str]: Given argument rewrite properly
114+
"""
115+
if args and isinstance(args[0], itertools.product):
116+
args = args[0]
117+
118+
for arg in list(args):
119+
if isinstance(arg, tuple):
120+
arg = list(arg)
121+
if isinstance(arg, list):
122+
arg.insert(0, reduce(lambda x, y: f"{x.replace('-', '')}_{y.replace('-', '')}", arg))
123+
if isinstance(arg, str):
124+
arg = arg.replace("-", "")
125+
126+
yield arg
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from parameterized import parameterized
2+
3+
from gitflow_toolbox.tests.factories import MockBranch
4+
from gitflow_toolbox.tests.testcases import GitflowTestCase
5+
6+
7+
class CheckBranchExistsTests(GitflowTestCase):
8+
@parameterized.expand(["--remote", "--current", "-r", "-c"])
9+
def test_cli_check_branch_exist_success(self, flag: str):
10+
self.project_mock.branches.reset_mock()
11+
self.project_mock.branches.list.return_value = [MockBranch("target_branch")]
12+
13+
result = self.runner.invoke(
14+
self.main_cli, ["check-branch-exists", "target_branch", flag], catch_exceptions=False
15+
)
16+
self.assertEqual(result.exit_code, 0, result.output)
17+
self.assertEqual(result.output, "Checking if target_branch branch exists...\nTrue\n")
18+
19+
self.project_mock.branches.list.assert_called_once_with()
20+
21+
@parameterized.expand(["--remote", "--current", "-r", "-c"])
22+
def test_cli_check_branch_exist_failure(self, flag: str):
23+
self.project_mock.branches.reset_mock()
24+
25+
result = self.runner.invoke(
26+
self.main_cli, ["check-branch-exists", "target_branch", flag], catch_exceptions=False
27+
)
28+
self.assertEqual(result.exit_code, 1, result.output)
29+
self.assertEqual(result.output, "Checking if target_branch branch exists...\nFalse\n")
30+
31+
self.project_mock.branches.list.assert_called_once_with()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import itertools
2+
3+
from parameterized import parameterized
4+
5+
from gitflow_toolbox.tests.factories import MockMergeRequest, append_title
6+
from gitflow_toolbox.tests.testcases import GitflowTestCase
7+
8+
9+
class CheckMrExistsTests(GitflowTestCase):
10+
@parameterized.expand(
11+
append_title(
12+
itertools.product(
13+
["--remote", "--current", "-r", "-c"], ["-s opened", "-s closed", "-s locked", "-s merged"]
14+
)
15+
)
16+
)
17+
def test_cli_check_mr_exist_success(self, _, remote: str, state: str):
18+
self.project_mock.mergerequests.reset_mock()
19+
self.project_mock.mergerequests.list.return_value = [MockMergeRequest()]
20+
21+
result = self.runner.invoke(
22+
self.main_cli, ["check-mr-exists", "src", "dst", remote, *state.split(" ")], catch_exceptions=False
23+
)
24+
self.assertEqual(result.exit_code, 0, result.output)
25+
self.assertEqual(result.output, "Checking if an opened merge request from src to dst exists...\nTrue\n")
26+
27+
self.project_mock.mergerequests.list.assert_called_once_with(
28+
state=state.split(" ")[1], source_branch="src", target_branch="dst"
29+
)
30+
31+
@parameterized.expand(
32+
append_title(
33+
itertools.product(
34+
["--remote", "--current", "-r", "-c"], ["-s opened", "-s closed", "-s locked", "-s merged"]
35+
)
36+
)
37+
)
38+
def test_cli_check_mr_exist_failure(self, _, remote: str, state: str):
39+
self.project_mock.mergerequests.reset_mock()
40+
41+
result = self.runner.invoke(
42+
self.main_cli, ["check-mr-exists", "src", "dst", remote, *state.split(" ")], catch_exceptions=False
43+
)
44+
self.assertEqual(result.exit_code, 1, result.output)
45+
self.assertEqual(result.output, "Checking if an opened merge request from src to dst exists...\nFalse\n")
46+
47+
self.project_mock.mergerequests.list.assert_called_once_with(
48+
state=state.split(" ")[1], source_branch="src", target_branch="dst"
49+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from parameterized import parameterized
2+
3+
from gitflow_toolbox.common.get_env import get_env
4+
from gitflow_toolbox.common.gitlab import CurrentGitlab, RemoteGitlab
5+
from gitflow_toolbox.tests.testcases import GitflowTestCase
6+
7+
8+
class CommonTests(GitflowTestCase):
9+
def test_get_env_raise_not_set_1_arg(self):
10+
self.assertRaises(Exception, lambda: get_env("FIRST"))
11+
12+
def test_get_env_raise_not_set_3_args(self):
13+
self.assertRaises(Exception, lambda: get_env("FIRST", "SECOND", "THIRD"))
14+
15+
def test_current_gitlab_project_authenticated_url(self):
16+
self.assertEqual(CurrentGitlab().project_authenticated_url, "https://gitflow:CI_JOB_TOKEN@sample.spikeelabs.fr")
17+
18+
def test_remote_gitlab_project_authenticated_url(self):
19+
self.assertEqual(
20+
RemoteGitlab().project_authenticated_url, "https://gitflow:REMOTE_GITLAB_PRIVATE_TOKEN@sample.spikeelabs.fr"
21+
)

gitflow_toolbox/tests/test_diff.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import itertools
2+
from unittest import mock
3+
4+
from parameterized import parameterized
5+
6+
from gitflow_toolbox.tests.factories import MockGitRepo, append_title
7+
from gitflow_toolbox.tests.testcases import GitflowTestCase
8+
9+
10+
class DiffTests(GitflowTestCase):
11+
@parameterized.expand(
12+
append_title(itertools.product(["--from-gitlab", "-f", "--to-gitlab", "-t"], ["current", "remote"]))
13+
)
14+
def test_diff_successful(self, _, flag_key: str, flag_val: str):
15+
with mock.patch("git.Repo.clone_from") as mock_clone_repo:
16+
mock_repo = MockGitRepo()
17+
mock_clone_repo.return_value = mock_repo
18+
mock_repo.git.diff.return_value = "diff"
19+
20+
result = self.runner.invoke(
21+
self.main_cli, ["diff", "src", "dst", flag_key, flag_val], catch_exceptions=False
22+
)
23+
self.assertEqual(result.exit_code, 0, result.output)
24+
self.assertEqual(result.output, "diff\n")
25+
26+
mock_clone_repo.assert_called_once()
27+
mock_repo.create_remote.assert_called_once()
28+
mock_repo.remotes.target.fetch.assert_called_once_with("dst")
29+
mock_repo.git.diff.assert_called_once()
30+
31+
@parameterized.expand(
32+
append_title(itertools.product(["--from-gitlab", "-f", "--to-gitlab", "-t"], ["current", "remote"]))
33+
)
34+
def test_when_no_diff(self, _, flag_key: str, flag_val: str):
35+
with mock.patch("git.Repo.clone_from") as mock_clone_repo:
36+
mock_repo = MockGitRepo()
37+
mock_clone_repo.return_value = mock_repo
38+
mock_repo.git.diff.return_value = ""
39+
40+
result = self.runner.invoke(
41+
self.main_cli, ["diff", "src", "dst", flag_key, flag_val], catch_exceptions=False
42+
)
43+
self.assertEqual(result.exit_code, 0, result.output)
44+
self.assertEqual(result.output, "")
45+
46+
mock_clone_repo.assert_called_once()
47+
mock_repo.create_remote.assert_called_once()
48+
mock_repo.remotes.target.fetch.assert_called_once_with("dst")
49+
mock_repo.git.diff.assert_called_once()

0 commit comments

Comments
 (0)