Skip to content

Commit df072e1

Browse files
nejchJohnVillalovos
authored andcommitted
test(gitlab): increase unit test coverage
1 parent c51b538 commit df072e1

12 files changed

+304
-34
lines changed

gitlab/client.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ def __setstate__(self, state: Dict[str, Any]) -> None:
208208
self.__dict__.update(state)
209209
# We only support v4 API at this time
210210
if self._api_version not in ("4",):
211-
raise ModuleNotFoundError(name=f"gitlab.v{self._api_version}.objects")
211+
raise ModuleNotFoundError(
212+
name=f"gitlab.v{self._api_version}.objects"
213+
) # pragma: no cover, dead code currently
212214
# NOTE: We must delay import of gitlab.v4.objects until now or
213215
# otherwise it will cause circular import errors
214216
import gitlab.v4.objects

gitlab/config.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def _parse_config(self) -> None:
154154
# CA bundle.
155155
try:
156156
self.ssl_verify = _config.get("global", "ssl_verify")
157-
except Exception:
157+
except Exception: # pragma: no cover
158158
pass
159159
except Exception:
160160
pass
@@ -166,7 +166,7 @@ def _parse_config(self) -> None:
166166
# CA bundle.
167167
try:
168168
self.ssl_verify = _config.get(self.gitlab_id, "ssl_verify")
169-
except Exception:
169+
except Exception: # pragma: no cover
170170
pass
171171
except Exception:
172172
pass
@@ -197,7 +197,9 @@ def _parse_config(self) -> None:
197197

198198
try:
199199
self.http_username = _config.get(self.gitlab_id, "http_username")
200-
self.http_password = _config.get(self.gitlab_id, "http_password")
200+
self.http_password = _config.get(
201+
self.gitlab_id, "http_password"
202+
) # pragma: no cover
201203
except Exception:
202204
pass
203205

tests/functional/cli/test_cli.py

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ def test_version(script_runner):
2727
assert ret.stdout.strip() == __version__
2828

2929

30+
def test_config_error_with_help_prints_help(script_runner):
31+
ret = script_runner.run("gitlab", "-c", "invalid-file", "--help")
32+
assert ret.stdout.startswith("usage:")
33+
assert ret.returncode == 0
34+
35+
3036
@pytest.mark.script_launch_mode("inprocess")
3137
@responses.activate
3238
def test_defaults_to_gitlab_com(script_runner, resp_get_project, monkeypatch):

tests/unit/helpers.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from typing import Optional
55

66
import requests
7+
import responses
8+
9+
MATCH_EMPTY_QUERY_PARAMS = [responses.matchers.query_param_matcher({})]
710

811

912
# NOTE: The function `httmock_response` and the class `Headers` is taken from

tests/unit/mixins/test_mixin_methods.py

+55
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,17 @@ class M(ListMixin, FakeManager):
9797
pass
9898

9999
url = "http://localhost/api/v4/tests"
100+
headers = {
101+
"X-Page": "1",
102+
"X-Next-Page": "2",
103+
"X-Per-Page": "1",
104+
"X-Total-Pages": "2",
105+
"X-Total": "2",
106+
"Link": ("<http://localhost/api/v4/tests" ' rel="next"'),
107+
}
100108
responses.add(
101109
method=responses.GET,
110+
headers=headers,
102111
url=url,
103112
json=[{"id": 42, "foo": "bar"}, {"id": 43, "foo": "baz"}],
104113
status=200,
@@ -109,6 +118,14 @@ class M(ListMixin, FakeManager):
109118
mgr = M(gl)
110119
obj_list = mgr.list(iterator=True)
111120
assert isinstance(obj_list, base.RESTObjectList)
121+
assert obj_list.current_page == 1
122+
assert obj_list.prev_page is None
123+
assert obj_list.next_page == 2
124+
assert obj_list.per_page == 1
125+
assert obj_list.total == 2
126+
assert obj_list.total_pages == 2
127+
assert len(obj_list) == 2
128+
112129
for obj in obj_list:
113130
assert isinstance(obj, FakeObject)
114131
assert obj.id in (42, 43)
@@ -254,6 +271,25 @@ class M(UpdateMixin, FakeManager):
254271
assert responses.assert_call_count(url, 1) is True
255272

256273

274+
@responses.activate
275+
def test_update_mixin_uses_post(gl):
276+
class M(UpdateMixin, FakeManager):
277+
_update_uses_post = True
278+
279+
url = "http://localhost/api/v4/tests/1"
280+
responses.add(
281+
method=responses.POST,
282+
url=url,
283+
json={},
284+
status=200,
285+
match=[responses.matchers.query_param_matcher({})],
286+
)
287+
288+
mgr = M(gl)
289+
mgr.update(1, {})
290+
assert responses.assert_call_count(url, 1) is True
291+
292+
257293
@responses.activate
258294
def test_update_mixin_no_id(gl):
259295
class M(UpdateMixin, FakeManager):
@@ -323,6 +359,25 @@ class TestClass(SaveMixin, base.RESTObject):
323359
assert responses.assert_call_count(url, 1) is True
324360

325361

362+
@responses.activate
363+
def test_save_mixin_without_new_data(gl):
364+
class M(UpdateMixin, FakeManager):
365+
pass
366+
367+
class TestClass(SaveMixin, base.RESTObject):
368+
pass
369+
370+
url = "http://localhost/api/v4/tests/1"
371+
responses.add(method=responses.PUT, url=url)
372+
373+
mgr = M(gl)
374+
obj = TestClass(mgr, {"id": 1, "foo": "bar"})
375+
obj.save()
376+
377+
assert obj._attrs["foo"] == "bar"
378+
assert responses.assert_call_count(url, 0) is True
379+
380+
326381
@responses.activate
327382
def test_set_mixin(gl):
328383
class M(SetMixin, FakeManager):

tests/unit/test_base.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,15 @@ class MGR(base.RESTManager):
7878

7979
class TestRESTObject:
8080
def test_instantiate(self, fake_gitlab, fake_manager):
81-
obj = FakeObject(fake_manager, {"foo": "bar"})
81+
attrs = {"foo": "bar"}
82+
obj = FakeObject(fake_manager, attrs.copy())
8283

83-
assert {"foo": "bar"} == obj._attrs
84+
assert attrs == obj._attrs
8485
assert {} == obj._updated_attrs
8586
assert obj._create_managers() is None
8687
assert fake_manager == obj.manager
8788
assert fake_gitlab == obj.manager.gitlab
89+
assert str(obj) == f"{type(obj)} => {attrs}"
8890

8991
def test_instantiate_non_dict(self, fake_gitlab, fake_manager):
9092
with pytest.raises(gitlab.exceptions.GitlabParsingError):
@@ -201,6 +203,7 @@ def test_equality(self, fake_manager):
201203
obj1 = FakeObject(fake_manager, {"id": "foo"})
202204
obj2 = FakeObject(fake_manager, {"id": "foo", "other_attr": "bar"})
203205
assert obj1 == obj2
206+
assert len(set((obj1, obj2))) == 1
204207

205208
def test_equality_custom_id(self, fake_manager):
206209
class OtherFakeObject(FakeObject):
@@ -210,6 +213,11 @@ class OtherFakeObject(FakeObject):
210213
obj2 = OtherFakeObject(fake_manager, {"foo": "bar", "other_attr": "baz"})
211214
assert obj1 == obj2
212215

216+
def test_equality_no_id(self, fake_manager):
217+
obj1 = FakeObject(fake_manager, {"attr1": "foo"})
218+
obj2 = FakeObject(fake_manager, {"attr1": "bar"})
219+
assert not obj1 == obj2
220+
213221
def test_inequality(self, fake_manager):
214222
obj1 = FakeObject(fake_manager, {"id": "foo"})
215223
obj2 = FakeObject(fake_manager, {"id": "bar"})
@@ -219,6 +227,12 @@ def test_inequality_no_id(self, fake_manager):
219227
obj1 = FakeObject(fake_manager, {"attr1": "foo"})
220228
obj2 = FakeObject(fake_manager, {"attr1": "bar"})
221229
assert obj1 != obj2
230+
assert len(set((obj1, obj2))) == 2
231+
232+
def test_equality_with_other_objects(self, fake_manager):
233+
obj1 = FakeObject(fake_manager, {"id": "foo"})
234+
obj2 = None
235+
assert not obj1 == obj2
222236

223237
def test_dunder_str(self, fake_manager):
224238
fake_object = FakeObject(fake_manager, {"attr1": "foo"})
@@ -280,3 +294,11 @@ def test_pprint(self, capfd, fake_manager):
280294
" 'ham': 'eggseggseggseggseggseggseggseggseggseggseggseggseggseggseggs'}\n"
281295
)
282296
assert stderr == ""
297+
298+
def test_repr(self, fake_manager):
299+
attrs = {"attr1": "foo"}
300+
obj = FakeObject(fake_manager, attrs)
301+
assert repr(obj) == "<FakeObject id:None>"
302+
303+
FakeObject._id_attr = None
304+
assert repr(obj) == "<FakeObject>"

tests/unit/test_config.py

+62-4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@
5454
oauth_token = STUV
5555
"""
5656

57+
ssl_verify_str_config = """[global]
58+
default = one
59+
ssl_verify = /etc/ssl/certs/ca-certificates.crt
60+
61+
[one]
62+
url = http://one.url
63+
private_token = ABCDEF
64+
"""
65+
5766
custom_user_agent_config = f"""[global]
5867
default = one
5968
user_agent = {custom_user_agent}
@@ -69,7 +78,7 @@
6978
private_token = ABCDEF
7079
"""
7180

72-
missing_attr_config = """[global]
81+
invalid_data_config = """[global]
7382
[one]
7483
url = http://one.url
7584
@@ -83,6 +92,11 @@
8392
url = http://four.url
8493
private_token = ABCDEF
8594
per_page = 200
95+
96+
[invalid-api-version]
97+
url = http://invalid-api-version.url
98+
private_token = ABCDEF
99+
api_version = 1
86100
"""
87101

88102

@@ -173,7 +187,7 @@ def test_invalid_id(m_open, mock_clean_env, monkeypatch):
173187

174188
@mock.patch("builtins.open")
175189
def test_invalid_data(m_open, monkeypatch):
176-
fd = io.StringIO(missing_attr_config)
190+
fd = io.StringIO(invalid_data_config)
177191
fd.close = mock.Mock(return_value=None, side_effect=lambda: fd.seek(0))
178192
m_open.return_value = fd
179193

@@ -185,9 +199,14 @@ def test_invalid_data(m_open, monkeypatch):
185199
config.GitlabConfigParser(gitlab_id="two")
186200
with pytest.raises(config.GitlabDataError):
187201
config.GitlabConfigParser(gitlab_id="three")
188-
with pytest.raises(config.GitlabDataError) as emgr:
202+
203+
with pytest.raises(config.GitlabDataError) as e:
189204
config.GitlabConfigParser("four")
190-
assert "Unsupported per_page number: 200" == emgr.value.args[0]
205+
assert str(e.value) == "Unsupported per_page number: 200"
206+
207+
with pytest.raises(config.GitlabDataError) as e:
208+
config.GitlabConfigParser("invalid-api-version")
209+
assert str(e.value) == "Unsupported API version: 1"
191210

192211

193212
@mock.patch("builtins.open")
@@ -248,6 +267,18 @@ def test_valid_data(m_open, monkeypatch):
248267
assert cp.ssl_verify is True
249268

250269

270+
@mock.patch("builtins.open")
271+
def test_ssl_verify_as_str(m_open, monkeypatch):
272+
fd = io.StringIO(ssl_verify_str_config)
273+
fd.close = mock.Mock(return_value=None)
274+
m_open.return_value = fd
275+
276+
with monkeypatch.context() as m:
277+
m.setattr(Path, "resolve", _mock_existent_file)
278+
cp = config.GitlabConfigParser()
279+
assert cp.ssl_verify == "/etc/ssl/certs/ca-certificates.crt"
280+
281+
251282
@mock.patch("builtins.open")
252283
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
253284
def test_data_from_helper(m_open, monkeypatch, tmp_path):
@@ -286,6 +317,33 @@ def test_data_from_helper(m_open, monkeypatch, tmp_path):
286317
assert "secret" == cp.oauth_token
287318

288319

320+
@mock.patch("builtins.open")
321+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
322+
def test_from_helper_subprocess_error_raises_error(m_open, monkeypatch):
323+
# using /usr/bin/false here to force a non-zero return code
324+
fd = io.StringIO(
325+
dedent(
326+
"""\
327+
[global]
328+
default = helper
329+
330+
[helper]
331+
url = https://helper.url
332+
oauth_token = helper: /usr/bin/false
333+
"""
334+
)
335+
)
336+
337+
fd.close = mock.Mock(return_value=None)
338+
m_open.return_value = fd
339+
with monkeypatch.context() as m:
340+
m.setattr(Path, "resolve", _mock_existent_file)
341+
with pytest.raises(config.GitlabConfigHelperError) as e:
342+
config.GitlabConfigParser(gitlab_id="helper")
343+
344+
assert "Failed to read oauth_token value from helper" in str(e.value)
345+
346+
289347
@mock.patch("builtins.open")
290348
@pytest.mark.parametrize(
291349
"config_string,expected_agent",

tests/unit/test_exceptions.py

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
from gitlab import exceptions
44

55

6+
@pytest.mark.parametrize(
7+
"kwargs,expected",
8+
[
9+
({"error_message": "foo"}, "foo"),
10+
({"error_message": "foo", "response_code": "400"}, "400: foo"),
11+
],
12+
)
13+
def test_gitlab_error(kwargs, expected):
14+
error = exceptions.GitlabError(**kwargs)
15+
assert str(error) == expected
16+
17+
618
def test_error_raises_from_http_error():
719
"""Methods decorated with @on_http_error should raise from GitlabHttpError."""
820

0 commit comments

Comments
 (0)