summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/unit/__init__.py0
-rw-r--r--tests/unit/conftest.py73
-rw-r--r--tests/unit/data/todo.json75
-rw-r--r--tests/unit/mixins/test_meta_mixins.py58
-rw-r--r--tests/unit/mixins/test_mixin_methods.py300
-rw-r--r--tests/unit/mixins/test_object_mixins_attributes.py79
-rw-r--r--tests/unit/objects/__init__.py0
-rw-r--r--tests/unit/objects/conftest.py70
-rw-r--r--tests/unit/objects/test_appearance.py65
-rw-r--r--tests/unit/objects/test_applications.py44
-rw-r--r--tests/unit/objects/test_audit_events.py109
-rw-r--r--tests/unit/objects/test_badges.py210
-rw-r--r--tests/unit/objects/test_bridges.py109
-rw-r--r--tests/unit/objects/test_commits.py115
-rw-r--r--tests/unit/objects/test_deploy_tokens.py45
-rw-r--r--tests/unit/objects/test_deployments.py50
-rw-r--r--tests/unit/objects/test_environments.py30
-rw-r--r--tests/unit/objects/test_groups.py97
-rw-r--r--tests/unit/objects/test_hooks.py29
-rw-r--r--tests/unit/objects/test_issues.py69
-rw-r--r--tests/unit/objects/test_job_artifacts.py30
-rw-r--r--tests/unit/objects/test_jobs.py96
-rw-r--r--tests/unit/objects/test_members.py58
-rw-r--r--tests/unit/objects/test_mro.py122
-rw-r--r--tests/unit/objects/test_packages.py186
-rw-r--r--tests/unit/objects/test_personal_access_tokens.py46
-rw-r--r--tests/unit/objects/test_pipeline_schedules.py62
-rw-r--r--tests/unit/objects/test_pipelines.py94
-rw-r--r--tests/unit/objects/test_project_access_tokens.py113
-rw-r--r--tests/unit/objects/test_project_import_export.py112
-rw-r--r--tests/unit/objects/test_project_merge_request_approvals.py317
-rw-r--r--tests/unit/objects/test_project_statistics.py28
-rw-r--r--tests/unit/objects/test_projects.py262
-rw-r--r--tests/unit/objects/test_releases.py131
-rw-r--r--tests/unit/objects/test_remote_mirrors.py72
-rw-r--r--tests/unit/objects/test_repositories.py49
-rw-r--r--tests/unit/objects/test_resource_label_events.py105
-rw-r--r--tests/unit/objects/test_resource_milestone_events.py73
-rw-r--r--tests/unit/objects/test_resource_state_events.py104
-rw-r--r--tests/unit/objects/test_runners.py282
-rw-r--r--tests/unit/objects/test_services.py93
-rw-r--r--tests/unit/objects/test_snippets.py89
-rw-r--r--tests/unit/objects/test_submodules.py46
-rw-r--r--tests/unit/objects/test_todos.py62
-rw-r--r--tests/unit/objects/test_users.py217
-rw-r--r--tests/unit/objects/test_variables.py192
-rw-r--r--tests/unit/test_base.py174
-rw-r--r--tests/unit/test_cli.py157
-rw-r--r--tests/unit/test_config.py247
-rw-r--r--tests/unit/test_exceptions.py18
-rw-r--r--tests/unit/test_gitlab.py153
-rw-r--r--tests/unit/test_gitlab_auth.py85
-rw-r--r--tests/unit/test_gitlab_http_methods.py234
-rw-r--r--tests/unit/test_types.py74
-rw-r--r--tests/unit/test_utils.py42
55 files changed, 5852 insertions, 0 deletions
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/__init__.py
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
new file mode 100644
index 0000000..64df051
--- /dev/null
+++ b/tests/unit/conftest.py
@@ -0,0 +1,73 @@
+import pytest
+
+import gitlab
+
+
+@pytest.fixture
+def gl():
+ return gitlab.Gitlab(
+ "http://localhost",
+ private_token="private_token",
+ ssl_verify=True,
+ api_version=4,
+ )
+
+
+# Todo: parametrize, but check what tests it's really useful for
+@pytest.fixture
+def gl_trailing():
+ return gitlab.Gitlab(
+ "http://localhost/", private_token="private_token", api_version=4
+ )
+
+
+@pytest.fixture
+def default_config(tmpdir):
+ valid_config = """[global]
+ default = one
+ ssl_verify = true
+ timeout = 2
+
+ [one]
+ url = http://one.url
+ private_token = ABCDEF
+ """
+
+ config_path = tmpdir.join("python-gitlab.cfg")
+ config_path.write(valid_config)
+ return str(config_path)
+
+
+@pytest.fixture
+def tag_name():
+ return "v1.0.0"
+
+
+@pytest.fixture
+def group(gl):
+ return gl.groups.get(1, lazy=True)
+
+
+@pytest.fixture
+def project(gl):
+ return gl.projects.get(1, lazy=True)
+
+
+@pytest.fixture
+def project_issue(project):
+ return project.issues.get(1, lazy=True)
+
+
+@pytest.fixture
+def project_merge_request(project):
+ return project.mergerequests.get(1, lazy=True)
+
+
+@pytest.fixture
+def release(project, tag_name):
+ return project.releases.get(tag_name, lazy=True)
+
+
+@pytest.fixture
+def user(gl):
+ return gl.users.get(1, lazy=True)
diff --git a/tests/unit/data/todo.json b/tests/unit/data/todo.json
new file mode 100644
index 0000000..93b2151
--- /dev/null
+++ b/tests/unit/data/todo.json
@@ -0,0 +1,75 @@
+[
+ {
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_pipeline_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "pending",
+ "created_at": "2016-06-17T07:52:35.225Z"
+ }
+]
diff --git a/tests/unit/mixins/test_meta_mixins.py b/tests/unit/mixins/test_meta_mixins.py
new file mode 100644
index 0000000..4c8845b
--- /dev/null
+++ b/tests/unit/mixins/test_meta_mixins.py
@@ -0,0 +1,58 @@
+from gitlab.mixins import (
+ CreateMixin,
+ CRUDMixin,
+ DeleteMixin,
+ GetMixin,
+ ListMixin,
+ NoUpdateMixin,
+ RetrieveMixin,
+ UpdateMixin,
+)
+
+
+def test_retrieve_mixin():
+ class M(RetrieveMixin):
+ pass
+
+ obj = M()
+ assert hasattr(obj, "list")
+ assert hasattr(obj, "get")
+ assert not hasattr(obj, "create")
+ assert not hasattr(obj, "update")
+ assert not hasattr(obj, "delete")
+ assert isinstance(obj, ListMixin)
+ assert isinstance(obj, GetMixin)
+
+
+def test_crud_mixin():
+ class M(CRUDMixin):
+ pass
+
+ obj = M()
+ assert hasattr(obj, "get")
+ assert hasattr(obj, "list")
+ assert hasattr(obj, "create")
+ assert hasattr(obj, "update")
+ assert hasattr(obj, "delete")
+ assert isinstance(obj, ListMixin)
+ assert isinstance(obj, GetMixin)
+ assert isinstance(obj, CreateMixin)
+ assert isinstance(obj, UpdateMixin)
+ assert isinstance(obj, DeleteMixin)
+
+
+def test_no_update_mixin():
+ class M(NoUpdateMixin):
+ pass
+
+ obj = M()
+ assert hasattr(obj, "get")
+ assert hasattr(obj, "list")
+ assert hasattr(obj, "create")
+ assert not hasattr(obj, "update")
+ assert hasattr(obj, "delete")
+ assert isinstance(obj, ListMixin)
+ assert isinstance(obj, GetMixin)
+ assert isinstance(obj, CreateMixin)
+ assert not isinstance(obj, UpdateMixin)
+ assert isinstance(obj, DeleteMixin)
diff --git a/tests/unit/mixins/test_mixin_methods.py b/tests/unit/mixins/test_mixin_methods.py
new file mode 100644
index 0000000..626230e
--- /dev/null
+++ b/tests/unit/mixins/test_mixin_methods.py
@@ -0,0 +1,300 @@
+import pytest
+from httmock import HTTMock, response, urlmatch # noqa
+
+from gitlab import base
+from gitlab.mixins import (
+ CreateMixin,
+ DeleteMixin,
+ GetMixin,
+ GetWithoutIdMixin,
+ ListMixin,
+ RefreshMixin,
+ SaveMixin,
+ SetMixin,
+ UpdateMixin,
+)
+
+
+class FakeObject(base.RESTObject):
+ pass
+
+
+class FakeManager(base.RESTManager):
+ _path = "/tests"
+ _obj_cls = FakeObject
+
+
+def test_get_mixin(gl):
+ class M(GetMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = mgr.get(42)
+ assert isinstance(obj, FakeObject)
+ assert obj.foo == "bar"
+ assert obj.id == 42
+
+
+def test_refresh_mixin(gl):
+ class TestClass(RefreshMixin, FakeObject):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = FakeManager(gl)
+ obj = TestClass(mgr, {"id": 42})
+ res = obj.refresh()
+ assert res is None
+ assert obj.foo == "bar"
+ assert obj.id == 42
+
+
+def test_get_without_id_mixin(gl):
+ class M(GetWithoutIdMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = mgr.get()
+ assert isinstance(obj, FakeObject)
+ assert obj.foo == "bar"
+ assert not hasattr(obj, "id")
+
+
+def test_list_mixin(gl):
+ class M(ListMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ # test RESTObjectList
+ mgr = M(gl)
+ obj_list = mgr.list(as_list=False)
+ assert isinstance(obj_list, base.RESTObjectList)
+ for obj in obj_list:
+ assert isinstance(obj, FakeObject)
+ assert obj.id in (42, 43)
+
+ # test list()
+ obj_list = mgr.list(all=True)
+ assert isinstance(obj_list, list)
+ assert obj_list[0].id == 42
+ assert obj_list[1].id == 43
+ assert isinstance(obj_list[0], FakeObject)
+ assert len(obj_list) == 2
+
+
+def test_list_other_url(gl):
+ class M(ListMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/others", method="get")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '[{"id": 42, "foo": "bar"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj_list = mgr.list(path="/others", as_list=False)
+ assert isinstance(obj_list, base.RESTObjectList)
+ obj = obj_list.next()
+ assert obj.id == 42
+ assert obj.foo == "bar"
+ with pytest.raises(StopIteration):
+ obj_list.next()
+
+
+def test_create_mixin_missing_attrs(gl):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+
+ mgr = M(gl)
+ data = {"foo": "bar", "baz": "blah"}
+ mgr._check_missing_create_attrs(data)
+
+ data = {"baz": "blah"}
+ with pytest.raises(AttributeError) as error:
+ mgr._check_missing_create_attrs(data)
+ assert "foo" in str(error.value)
+
+
+def test_create_mixin(gl):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+ _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",))
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="post")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = mgr.create({"foo": "bar"})
+ assert isinstance(obj, FakeObject)
+ assert obj.id == 42
+ assert obj.foo == "bar"
+
+
+def test_create_mixin_custom_path(gl):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+ _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",))
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/others", method="post")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = mgr.create({"foo": "bar"}, path="/others")
+ assert isinstance(obj, FakeObject)
+ assert obj.id == 42
+ assert obj.foo == "bar"
+
+
+def test_update_mixin_missing_attrs(gl):
+ class M(UpdateMixin, FakeManager):
+ _update_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+
+ mgr = M(gl)
+ data = {"foo": "bar", "baz": "blah"}
+ mgr._check_missing_update_attrs(data)
+
+ data = {"baz": "blah"}
+ with pytest.raises(AttributeError) as error:
+ mgr._check_missing_update_attrs(data)
+ assert "foo" in str(error.value)
+
+
+def test_update_mixin(gl):
+ class M(UpdateMixin, FakeManager):
+ _create_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+ _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",))
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ server_data = mgr.update(42, {"foo": "baz"})
+ assert isinstance(server_data, dict)
+ assert server_data["id"] == 42
+ assert server_data["foo"] == "baz"
+
+
+def test_update_mixin_no_id(gl):
+ class M(UpdateMixin, FakeManager):
+ _create_attrs = base.RequiredOptional(
+ required=("foo",), optional=("bar", "baz")
+ )
+ _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",))
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="put")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ server_data = mgr.update(new_data={"foo": "baz"})
+ assert isinstance(server_data, dict)
+ assert server_data["foo"] == "baz"
+
+
+def test_delete_mixin(gl):
+ class M(DeleteMixin, FakeManager):
+ pass
+
+ @urlmatch(
+ scheme="http", netloc="localhost", path="/api/v4/tests/42", method="delete"
+ )
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = ""
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ mgr.delete(42)
+
+
+def test_save_mixin(gl):
+ class M(UpdateMixin, FakeManager):
+ pass
+
+ class TestClass(SaveMixin, base.RESTObject):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"id": 42, "foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = TestClass(mgr, {"id": 42, "foo": "bar"})
+ obj.foo = "baz"
+ obj.save()
+ assert obj._attrs["foo"] == "baz"
+ assert obj._updated_attrs == {}
+
+
+def test_set_mixin(gl):
+ class M(SetMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/foo", method="put")
+ def resp_cont(url, request):
+ headers = {"Content-Type": "application/json"}
+ content = '{"key": "foo", "value": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(gl)
+ obj = mgr.set("foo", "bar")
+ assert isinstance(obj, FakeObject)
+ assert obj.key == "foo"
+ assert obj.value == "bar"
diff --git a/tests/unit/mixins/test_object_mixins_attributes.py b/tests/unit/mixins/test_object_mixins_attributes.py
new file mode 100644
index 0000000..d54fa3a
--- /dev/null
+++ b/tests/unit/mixins/test_object_mixins_attributes.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 Mika Mäenpää <mika.j.maenpaa@tut.fi>,
+# Tampere University of Technology
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from gitlab.mixins import (
+ AccessRequestMixin,
+ SetMixin,
+ SubscribableMixin,
+ TimeTrackingMixin,
+ TodoMixin,
+ UserAgentDetailMixin,
+)
+
+
+def test_access_request_mixin():
+ class TestClass(AccessRequestMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "approve")
+
+
+def test_subscribable_mixin():
+ class TestClass(SubscribableMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "subscribe")
+ assert hasattr(obj, "unsubscribe")
+
+
+def test_todo_mixin():
+ class TestClass(TodoMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "todo")
+
+
+def test_time_tracking_mixin():
+ class TestClass(TimeTrackingMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "time_stats")
+ assert hasattr(obj, "time_estimate")
+ assert hasattr(obj, "reset_time_estimate")
+ assert hasattr(obj, "add_spent_time")
+ assert hasattr(obj, "reset_spent_time")
+
+
+def test_set_mixin():
+ class TestClass(SetMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "set")
+
+
+def test_user_agent_detail_mixin():
+ class TestClass(UserAgentDetailMixin):
+ pass
+
+ obj = TestClass()
+ assert hasattr(obj, "user_agent_detail")
diff --git a/tests/unit/objects/__init__.py b/tests/unit/objects/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/objects/__init__.py
diff --git a/tests/unit/objects/conftest.py b/tests/unit/objects/conftest.py
new file mode 100644
index 0000000..d8a40d9
--- /dev/null
+++ b/tests/unit/objects/conftest.py
@@ -0,0 +1,70 @@
+"""Common mocks for resources in gitlab.v4.objects"""
+
+import re
+
+import pytest
+import responses
+
+
+@pytest.fixture
+def binary_content():
+ return b"binary content"
+
+
+@pytest.fixture
+def accepted_content():
+ return {"message": "202 Accepted"}
+
+
+@pytest.fixture
+def created_content():
+ return {"message": "201 Created"}
+
+
+@pytest.fixture
+def no_content():
+ return {"message": "204 No Content"}
+
+
+@pytest.fixture
+def resp_export(accepted_content, binary_content):
+ """Common fixture for group and project exports."""
+ export_status_content = {
+ "id": 1,
+ "description": "Itaque perspiciatis minima aspernatur",
+ "name": "Gitlab Test",
+ "name_with_namespace": "Gitlab Org / Gitlab Test",
+ "path": "gitlab-test",
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "created_at": "2017-08-29T04:36:44.383Z",
+ "export_status": "finished",
+ "_links": {
+ "api_url": "https://gitlab.test/api/v4/projects/1/export/download",
+ "web_url": "https://gitlab.test/gitlab-test/download_export",
+ },
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.POST,
+ url=re.compile(r".*/api/v4/(groups|projects)/1/export"),
+ json=accepted_content,
+ content_type="application/json",
+ status=202,
+ )
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(r".*/api/v4/(groups|projects)/1/export/download"),
+ body=binary_content,
+ content_type="application/octet-stream",
+ status=200,
+ )
+ # Currently only project export supports status checks
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/export",
+ json=export_status_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
diff --git a/tests/unit/objects/test_appearance.py b/tests/unit/objects/test_appearance.py
new file mode 100644
index 0000000..0de6524
--- /dev/null
+++ b/tests/unit/objects/test_appearance.py
@@ -0,0 +1,65 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/appearance.html
+"""
+
+import pytest
+import responses
+
+title = "GitLab Test Instance"
+description = "gitlab-test.example.com"
+new_title = "new-title"
+new_description = "new-description"
+
+
+@pytest.fixture
+def resp_application_appearance():
+ content = {
+ "title": title,
+ "description": description,
+ "logo": "/uploads/-/system/appearance/logo/1/logo.png",
+ "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png",
+ "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png",
+ "new_project_guidelines": "Please read the FAQs for help.",
+ "header_message": "",
+ "footer_message": "",
+ "message_background_color": "#e75e40",
+ "message_font_color": "#ffffff",
+ "email_header_and_footer_enabled": False,
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/application/appearance",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+
+ updated_content = dict(content)
+ updated_content["title"] = new_title
+ updated_content["description"] = new_description
+
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/application/appearance",
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_get_update_appearance(gl, resp_application_appearance):
+ appearance = gl.appearance.get()
+ assert appearance.title == title
+ assert appearance.description == description
+ appearance.title = new_title
+ appearance.description = new_description
+ appearance.save()
+ assert appearance.title == new_title
+ assert appearance.description == new_description
+
+
+def test_update_appearance(gl, resp_application_appearance):
+ gl.appearance.update(title=new_title, description=new_description)
diff --git a/tests/unit/objects/test_applications.py b/tests/unit/objects/test_applications.py
new file mode 100644
index 0000000..61de019
--- /dev/null
+++ b/tests/unit/objects/test_applications.py
@@ -0,0 +1,44 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/applications.html
+"""
+
+import pytest
+import responses
+
+title = "GitLab Test Instance"
+description = "gitlab-test.example.com"
+new_title = "new-title"
+new_description = "new-description"
+
+
+@pytest.fixture
+def resp_application_create():
+ content = {
+ "name": "test_app",
+ "redirect_uri": "http://localhost:8080",
+ "scopes": ["api", "email"],
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/applications",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_create_application(gl, resp_application_create):
+ application = gl.applications.create(
+ {
+ "name": "test_app",
+ "redirect_uri": "http://localhost:8080",
+ "scopes": ["api", "email"],
+ "confidential": False,
+ }
+ )
+ assert application.name == "test_app"
+ assert application.redirect_uri == "http://localhost:8080"
+ assert application.scopes == ["api", "email"]
diff --git a/tests/unit/objects/test_audit_events.py b/tests/unit/objects/test_audit_events.py
new file mode 100644
index 0000000..aba778b
--- /dev/null
+++ b/tests/unit/objects/test_audit_events.py
@@ -0,0 +1,109 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/audit_events.html#project-audit-events
+"""
+
+import re
+
+import pytest
+import responses
+
+from gitlab.v4.objects.audit_events import (
+ AuditEvent,
+ GroupAuditEvent,
+ ProjectAuditEvent,
+)
+
+id = 5
+
+audit_events_content = {
+ "id": 5,
+ "author_id": 1,
+ "entity_id": 7,
+ "entity_type": "Project",
+ "details": {
+ "change": "prevent merge request approval from reviewers",
+ "from": "",
+ "to": "true",
+ "author_name": "Administrator",
+ "target_id": 7,
+ "target_type": "Project",
+ "target_details": "twitter/typeahead-js",
+ "ip_address": "127.0.0.1",
+ "entity_path": "twitter/typeahead-js",
+ },
+ "created_at": "2020-05-26T22:55:04.230Z",
+}
+
+audit_events_url = re.compile(
+ r"http://localhost/api/v4/((groups|projects)/1/)?audit_events"
+)
+
+audit_events_url_id = re.compile(
+ rf"http://localhost/api/v4/((groups|projects)/1/)?audit_events/{id}"
+)
+
+
+@pytest.fixture
+def resp_list_audit_events():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=audit_events_url,
+ json=[audit_events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_audit_event():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=audit_events_url_id,
+ json=audit_events_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_instance_audit_events(gl, resp_list_audit_events):
+ audit_events = gl.audit_events.list()
+ assert isinstance(audit_events, list)
+ assert isinstance(audit_events[0], AuditEvent)
+ assert audit_events[0].id == id
+
+
+def test_get_instance_audit_events(gl, resp_get_audit_event):
+ audit_event = gl.audit_events.get(id)
+ assert isinstance(audit_event, AuditEvent)
+ assert audit_event.id == id
+
+
+def test_list_group_audit_events(group, resp_list_audit_events):
+ audit_events = group.audit_events.list()
+ assert isinstance(audit_events, list)
+ assert isinstance(audit_events[0], GroupAuditEvent)
+ assert audit_events[0].id == id
+
+
+def test_get_group_audit_events(group, resp_get_audit_event):
+ audit_event = group.audit_events.get(id)
+ assert isinstance(audit_event, GroupAuditEvent)
+ assert audit_event.id == id
+
+
+def test_list_project_audit_events(project, resp_list_audit_events):
+ audit_events = project.audit_events.list()
+ assert isinstance(audit_events, list)
+ assert isinstance(audit_events[0], ProjectAuditEvent)
+ assert audit_events[0].id == id
+
+
+def test_get_project_audit_events(project, resp_get_audit_event):
+ audit_event = project.audit_events.get(id)
+ assert isinstance(audit_event, ProjectAuditEvent)
+ assert audit_event.id == id
diff --git a/tests/unit/objects/test_badges.py b/tests/unit/objects/test_badges.py
new file mode 100644
index 0000000..e226684
--- /dev/null
+++ b/tests/unit/objects/test_badges.py
@@ -0,0 +1,210 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/project_badges.html
+GitLab API: https://docs.gitlab.com/ee/api/group_badges.html
+"""
+import re
+
+import pytest
+import responses
+
+from gitlab.v4.objects import GroupBadge, ProjectBadge
+
+link_url = (
+ "http://example.com/ci_status.svg?project=example-org/example-project&ref=master"
+)
+image_url = "https://example.io/my/badge"
+
+rendered_link_url = (
+ "http://example.com/ci_status.svg?project=example-org/example-project&ref=master"
+)
+rendered_image_url = "https://example.io/my/badge"
+
+new_badge = {
+ "link_url": link_url,
+ "image_url": image_url,
+}
+
+badge_content = {
+ "name": "Coverage",
+ "id": 1,
+ "link_url": link_url,
+ "image_url": image_url,
+ "rendered_link_url": rendered_image_url,
+ "rendered_image_url": rendered_image_url,
+}
+
+preview_badge_content = {
+ "link_url": link_url,
+ "image_url": image_url,
+ "rendered_link_url": rendered_link_url,
+ "rendered_image_url": rendered_image_url,
+}
+
+
+@pytest.fixture()
+def resp_get_badge():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"),
+ json=badge_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_list_badges():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"),
+ json=[badge_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_create_badge():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"),
+ json=badge_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_update_badge():
+ updated_content = dict(badge_content)
+ updated_content["link_url"] = "http://link_url"
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.PUT,
+ url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"),
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_delete_badge(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"),
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_preview_badge():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(
+ r"http://localhost/api/v4/(projects|groups)/1/badges/render"
+ ),
+ json=preview_badge_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_badges(project, resp_list_badges):
+ badges = project.badges.list()
+ assert isinstance(badges, list)
+ assert isinstance(badges[0], ProjectBadge)
+
+
+def test_list_group_badges(group, resp_list_badges):
+ badges = group.badges.list()
+ assert isinstance(badges, list)
+ assert isinstance(badges[0], GroupBadge)
+
+
+def test_get_project_badge(project, resp_get_badge):
+ badge = project.badges.get(1)
+ assert isinstance(badge, ProjectBadge)
+ assert badge.name == "Coverage"
+ assert badge.id == 1
+
+
+def test_get_group_badge(group, resp_get_badge):
+ badge = group.badges.get(1)
+ assert isinstance(badge, GroupBadge)
+ assert badge.name == "Coverage"
+ assert badge.id == 1
+
+
+def test_delete_project_badge(project, resp_delete_badge):
+ badge = project.badges.get(1, lazy=True)
+ badge.delete()
+
+
+def test_delete_group_badge(group, resp_delete_badge):
+ badge = group.badges.get(1, lazy=True)
+ badge.delete()
+
+
+def test_create_project_badge(project, resp_create_badge):
+ badge = project.badges.create(new_badge)
+ assert isinstance(badge, ProjectBadge)
+ assert badge.image_url == image_url
+
+
+def test_create_group_badge(group, resp_create_badge):
+ badge = group.badges.create(new_badge)
+ assert isinstance(badge, GroupBadge)
+ assert badge.image_url == image_url
+
+
+def test_preview_project_badge(project, resp_preview_badge):
+ output = project.badges.render(
+ link_url=link_url,
+ image_url=image_url,
+ )
+ assert isinstance(output, dict)
+ assert "rendered_link_url" in output
+ assert "rendered_image_url" in output
+ assert output["link_url"] == output["rendered_link_url"]
+ assert output["image_url"] == output["rendered_image_url"]
+
+
+def test_preview_group_badge(group, resp_preview_badge):
+ output = group.badges.render(
+ link_url=link_url,
+ image_url=image_url,
+ )
+ assert isinstance(output, dict)
+ assert "rendered_link_url" in output
+ assert "rendered_image_url" in output
+ assert output["link_url"] == output["rendered_link_url"]
+ assert output["image_url"] == output["rendered_image_url"]
+
+
+def test_update_project_badge(project, resp_update_badge):
+ badge = project.badges.get(1, lazy=True)
+ badge.link_url = "http://link_url"
+ badge.save()
+ assert badge.link_url == "http://link_url"
+
+
+def test_update_group_badge(group, resp_update_badge):
+ badge = group.badges.get(1, lazy=True)
+ badge.link_url = "http://link_url"
+ badge.save()
+ assert badge.link_url == "http://link_url"
diff --git a/tests/unit/objects/test_bridges.py b/tests/unit/objects/test_bridges.py
new file mode 100644
index 0000000..4d39186
--- /dev/null
+++ b/tests/unit/objects/test_bridges.py
@@ -0,0 +1,109 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/jobs.html#list-pipeline-bridges
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectPipelineBridge
+
+
+@pytest.fixture
+def resp_list_bridges():
+ export_bridges_content = {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration.",
+ },
+ "allow_failure": False,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "finished_at": "2015-12-24T17:58:27.895Z",
+ "duration": 240,
+ "id": 7,
+ "name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending",
+ "created_at": "2015-12-24T15:50:16.123Z",
+ "updated_at": "2015-12-24T18:00:44.432Z",
+ "web_url": "https://example.com/foo/bar/pipelines/6",
+ },
+ "ref": "master",
+ "stage": "test",
+ "status": "pending",
+ "tag": False,
+ "web_url": "https://example.com/foo/bar/-/jobs/7",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.dev/root",
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "public_email": "",
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": "",
+ },
+ "downstream_pipeline": {
+ "id": 5,
+ "sha": "f62a4b2fb89754372a346f24659212eb8da13601",
+ "ref": "master",
+ "status": "pending",
+ "created_at": "2015-12-24T17:54:27.722Z",
+ "updated_at": "2015-12-24T17:58:27.896Z",
+ "web_url": "https://example.com/diaspora/diaspora-client/pipelines/5",
+ },
+ }
+
+ export_pipelines_content = [
+ {
+ "id": 6,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "web_url": "https://example.com/foo/bar/pipelines/47",
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ },
+ ]
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/pipelines/6/bridges",
+ json=[export_bridges_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/pipelines",
+ json=export_pipelines_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_projects_pipelines_bridges(project, resp_list_bridges):
+ pipeline = project.pipelines.list()[0]
+ bridges = pipeline.bridges.list()
+
+ assert isinstance(bridges, list)
+ assert isinstance(bridges[0], ProjectPipelineBridge)
+ assert bridges[0].downstream_pipeline["id"] == 5
+ assert (
+ bridges[0].downstream_pipeline["sha"]
+ == "f62a4b2fb89754372a346f24659212eb8da13601"
+ )
diff --git a/tests/unit/objects/test_commits.py b/tests/unit/objects/test_commits.py
new file mode 100644
index 0000000..6b98117
--- /dev/null
+++ b/tests/unit/objects/test_commits.py
@@ -0,0 +1,115 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/commits.html
+"""
+
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_create_commit():
+ content = {
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f",
+ "title": "Commit message",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/repository/commits",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_commit():
+ get_content = {
+ "id": "6b2257eabcec3db1f59dafbd84935e3caea04235",
+ "short_id": "6b2257ea",
+ "title": "Initial commit",
+ }
+ revert_content = {
+ "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title": 'Revert "Initial commit"',
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea",
+ json=get_content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/revert",
+ json=revert_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_commit_gpg_signature():
+ content = {
+ "gpg_key_id": 1,
+ "gpg_key_primary_keyid": "8254AAB3FBD54AC9",
+ "gpg_key_user_name": "John Doe",
+ "gpg_key_user_email": "johndoe@example.com",
+ "verification_status": "verified",
+ "gpg_key_subkey_id": None,
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/signature",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_get_commit(project, resp_commit):
+ commit = project.commits.get("6b2257ea")
+ assert commit.short_id == "6b2257ea"
+ assert commit.title == "Initial commit"
+
+
+def test_create_commit(project, resp_create_commit):
+ data = {
+ "branch": "master",
+ "commit_message": "Commit message",
+ "actions": [
+ {
+ "action": "create",
+ "file_path": "README",
+ "content": "",
+ }
+ ],
+ }
+ commit = project.commits.create(data)
+ assert commit.short_id == "ed899a2f"
+ assert commit.title == data["commit_message"]
+
+
+def test_revert_commit(project, resp_commit):
+ commit = project.commits.get("6b2257ea", lazy=True)
+ revert_commit = commit.revert(branch="master")
+ assert revert_commit["short_id"] == "8b090c1b"
+ assert revert_commit["title"] == 'Revert "Initial commit"'
+
+
+def test_get_commit_gpg_signature(project, resp_get_commit_gpg_signature):
+ commit = project.commits.get("6b2257ea", lazy=True)
+ signature = commit.signature()
+ assert signature["gpg_key_primary_keyid"] == "8254AAB3FBD54AC9"
+ assert signature["verification_status"] == "verified"
diff --git a/tests/unit/objects/test_deploy_tokens.py b/tests/unit/objects/test_deploy_tokens.py
new file mode 100644
index 0000000..66a79fa
--- /dev/null
+++ b/tests/unit/objects/test_deploy_tokens.py
@@ -0,0 +1,45 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectDeployToken
+
+create_content = {
+ "id": 1,
+ "name": "test_deploy_token",
+ "username": "custom-user",
+ "expires_at": "2022-01-01T00:00:00.000Z",
+ "token": "jMRvtPNxrn3crTAGukpZ",
+ "scopes": ["read_repository"],
+}
+
+
+@pytest.fixture
+def resp_deploy_token_create():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/deploy_tokens",
+ json=create_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_deploy_tokens(gl, resp_deploy_token_create):
+ deploy_token = gl.projects.get(1, lazy=True).deploytokens.create(
+ {
+ "name": "test_deploy_token",
+ "expires_at": "2022-01-01T00:00:00.000Z",
+ "username": "custom-user",
+ "scopes": ["read_repository"],
+ }
+ )
+ assert isinstance(deploy_token, ProjectDeployToken)
+ assert deploy_token.id == 1
+ assert deploy_token.expires_at == "2022-01-01T00:00:00.000Z"
+ assert deploy_token.username == "custom-user"
+ assert deploy_token.scopes == ["read_repository"]
diff --git a/tests/unit/objects/test_deployments.py b/tests/unit/objects/test_deployments.py
new file mode 100644
index 0000000..3cde8fe
--- /dev/null
+++ b/tests/unit/objects/test_deployments.py
@@ -0,0 +1,50 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/deployments.html
+"""
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_deployment():
+ content = {"id": 42, "status": "success", "ref": "master"}
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/deployments",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+
+ updated_content = dict(content)
+ updated_content["status"] = "failed"
+
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/deployments/42",
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_deployment(project, resp_deployment):
+ deployment = project.deployments.create(
+ {
+ "environment": "Test",
+ "sha": "1agf4gs",
+ "ref": "master",
+ "tag": False,
+ "status": "created",
+ }
+ )
+ assert deployment.id == 42
+ assert deployment.status == "success"
+ assert deployment.ref == "master"
+
+ deployment.status = "failed"
+ deployment.save()
+ assert deployment.status == "failed"
diff --git a/tests/unit/objects/test_environments.py b/tests/unit/objects/test_environments.py
new file mode 100644
index 0000000..b49a1db
--- /dev/null
+++ b/tests/unit/objects/test_environments.py
@@ -0,0 +1,30 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/environments.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectEnvironment
+
+
+@pytest.fixture
+def resp_get_environment():
+ content = {"name": "environment_name", "id": 1, "last_deployment": "sometime"}
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/environments/1",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_project_environments(project, resp_get_environment):
+ environment = project.environments.get(1)
+ assert isinstance(environment, ProjectEnvironment)
+ assert environment.id == 1
+ assert environment.last_deployment == "sometime"
+ assert environment.name == "environment_name"
diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py
new file mode 100644
index 0000000..d4786f4
--- /dev/null
+++ b/tests/unit/objects/test_groups.py
@@ -0,0 +1,97 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/groups.html
+"""
+
+import pytest
+import responses
+
+import gitlab
+
+
+@pytest.fixture
+def resp_groups():
+ content = {"name": "name", "id": 1, "path": "path"}
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/groups/1",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/groups",
+ json=[content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/groups",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_create_import(accepted_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/groups/import",
+ json=accepted_content,
+ content_type="application/json",
+ status=202,
+ )
+ yield rsps
+
+
+def test_get_group(gl, resp_groups):
+ data = gl.groups.get(1)
+ assert isinstance(data, gitlab.v4.objects.Group)
+ assert data.name == "name"
+ assert data.path == "path"
+ assert data.id == 1
+
+
+def test_create_group(gl, resp_groups):
+ name, path = "name", "path"
+ data = gl.groups.create({"name": name, "path": path})
+ assert isinstance(data, gitlab.v4.objects.Group)
+ assert data.name == name
+ assert data.path == path
+
+
+def test_create_group_export(group, resp_export):
+ export = group.exports.create()
+ assert export.message == "202 Accepted"
+
+
+@pytest.mark.skip("GitLab API endpoint not implemented")
+def test_refresh_group_export_status(group, resp_export):
+ export = group.exports.create()
+ export.refresh()
+ assert export.export_status == "finished"
+
+
+def test_download_group_export(group, resp_export, binary_content):
+ export = group.exports.create()
+ download = export.download()
+ assert isinstance(download, bytes)
+ assert download == binary_content
+
+
+def test_import_group(gl, resp_create_import):
+ group_import = gl.groups.import_group("file", "api-group", "API Group")
+ assert group_import["message"] == "202 Accepted"
+
+
+@pytest.mark.skip("GitLab API endpoint not implemented")
+def test_refresh_group_import_status(group, resp_groups):
+ group_import = group.imports.get()
+ group_import.refresh()
+ assert group_import.import_status == "finished"
diff --git a/tests/unit/objects/test_hooks.py b/tests/unit/objects/test_hooks.py
new file mode 100644
index 0000000..fe5c21c
--- /dev/null
+++ b/tests/unit/objects/test_hooks.py
@@ -0,0 +1,29 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/system_hooks.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import Hook
+
+
+@pytest.fixture
+def resp_get_hook():
+ content = {"url": "testurl", "id": 1}
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/hooks/1",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_hooks(gl, resp_get_hook):
+ data = gl.hooks.get(1)
+ assert isinstance(data, Hook)
+ assert data.url == "testurl"
+ assert data.id == 1
diff --git a/tests/unit/objects/test_issues.py b/tests/unit/objects/test_issues.py
new file mode 100644
index 0000000..93d8e0c
--- /dev/null
+++ b/tests/unit/objects/test_issues.py
@@ -0,0 +1,69 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/issues.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectIssuesStatistics
+
+
+@pytest.fixture
+def resp_list_issues():
+ content = [{"name": "name", "id": 1}, {"name": "other_name", "id": 2}]
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/issues",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_issue():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/issues/1",
+ json={"name": "name", "id": 1},
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_issue_statistics():
+ content = {"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}}
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues_statistics",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_issues(gl, resp_list_issues):
+ data = gl.issues.list()
+ assert data[1].id == 2
+ assert data[1].name == "other_name"
+
+
+def test_get_issue(gl, resp_get_issue):
+ issue = gl.issues.get(1)
+ assert issue.id == 1
+ assert issue.name == "name"
+
+
+def test_project_issues_statistics(project, resp_issue_statistics):
+ statistics = project.issuesstatistics.get()
+ assert isinstance(statistics, ProjectIssuesStatistics)
+ assert statistics.statistics["counts"]["all"] == 20
diff --git a/tests/unit/objects/test_job_artifacts.py b/tests/unit/objects/test_job_artifacts.py
new file mode 100644
index 0000000..7c5f1df
--- /dev/null
+++ b/tests/unit/objects/test_job_artifacts.py
@@ -0,0 +1,30 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/job_artifacts.html
+"""
+
+import pytest
+import responses
+
+ref_name = "master"
+job = "build"
+
+
+@pytest.fixture
+def resp_artifacts_by_ref_name(binary_content):
+ url = f"http://localhost/api/v4/projects/1/jobs/artifacts/{ref_name}/download?job={job}"
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=url,
+ body=binary_content,
+ content_type="application/octet-stream",
+ status=200,
+ )
+ yield rsps
+
+
+def test_download_artifacts_by_ref_name(gl, binary_content, resp_artifacts_by_ref_name):
+ project = gl.projects.get(1, lazy=True)
+ artifacts = project.artifacts(ref_name=ref_name, job=job)
+ assert artifacts == binary_content
diff --git a/tests/unit/objects/test_jobs.py b/tests/unit/objects/test_jobs.py
new file mode 100644
index 0000000..104d59d
--- /dev/null
+++ b/tests/unit/objects/test_jobs.py
@@ -0,0 +1,96 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/jobs.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectJob
+
+job_content = {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ },
+ "coverage": None,
+ "allow_failure": False,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "duration": 0.465,
+ "queued_duration": 0.010,
+ "artifacts_expire_at": "2016-01-23T17:54:31.198Z",
+ "tag_list": ["docker runner", "macos-10.15"],
+ "id": 1,
+ "name": "rubocop",
+ "pipeline": {
+ "id": 1,
+ "project_id": 1,
+ },
+ "ref": "master",
+ "artifacts": [],
+ "runner": None,
+ "stage": "test",
+ "status": "failed",
+ "tag": False,
+ "web_url": "https://example.com/foo/bar/-/jobs/1",
+ "user": {"id": 1},
+}
+
+
+@pytest.fixture
+def resp_get_job():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/jobs/1",
+ json=job_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_cancel_job():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/jobs/1/cancel",
+ json=job_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_retry_job():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/jobs/1/retry",
+ json=job_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+def test_get_project_job(project, resp_get_job):
+ job = project.jobs.get(1)
+ assert isinstance(job, ProjectJob)
+ assert job.ref == "master"
+
+
+def test_cancel_project_job(project, resp_cancel_job):
+ job = project.jobs.get(1, lazy=True)
+
+ output = job.cancel()
+ assert output["ref"] == "master"
+
+
+def test_retry_project_job(project, resp_retry_job):
+ job = project.jobs.get(1, lazy=True)
+
+ output = job.retry()
+ assert output["ref"] == "master"
diff --git a/tests/unit/objects/test_members.py b/tests/unit/objects/test_members.py
new file mode 100644
index 0000000..6a39369
--- /dev/null
+++ b/tests/unit/objects/test_members.py
@@ -0,0 +1,58 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/members.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import GroupBillableMember
+
+billable_members_content = [
+ {
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "last_activity_on": "2021-01-27",
+ "membership_type": "group_member",
+ "removable": True,
+ }
+]
+
+
+@pytest.fixture
+def resp_list_billable_group_members():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/groups/1/billable_members",
+ json=billable_members_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_billable_group_member(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url="http://localhost/api/v4/groups/1/billable_members/1",
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+def test_list_group_billable_members(group, resp_list_billable_group_members):
+ billable_members = group.billable_members.list()
+ assert isinstance(billable_members, list)
+ assert isinstance(billable_members[0], GroupBillableMember)
+ assert billable_members[0].removable is True
+
+
+def test_delete_group_billable_member(group, resp_delete_billable_group_member):
+ group.billable_members.delete(1)
diff --git a/tests/unit/objects/test_mro.py b/tests/unit/objects/test_mro.py
new file mode 100644
index 0000000..8f67b77
--- /dev/null
+++ b/tests/unit/objects/test_mro.py
@@ -0,0 +1,122 @@
+"""
+Ensure objects defined in gitlab.v4.objects have REST* as last item in class
+definition
+
+Original notes by John L. Villalovos
+
+An example of an incorrect definition:
+ class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin):
+ ^^^^^^^^^^ This should be at the end.
+
+Correct way would be:
+ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject):
+ Correctly at the end ^^^^^^^^^^
+
+
+Why this is an issue:
+
+ When we do type-checking for gitlab/mixins.py we make RESTObject or
+ RESTManager the base class for the mixins
+
+ Here is how our classes look when type-checking:
+
+ class RESTObject(object):
+ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
+ ...
+
+ class Mixin(RESTObject):
+ ...
+
+ # Wrong ordering here
+ class Wrongv4Object(RESTObject, RefreshMixin):
+ ...
+
+ If we actually ran this in Python we would get the following error:
+ class Wrongv4Object(RESTObject, Mixin):
+ TypeError: Cannot create a consistent method resolution
+ order (MRO) for bases RESTObject, Mixin
+
+ When we are type-checking it fails to understand the class Wrongv4Object
+ and thus we can't type check it correctly.
+
+Almost all classes in gitlab/v4/objects/*py were already correct before this
+check was added.
+"""
+import inspect
+
+import pytest
+
+import gitlab.v4.objects
+
+
+def test_show_issue():
+ """Test case to demonstrate the TypeError that occurs"""
+
+ class RESTObject(object):
+ def __init__(self, manager: str, attrs: int) -> None:
+ ...
+
+ class Mixin(RESTObject):
+ ...
+
+ with pytest.raises(TypeError) as exc_info:
+ # Wrong ordering here
+ class Wrongv4Object(RESTObject, Mixin):
+ ...
+
+ # The error message in the exception should be:
+ # TypeError: Cannot create a consistent method resolution
+ # order (MRO) for bases RESTObject, Mixin
+
+ # Make sure the exception string contains "MRO"
+ assert "MRO" in exc_info.exconly()
+
+ # Correctly ordered class, no exception
+ class Correctv4Object(Mixin, RESTObject):
+ ...
+
+
+def test_mros():
+ """Ensure objects defined in gitlab.v4.objects have REST* as last item in
+ class definition.
+
+ We do this as we need to ensure the MRO (Method Resolution Order) is
+ correct.
+ """
+
+ failed_messages = []
+ for module_name, module_value in inspect.getmembers(gitlab.v4.objects):
+ if not inspect.ismodule(module_value):
+ # We only care about the modules
+ continue
+ # Iterate through all the classes in our module
+ for class_name, class_value in inspect.getmembers(module_value):
+ if not inspect.isclass(class_value):
+ continue
+
+ # Ignore imported classes from gitlab.base
+ if class_value.__module__ == "gitlab.base":
+ continue
+
+ mro = class_value.mro()
+
+ # We only check classes which have a 'gitlab.base' class in their MRO
+ has_base = False
+ for count, obj in enumerate(mro, start=1):
+ if obj.__module__ == "gitlab.base":
+ has_base = True
+ base_classname = obj.__name__
+ if has_base:
+ filename = inspect.getfile(class_value)
+ # NOTE(jlvillal): The very last item 'mro[-1]' is always going
+ # to be 'object'. That is why we are checking 'mro[-2]'.
+ if mro[-2].__module__ != "gitlab.base":
+ failed_messages.append(
+ (
+ f"class definition for {class_name!r} in file {filename!r} "
+ f"must have {base_classname!r} as the last class in the "
+ f"class definition"
+ )
+ )
+ failed_msg = "\n".join(failed_messages)
+ assert not failed_messages, failed_msg
diff --git a/tests/unit/objects/test_packages.py b/tests/unit/objects/test_packages.py
new file mode 100644
index 0000000..672eee0
--- /dev/null
+++ b/tests/unit/objects/test_packages.py
@@ -0,0 +1,186 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/packages.html
+"""
+import re
+
+import pytest
+import responses
+
+from gitlab.v4.objects import GroupPackage, ProjectPackage, ProjectPackageFile
+
+package_content = {
+ "id": 1,
+ "name": "com/mycompany/my-app",
+ "version": "1.0-SNAPSHOT",
+ "package_type": "maven",
+ "_links": {
+ "web_path": "/namespace1/project1/-/packages/1",
+ "delete_api_path": "/namespace1/project1/-/packages/1",
+ },
+ "created_at": "2019-11-27T03:37:38.711Z",
+ "pipeline": {
+ "id": 123,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "web_url": "https://example.com/foo/bar/pipelines/47",
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "user": {
+ "name": "Administrator",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ },
+ },
+ "versions": [
+ {
+ "id": 2,
+ "version": "2.0-SNAPSHOT",
+ "created_at": "2020-04-28T04:42:11.573Z",
+ "pipeline": {
+ "id": 234,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "web_url": "https://example.com/foo/bar/pipelines/58",
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "user": {
+ "name": "Administrator",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ },
+ },
+ }
+ ],
+}
+
+package_file_content = [
+ {
+ "id": 25,
+ "package_id": 1,
+ "created_at": "2018-11-07T15:25:52.199Z",
+ "file_name": "my-app-1.5-20181107.152550-1.jar",
+ "size": 2421,
+ "file_md5": "58e6a45a629910c6ff99145a688971ac",
+ "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe",
+ "pipelines": [
+ {
+ "id": 123,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "web_url": "https://example.com/foo/bar/pipelines/47",
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "user": {
+ "name": "Administrator",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ },
+ }
+ ],
+ },
+ {
+ "id": 26,
+ "package_id": 1,
+ "created_at": "2018-11-07T15:25:56.776Z",
+ "file_name": "my-app-1.5-20181107.152550-1.pom",
+ "size": 1122,
+ "file_md5": "d90f11d851e17c5513586b4a7e98f1b2",
+ "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5",
+ },
+ {
+ "id": 27,
+ "package_id": 1,
+ "created_at": "2018-11-07T15:26:00.556Z",
+ "file_name": "maven-metadata.xml",
+ "size": 767,
+ "file_md5": "6dfd0cce1203145a927fef5e3a1c650c",
+ "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2",
+ },
+]
+
+
+@pytest.fixture
+def resp_list_packages():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/packages"),
+ json=[package_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_package():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/packages/1",
+ json=package_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_package(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url="http://localhost/api/v4/projects/1/packages/1",
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_list_package_files():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(
+ r"http://localhost/api/v4/projects/1/packages/1/package_files"
+ ),
+ json=package_file_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_packages(project, resp_list_packages):
+ packages = project.packages.list()
+ assert isinstance(packages, list)
+ assert isinstance(packages[0], ProjectPackage)
+ assert packages[0].version == "1.0-SNAPSHOT"
+
+
+def test_list_group_packages(group, resp_list_packages):
+ packages = group.packages.list()
+ assert isinstance(packages, list)
+ assert isinstance(packages[0], GroupPackage)
+ assert packages[0].version == "1.0-SNAPSHOT"
+
+
+def test_get_project_package(project, resp_get_package):
+ package = project.packages.get(1)
+ assert isinstance(package, ProjectPackage)
+ assert package.version == "1.0-SNAPSHOT"
+
+
+def test_delete_project_package(project, resp_delete_package):
+ package = project.packages.get(1, lazy=True)
+ package.delete()
+
+
+def test_list_project_package_files(project, resp_list_package_files):
+ package = project.packages.get(1, lazy=True)
+ package_files = package.package_files.list()
+ assert isinstance(package_files, list)
+ assert isinstance(package_files[0], ProjectPackageFile)
+ assert package_files[0].id == 25
diff --git a/tests/unit/objects/test_personal_access_tokens.py b/tests/unit/objects/test_personal_access_tokens.py
new file mode 100644
index 0000000..920cb1d
--- /dev/null
+++ b/tests/unit/objects/test_personal_access_tokens.py
@@ -0,0 +1,46 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/personal_access_tokens.html
+"""
+
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_list_personal_access_token():
+ content = [
+ {
+ "id": 4,
+ "name": "Test Token",
+ "revoked": False,
+ "created_at": "2020-07-23T14:31:47.729Z",
+ "scopes": ["api"],
+ "active": True,
+ "user_id": 24,
+ "expires_at": None,
+ }
+ ]
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/personal_access_tokens",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_personal_access_tokens(gl, resp_list_personal_access_token):
+ access_tokens = gl.personal_access_tokens.list()
+ assert len(access_tokens) == 1
+ assert access_tokens[0].revoked is False
+ assert access_tokens[0].name == "Test Token"
+
+
+def test_list_personal_access_tokens_filter(gl, resp_list_personal_access_token):
+ access_tokens = gl.personal_access_tokens.list(user_id=24)
+ assert len(access_tokens) == 1
+ assert access_tokens[0].revoked is False
+ assert access_tokens[0].user_id == 24
diff --git a/tests/unit/objects/test_pipeline_schedules.py b/tests/unit/objects/test_pipeline_schedules.py
new file mode 100644
index 0000000..c5dcc76
--- /dev/null
+++ b/tests/unit/objects/test_pipeline_schedules.py
@@ -0,0 +1,62 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/pipeline_schedules.html
+"""
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_project_pipeline_schedule(created_content):
+ content = {
+ "id": 14,
+ "description": "Build packages",
+ "ref": "master",
+ "cron": "0 1 * * 5",
+ "cron_timezone": "UTC",
+ "next_run_at": "2017-05-26T01:00:00.000Z",
+ "active": True,
+ "created_at": "2017-05-19T13:43:08.169Z",
+ "updated_at": "2017-05-19T13:43:08.169Z",
+ "last_pipeline": None,
+ "owner": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root",
+ },
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/pipeline_schedules",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/pipeline_schedules/14/play",
+ json=created_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+def test_project_pipeline_schedule_play(project, resp_project_pipeline_schedule):
+ description = "Build packages"
+ cronline = "0 1 * * 5"
+ sched = project.pipelineschedules.create(
+ {"ref": "master", "description": description, "cron": cronline}
+ )
+ assert sched is not None
+ assert description == sched.description
+ assert cronline == sched.cron
+
+ play_result = sched.play()
+ assert play_result is not None
+ assert "message" in play_result
+ assert play_result["message"] == "201 Created"
diff --git a/tests/unit/objects/test_pipelines.py b/tests/unit/objects/test_pipelines.py
new file mode 100644
index 0000000..d474296
--- /dev/null
+++ b/tests/unit/objects/test_pipelines.py
@@ -0,0 +1,94 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/pipelines.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectPipeline
+
+pipeline_content = {
+ "id": 46,
+ "project_id": 1,
+ "status": "pending",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "tag": False,
+ "yaml_errors": None,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root",
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": None,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": None,
+ "duration": None,
+ "queued_duration": 0.010,
+ "coverage": None,
+ "web_url": "https://example.com/foo/bar/pipelines/46",
+}
+
+
+@pytest.fixture
+def resp_get_pipeline():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/pipelines/1",
+ json=pipeline_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_cancel_pipeline():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/pipelines/1/cancel",
+ json=pipeline_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_retry_pipeline():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/pipelines/1/retry",
+ json=pipeline_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+def test_get_project_pipeline(project, resp_get_pipeline):
+ pipeline = project.pipelines.get(1)
+ assert isinstance(pipeline, ProjectPipeline)
+ assert pipeline.ref == "master"
+
+
+def test_cancel_project_pipeline(project, resp_cancel_pipeline):
+ pipeline = project.pipelines.get(1, lazy=True)
+
+ output = pipeline.cancel()
+ assert output["ref"] == "master"
+
+
+def test_retry_project_pipeline(project, resp_retry_pipeline):
+ pipeline = project.pipelines.get(1, lazy=True)
+
+ output = pipeline.retry()
+ assert output["ref"] == "master"
diff --git a/tests/unit/objects/test_project_access_tokens.py b/tests/unit/objects/test_project_access_tokens.py
new file mode 100644
index 0000000..4d4788d
--- /dev/null
+++ b/tests/unit/objects/test_project_access_tokens.py
@@ -0,0 +1,113 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/resource_access_tokens.html
+"""
+
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_list_project_access_token():
+ content = [
+ {
+ "user_id": 141,
+ "scopes": ["api"],
+ "name": "token",
+ "expires_at": "2021-01-31",
+ "id": 42,
+ "active": True,
+ "created_at": "2021-01-20T22:11:48.151Z",
+ "revoked": False,
+ }
+ ]
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/access_tokens",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_create_project_access_token():
+ content = {
+ "user_id": 141,
+ "scopes": ["api"],
+ "name": "token",
+ "expires_at": "2021-01-31",
+ "id": 42,
+ "active": True,
+ "created_at": "2021-01-20T22:11:48.151Z",
+ "revoked": False,
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/access_tokens",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_revoke_project_access_token():
+ content = [
+ {
+ "user_id": 141,
+ "scopes": ["api"],
+ "name": "token",
+ "expires_at": "2021-01-31",
+ "id": 42,
+ "active": True,
+ "created_at": "2021-01-20T22:11:48.151Z",
+ "revoked": False,
+ }
+ ]
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url="http://localhost/api/v4/projects/1/access_tokens/42",
+ json=content,
+ content_type="application/json",
+ status=204,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/access_tokens",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_access_tokens(gl, resp_list_project_access_token):
+ access_tokens = gl.projects.get(1, lazy=True).access_tokens.list()
+ assert len(access_tokens) == 1
+ assert access_tokens[0].revoked is False
+ assert access_tokens[0].name == "token"
+
+
+def test_create_project_access_token(gl, resp_create_project_access_token):
+ access_tokens = gl.projects.get(1, lazy=True).access_tokens.create(
+ {"name": "test", "scopes": ["api"]}
+ )
+ assert access_tokens.revoked is False
+ assert access_tokens.user_id == 141
+ assert access_tokens.expires_at == "2021-01-31"
+
+
+def test_revoke_project_access_token(
+ gl, resp_list_project_access_token, resp_revoke_project_access_token
+):
+ gl.projects.get(1, lazy=True).access_tokens.delete(42)
+ access_token = gl.projects.get(1, lazy=True).access_tokens.list()[0]
+ access_token.delete()
diff --git a/tests/unit/objects/test_project_import_export.py b/tests/unit/objects/test_project_import_export.py
new file mode 100644
index 0000000..78e51b1
--- /dev/null
+++ b/tests/unit/objects/test_project_import_export.py
@@ -0,0 +1,112 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/project_import_export.html
+"""
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_import_project():
+ content = {
+ "id": 1,
+ "description": None,
+ "name": "api-project",
+ "name_with_namespace": "Administrator / api-project",
+ "path": "api-project",
+ "path_with_namespace": "root/api-project",
+ "created_at": "2018-02-13T09:05:58.023Z",
+ "import_status": "scheduled",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/import",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_import_status():
+ content = {
+ "id": 1,
+ "description": "Itaque perspiciatis minima aspernatur corporis consequatur.",
+ "name": "Gitlab Test",
+ "name_with_namespace": "Gitlab Org / Gitlab Test",
+ "path": "gitlab-test",
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "created_at": "2017-08-29T04:36:44.383Z",
+ "import_status": "finished",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/import",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_import_github():
+ content = {
+ "id": 27,
+ "name": "my-repo",
+ "full_path": "/root/my-repo",
+ "full_name": "Administrator / my-repo",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/import/github",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_import_project(gl, resp_import_project):
+ project_import = gl.projects.import_project("file", "api-project")
+ assert project_import["import_status"] == "scheduled"
+
+
+def test_refresh_project_import_status(project, resp_import_status):
+ project_import = project.imports.get()
+ project_import.refresh()
+ assert project_import.import_status == "finished"
+
+
+def test_import_github(gl, resp_import_github):
+ base_path = "/root"
+ name = "my-repo"
+ ret = gl.projects.import_github("githubkey", 1234, base_path, name)
+ assert isinstance(ret, dict)
+ assert ret["name"] == name
+ assert ret["full_path"] == "/".join((base_path, name))
+ assert ret["full_name"].endswith(name)
+
+
+def test_create_project_export(project, resp_export):
+ export = project.exports.create()
+ assert export.message == "202 Accepted"
+
+
+def test_refresh_project_export_status(project, resp_export):
+ export = project.exports.create()
+ export.refresh()
+ assert export.export_status == "finished"
+
+
+def test_download_project_export(project, resp_export, binary_content):
+ export = project.exports.create()
+ download = export.download()
+ assert isinstance(download, bytes)
+ assert download == binary_content
diff --git a/tests/unit/objects/test_project_merge_request_approvals.py b/tests/unit/objects/test_project_merge_request_approvals.py
new file mode 100644
index 0000000..16d58bd
--- /dev/null
+++ b/tests/unit/objects/test_project_merge_request_approvals.py
@@ -0,0 +1,317 @@
+"""
+Gitlab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html
+"""
+
+import copy
+
+import pytest
+import responses
+
+import gitlab
+
+approval_rule_id = 1
+approval_rule_name = "security"
+approvals_required = 3
+user_ids = [5, 50]
+group_ids = [5]
+
+new_approval_rule_name = "new approval rule"
+new_approval_rule_user_ids = user_ids
+new_approval_rule_approvals_required = 2
+
+updated_approval_rule_user_ids = [5]
+updated_approval_rule_approvals_required = 1
+
+
+@pytest.fixture
+def resp_snippet():
+ merge_request_content = [
+ {
+ "id": 1,
+ "iid": 1,
+ "project_id": 1,
+ "title": "test1",
+ "description": "fixed login page css paddings",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM",
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": None,
+ "closed_at": None,
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
+ "target_branch": "master",
+ "source_branch": "test1",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "admin",
+ "state": "active",
+ "avatar_url": None,
+ "web_url": "https://gitlab.example.com/admin",
+ },
+ "assignee": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "admin",
+ "state": "active",
+ "avatar_url": None,
+ "web_url": "https://gitlab.example.com/admin",
+ },
+ "assignees": [
+ {
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block",
+ }
+ ],
+ "source_project_id": 2,
+ "target_project_id": 3,
+ "labels": ["Community contribution", "Manage"],
+ "work_in_progress": None,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 3,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": "2018-09-22",
+ "start_date": "2018-08-08",
+ "web_url": "https://gitlab.example.com/my-group/my-project/milestones/1",
+ },
+ "merge_when_pipeline_succeeds": None,
+ "merge_status": "can_be_merged",
+ "sha": "8888888888888888888888888888888888888888",
+ "merge_commit_sha": None,
+ "squash_commit_sha": None,
+ "user_notes_count": 1,
+ "discussion_locked": None,
+ "should_remove_source_branch": True,
+ "force_remove_source_branch": False,
+ "allow_collaboration": False,
+ "allow_maintainer_to_push": False,
+ "web_url": "http://gitlab.example.com/my-group/my-project/merge_requests/1",
+ "references": {
+ "short": "!1",
+ "relative": "my-group/my-project!1",
+ "full": "my-group/my-project!1",
+ },
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": None,
+ "human_total_time_spent": None,
+ },
+ "squash": False,
+ "task_completion_status": {"count": 0, "completed_count": 0},
+ }
+ ]
+ mr_ars_content = [
+ {
+ "id": approval_rule_id,
+ "name": approval_rule_name,
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
+ "id": user_ids[0],
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe",
+ },
+ {
+ "id": user_ids[1],
+ "name": "Group Member 1",
+ "username": "group_member_1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/group_member_1",
+ },
+ ],
+ "approvals_required": approvals_required,
+ "source_rule": None,
+ "users": [
+ {
+ "id": 5,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe",
+ }
+ ],
+ "groups": [
+ {
+ "id": 5,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": False,
+ "avatar_url": None,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": False,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": None,
+ "ldap_cn": None,
+ "ldap_access": None,
+ }
+ ],
+ "contains_hidden_groups": False,
+ "overridden": False,
+ }
+ ]
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests",
+ json=merge_request_content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1",
+ json=merge_request_content[0],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules",
+ json=mr_ars_content,
+ content_type="application/json",
+ status=200,
+ )
+
+ new_mr_ars_content = dict(mr_ars_content[0])
+ new_mr_ars_content["name"] = new_approval_rule_name
+ new_mr_ars_content["approvals_required"] = new_approval_rule_approvals_required
+
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules",
+ json=new_mr_ars_content,
+ content_type="application/json",
+ status=200,
+ )
+
+ updated_mr_ars_content = copy.deepcopy(mr_ars_content[0])
+ updated_mr_ars_content["eligible_approvers"] = [
+ mr_ars_content[0]["eligible_approvers"][0]
+ ]
+
+ updated_mr_ars_content[
+ "approvals_required"
+ ] = updated_approval_rule_approvals_required
+
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules/1",
+ json=updated_mr_ars_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_project_approval_manager_update_uses_post(project, resp_snippet):
+ """Ensure the
+ gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager object has
+ _update_uses_post set to True"""
+ approvals = project.approvals
+ assert isinstance(
+ approvals, gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager
+ )
+ assert approvals._update_uses_post is True
+
+
+def test_list_merge_request_approval_rules(project, resp_snippet):
+ approval_rules = project.mergerequests.get(1).approval_rules.list()
+ assert len(approval_rules) == 1
+ assert approval_rules[0].name == approval_rule_name
+ assert approval_rules[0].id == approval_rule_id
+
+
+def test_update_merge_request_approvals_set_approvers(project, resp_snippet):
+ approvals = project.mergerequests.get(1).approvals
+ assert isinstance(
+ approvals,
+ gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager,
+ )
+ assert approvals._update_uses_post is True
+ response = approvals.set_approvers(
+ updated_approval_rule_approvals_required,
+ approver_ids=updated_approval_rule_user_ids,
+ approver_group_ids=group_ids,
+ approval_rule_name=approval_rule_name,
+ )
+
+ assert response.approvals_required == updated_approval_rule_approvals_required
+ assert len(response.eligible_approvers) == len(updated_approval_rule_user_ids)
+ assert response.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0]
+ assert response.name == approval_rule_name
+
+
+def test_create_merge_request_approvals_set_approvers(project, resp_snippet):
+ approvals = project.mergerequests.get(1).approvals
+ assert isinstance(
+ approvals,
+ gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager,
+ )
+ assert approvals._update_uses_post is True
+ response = approvals.set_approvers(
+ new_approval_rule_approvals_required,
+ approver_ids=new_approval_rule_user_ids,
+ approver_group_ids=group_ids,
+ approval_rule_name=new_approval_rule_name,
+ )
+ assert response.approvals_required == new_approval_rule_approvals_required
+ assert len(response.eligible_approvers) == len(new_approval_rule_user_ids)
+ assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0]
+ assert response.name == new_approval_rule_name
+
+
+def test_create_merge_request_approval_rule(project, resp_snippet):
+ approval_rules = project.mergerequests.get(1).approval_rules
+ data = {
+ "name": new_approval_rule_name,
+ "approvals_required": new_approval_rule_approvals_required,
+ "rule_type": "regular",
+ "user_ids": new_approval_rule_user_ids,
+ "group_ids": group_ids,
+ }
+ response = approval_rules.create(data)
+ assert response.approvals_required == new_approval_rule_approvals_required
+ assert len(response.eligible_approvers) == len(new_approval_rule_user_ids)
+ assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0]
+ assert response.name == new_approval_rule_name
+
+
+def test_update_merge_request_approval_rule(project, resp_snippet):
+ approval_rules = project.mergerequests.get(1).approval_rules
+ ar_1 = approval_rules.list()[0]
+ ar_1.user_ids = updated_approval_rule_user_ids
+ ar_1.approvals_required = updated_approval_rule_approvals_required
+ ar_1.save()
+
+ assert ar_1.approvals_required == updated_approval_rule_approvals_required
+ assert len(ar_1.eligible_approvers) == len(updated_approval_rule_user_ids)
+ assert ar_1.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0]
diff --git a/tests/unit/objects/test_project_statistics.py b/tests/unit/objects/test_project_statistics.py
new file mode 100644
index 0000000..50d9a6d
--- /dev/null
+++ b/tests/unit/objects/test_project_statistics.py
@@ -0,0 +1,28 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/project_statistics.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectAdditionalStatistics
+
+
+@pytest.fixture
+def resp_project_statistics():
+ content = {"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}}
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/statistics",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_project_additional_statistics(project, resp_project_statistics):
+ statistics = project.additionalstatistics.get()
+ assert isinstance(statistics, ProjectAdditionalStatistics)
+ assert statistics.fetches["total"] == 50
diff --git a/tests/unit/objects/test_projects.py b/tests/unit/objects/test_projects.py
new file mode 100644
index 0000000..73e119b
--- /dev/null
+++ b/tests/unit/objects/test_projects.py
@@ -0,0 +1,262 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/projects.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import Project
+
+project_content = {"name": "name", "id": 1}
+import_content = {
+ "id": 1,
+ "name": "project",
+ "import_status": "scheduled",
+}
+
+
+@pytest.fixture
+def resp_get_project():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1",
+ json=project_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_list_projects():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects",
+ json=[project_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_import_bitbucket_server():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/import/bitbucket_server",
+ json=import_content,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+def test_get_project(gl, resp_get_project):
+ data = gl.projects.get(1)
+ assert isinstance(data, Project)
+ assert data.name == "name"
+ assert data.id == 1
+
+
+def test_list_projects(gl, resp_list_projects):
+ projects = gl.projects.list()
+ assert isinstance(projects[0], Project)
+ assert projects[0].name == "name"
+
+
+def test_import_bitbucket_server(gl, resp_import_bitbucket_server):
+ res = gl.projects.import_bitbucket_server(
+ bitbucket_server_project="project",
+ bitbucket_server_repo="repo",
+ bitbucket_server_url="url",
+ bitbucket_server_username="username",
+ personal_access_token="token",
+ new_name="new_name",
+ target_namespace="namespace",
+ )
+ assert res["id"] == 1
+ assert res["name"] == "project"
+ assert res["import_status"] == "scheduled"
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_user_projects(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_user_starred_projects(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_project_users(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_create_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_create_user_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_update_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_fork_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_project_forks(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_star_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_unstar_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_project_starrers(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_get_project_languages(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_archive_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_unarchive_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_remove_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_restore_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_upload_file(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_share_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_delete_shared_project_link(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_list_project_hooks(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_get_project_hook(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_create_project_hook(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_update_project_hook(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_delete_project_hook(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_create_forked_from_relationship(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_delete_forked_from_relationship(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_search_projects_by_name(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_project_housekeeping(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_get_project_push_rules(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_create_project_push_rule(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_update_project_push_rule(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_delete_project_push_rule(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_transfer_project(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_project_pull_mirror(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_project_snapshot(gl):
+ pass
+
+
+@pytest.mark.skip(reason="missing test")
+def test_import_github(gl):
+ pass
diff --git a/tests/unit/objects/test_releases.py b/tests/unit/objects/test_releases.py
new file mode 100644
index 0000000..6c38a7c
--- /dev/null
+++ b/tests/unit/objects/test_releases.py
@@ -0,0 +1,131 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/releases/index.html
+https://docs.gitlab.com/ee/api/releases/links.html
+"""
+import re
+
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectReleaseLink
+
+encoded_tag_name = "v1%2E0%2E0"
+link_name = "hello-world"
+link_url = "https://gitlab.example.com/group/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64"
+direct_url = f"https://gitlab.example.com/group/hello/-/releases/{encoded_tag_name}/downloads/hello-world"
+new_link_type = "package"
+link_content = {
+ "id": 2,
+ "name": link_name,
+ "url": link_url,
+ "direct_asset_url": direct_url,
+ "external": False,
+ "link_type": "other",
+}
+
+links_url = re.compile(
+ rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links"
+)
+link_id_url = re.compile(
+ rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links/1"
+)
+
+
+@pytest.fixture
+def resp_list_links():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=links_url,
+ json=[link_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_link():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=link_id_url,
+ json=link_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_create_link():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url=links_url,
+ json=link_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_update_link():
+ updated_content = dict(link_content)
+ updated_content["link_type"] = new_link_type
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.PUT,
+ url=link_id_url,
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_link(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url=link_id_url,
+ json=link_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+def test_list_release_links(release, resp_list_links):
+ links = release.links.list()
+ assert isinstance(links, list)
+ assert isinstance(links[0], ProjectReleaseLink)
+ assert links[0].url == link_url
+
+
+def test_get_release_link(release, resp_get_link):
+ link = release.links.get(1)
+ assert isinstance(link, ProjectReleaseLink)
+ assert link.url == link_url
+
+
+def test_create_release_link(release, resp_create_link):
+ link = release.links.create({"url": link_url, "name": link_name})
+ assert isinstance(link, ProjectReleaseLink)
+ assert link.url == link_url
+
+
+def test_update_release_link(release, resp_update_link):
+ link = release.links.get(1, lazy=True)
+ link.link_type = new_link_type
+ link.save()
+ assert link.link_type == new_link_type
+
+
+def test_delete_release_link(release, resp_delete_link):
+ link = release.links.get(1, lazy=True)
+ link.delete()
diff --git a/tests/unit/objects/test_remote_mirrors.py b/tests/unit/objects/test_remote_mirrors.py
new file mode 100644
index 0000000..1ac35a2
--- /dev/null
+++ b/tests/unit/objects/test_remote_mirrors.py
@@ -0,0 +1,72 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/remote_mirrors.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectRemoteMirror
+
+
+@pytest.fixture
+def resp_remote_mirrors():
+ content = {
+ "enabled": True,
+ "id": 1,
+ "last_error": None,
+ "last_successful_update_at": "2020-01-06T17:32:02.823Z",
+ "last_update_at": "2020-01-06T17:32:02.823Z",
+ "last_update_started_at": "2020-01-06T17:31:55.864Z",
+ "only_protected_branches": True,
+ "update_status": "none",
+ "url": "https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git",
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/remote_mirrors",
+ json=[content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/remote_mirrors",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+
+ updated_content = dict(content)
+ updated_content["update_status"] = "finished"
+
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/remote_mirrors/1",
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_remote_mirrors(project, resp_remote_mirrors):
+ mirrors = project.remote_mirrors.list()
+ assert isinstance(mirrors, list)
+ assert isinstance(mirrors[0], ProjectRemoteMirror)
+ assert mirrors[0].enabled
+
+
+def test_create_project_remote_mirror(project, resp_remote_mirrors):
+ mirror = project.remote_mirrors.create({"url": "https://example.com"})
+ assert isinstance(mirror, ProjectRemoteMirror)
+ assert mirror.update_status == "none"
+
+
+def test_update_project_remote_mirror(project, resp_remote_mirrors):
+ mirror = project.remote_mirrors.create({"url": "https://example.com"})
+ mirror.only_protected_branches = True
+ mirror.save()
+ assert mirror.update_status == "finished"
+ assert mirror.only_protected_branches
diff --git a/tests/unit/objects/test_repositories.py b/tests/unit/objects/test_repositories.py
new file mode 100644
index 0000000..7c4d77d
--- /dev/null
+++ b/tests/unit/objects/test_repositories.py
@@ -0,0 +1,49 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/repositories.html
+https://docs.gitlab.com/ee/api/repository_files.html
+"""
+from urllib.parse import quote
+
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectFile
+
+file_path = "app/models/key.rb"
+ref = "main"
+
+
+@pytest.fixture
+def resp_get_repository_file():
+ file_response = {
+ "file_name": "key.rb",
+ "file_path": file_path,
+ "size": 1476,
+ "encoding": "base64",
+ "content": "IyA9PSBTY2hlbWEgSW5mb3...",
+ "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481",
+ "ref": ref,
+ "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
+ "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
+ "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ }
+
+ # requests also encodes `.`
+ encoded_path = quote(file_path, safe="").replace(".", "%2E")
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=f"http://localhost/api/v4/projects/1/repository/files/{encoded_path}",
+ json=file_response,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_get_repository_file(project, resp_get_repository_file):
+ file = project.files.get(file_path, ref=ref)
+ assert isinstance(file, ProjectFile)
+ assert file.file_path == file_path
diff --git a/tests/unit/objects/test_resource_label_events.py b/tests/unit/objects/test_resource_label_events.py
new file mode 100644
index 0000000..deea8a0
--- /dev/null
+++ b/tests/unit/objects/test_resource_label_events.py
@@ -0,0 +1,105 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/resource_label_events.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import (
+ GroupEpicResourceLabelEvent,
+ ProjectIssueResourceLabelEvent,
+ ProjectMergeRequestResourceLabelEvent,
+)
+
+
+@pytest.fixture()
+def resp_group_epic_request_label_events():
+ epic_content = {"id": 1}
+ events_content = {"id": 1, "resource_type": "Epic"}
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/groups/1/epics",
+ json=[epic_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/groups/1/epics/1/resource_label_events",
+ json=[events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_merge_request_label_events():
+ mr_content = {"iid": 1}
+ events_content = {"id": 1, "resource_type": "MergeRequest"}
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests",
+ json=[mr_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/resource_label_events",
+ json=[events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_project_issue_label_events():
+ issue_content = {"iid": 1}
+ events_content = {"id": 1, "resource_type": "Issue"}
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues",
+ json=[issue_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues/1/resource_label_events",
+ json=[events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_project_issue_label_events(project, resp_project_issue_label_events):
+ issue = project.issues.list()[0]
+ label_events = issue.resourcelabelevents.list()
+ assert isinstance(label_events, list)
+ label_event = label_events[0]
+ assert isinstance(label_event, ProjectIssueResourceLabelEvent)
+ assert label_event.resource_type == "Issue"
+
+
+def test_merge_request_label_events(project, resp_merge_request_label_events):
+ mr = project.mergerequests.list()[0]
+ label_events = mr.resourcelabelevents.list()
+ assert isinstance(label_events, list)
+ label_event = label_events[0]
+ assert isinstance(label_event, ProjectMergeRequestResourceLabelEvent)
+ assert label_event.resource_type == "MergeRequest"
+
+
+def test_group_epic_request_label_events(group, resp_group_epic_request_label_events):
+ epic = group.epics.list()[0]
+ label_events = epic.resourcelabelevents.list()
+ assert isinstance(label_events, list)
+ label_event = label_events[0]
+ assert isinstance(label_event, GroupEpicResourceLabelEvent)
+ assert label_event.resource_type == "Epic"
diff --git a/tests/unit/objects/test_resource_milestone_events.py b/tests/unit/objects/test_resource_milestone_events.py
new file mode 100644
index 0000000..99faeaa
--- /dev/null
+++ b/tests/unit/objects/test_resource_milestone_events.py
@@ -0,0 +1,73 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/resource_milestone_events.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import (
+ ProjectIssueResourceMilestoneEvent,
+ ProjectMergeRequestResourceMilestoneEvent,
+)
+
+
+@pytest.fixture()
+def resp_merge_request_milestone_events():
+ mr_content = {"iid": 1}
+ events_content = {"id": 1, "resource_type": "MergeRequest"}
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests",
+ json=[mr_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/resource_milestone_events",
+ json=[events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_project_issue_milestone_events():
+ issue_content = {"iid": 1}
+ events_content = {"id": 1, "resource_type": "Issue"}
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues",
+ json=[issue_content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues/1/resource_milestone_events",
+ json=[events_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_project_issue_milestone_events(project, resp_project_issue_milestone_events):
+ issue = project.issues.list()[0]
+ milestone_events = issue.resourcemilestoneevents.list()
+ assert isinstance(milestone_events, list)
+ milestone_event = milestone_events[0]
+ assert isinstance(milestone_event, ProjectIssueResourceMilestoneEvent)
+ assert milestone_event.resource_type == "Issue"
+
+
+def test_merge_request_milestone_events(project, resp_merge_request_milestone_events):
+ mr = project.mergerequests.list()[0]
+ milestone_events = mr.resourcemilestoneevents.list()
+ assert isinstance(milestone_events, list)
+ milestone_event = milestone_events[0]
+ assert isinstance(milestone_event, ProjectMergeRequestResourceMilestoneEvent)
+ assert milestone_event.resource_type == "MergeRequest"
diff --git a/tests/unit/objects/test_resource_state_events.py b/tests/unit/objects/test_resource_state_events.py
new file mode 100644
index 0000000..bf18193
--- /dev/null
+++ b/tests/unit/objects/test_resource_state_events.py
@@ -0,0 +1,104 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/resource_state_events.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import (
+ ProjectIssueResourceStateEvent,
+ ProjectMergeRequestResourceStateEvent,
+)
+
+issue_event_content = {"id": 1, "resource_type": "Issue"}
+mr_event_content = {"id": 1, "resource_type": "MergeRequest"}
+
+
+@pytest.fixture()
+def resp_list_project_issue_state_events():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues/1/resource_state_events",
+ json=[issue_event_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_get_project_issue_state_event():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/issues/1/resource_state_events/1",
+ json=issue_event_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_list_merge_request_state_events():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events",
+ json=[mr_event_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture()
+def resp_get_merge_request_state_event():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events/1",
+ json=mr_event_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_issue_state_events(
+ project_issue, resp_list_project_issue_state_events
+):
+ state_events = project_issue.resourcestateevents.list()
+ assert isinstance(state_events, list)
+
+ state_event = state_events[0]
+ assert isinstance(state_event, ProjectIssueResourceStateEvent)
+ assert state_event.resource_type == "Issue"
+
+
+def test_get_project_issue_state_event(
+ project_issue, resp_get_project_issue_state_event
+):
+ state_event = project_issue.resourcestateevents.get(1)
+ assert isinstance(state_event, ProjectIssueResourceStateEvent)
+ assert state_event.resource_type == "Issue"
+
+
+def test_list_merge_request_state_events(
+ project_merge_request, resp_list_merge_request_state_events
+):
+ state_events = project_merge_request.resourcestateevents.list()
+ assert isinstance(state_events, list)
+
+ state_event = state_events[0]
+ assert isinstance(state_event, ProjectMergeRequestResourceStateEvent)
+ assert state_event.resource_type == "MergeRequest"
+
+
+def test_get_merge_request_state_event(
+ project_merge_request, resp_get_merge_request_state_event
+):
+ state_event = project_merge_request.resourcestateevents.get(1)
+ assert isinstance(state_event, ProjectMergeRequestResourceStateEvent)
+ assert state_event.resource_type == "MergeRequest"
diff --git a/tests/unit/objects/test_runners.py b/tests/unit/objects/test_runners.py
new file mode 100644
index 0000000..686eec2
--- /dev/null
+++ b/tests/unit/objects/test_runners.py
@@ -0,0 +1,282 @@
+import re
+
+import pytest
+import responses
+
+import gitlab
+
+runner_detail = {
+ "active": True,
+ "architecture": "amd64",
+ "description": "test-1-20150125",
+ "id": 6,
+ "ip_address": "127.0.0.1",
+ "is_shared": False,
+ "contacted_at": "2016-01-25T16:39:48.066Z",
+ "name": "test-runner",
+ "online": True,
+ "status": "online",
+ "platform": "linux",
+ "projects": [
+ {
+ "id": 1,
+ "name": "GitLab Community Edition",
+ "name_with_namespace": "GitLab.org / GitLab Community Edition",
+ "path": "gitlab-foss",
+ "path_with_namespace": "gitlab-org/gitlab-foss",
+ }
+ ],
+ "revision": "5nj35",
+ "tag_list": ["ruby", "mysql"],
+ "version": "v13.0.0",
+ "access_level": "ref_protected",
+ "maximum_timeout": 3600,
+}
+
+runner_shortinfo = {
+ "active": True,
+ "description": "test-1-20150125",
+ "id": 6,
+ "is_shared": False,
+ "ip_address": "127.0.0.1",
+ "name": "test-name",
+ "online": True,
+ "status": "online",
+}
+
+runner_jobs = [
+ {
+ "id": 6,
+ "ip_address": "127.0.0.1",
+ "status": "running",
+ "stage": "test",
+ "name": "test",
+ "ref": "master",
+ "tag": False,
+ "coverage": "99%",
+ "created_at": "2017-11-16T08:50:29.000Z",
+ "started_at": "2017-11-16T08:51:29.000Z",
+ "finished_at": "2017-11-16T08:53:29.000Z",
+ "duration": 120,
+ "user": {
+ "id": 1,
+ "name": "John Doe2",
+ "username": "user2",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://localhost/user2",
+ "created_at": "2017-11-16T18:38:46.000Z",
+ "bio": None,
+ "location": None,
+ "public_email": "",
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": None,
+ },
+ }
+]
+
+
+@pytest.fixture
+def resp_get_runners_jobs():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/runners/6/jobs",
+ json=runner_jobs,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_runners_list():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=re.compile(r".*?(/runners(/all)?|/(groups|projects)/1/runners)"),
+ json=[runner_shortinfo],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_detail():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?/runners/6")
+ rsps.add(
+ method=responses.GET,
+ url=pattern,
+ json=runner_detail,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.PUT,
+ url=pattern,
+ json=runner_detail,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_register():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?/runners")
+ rsps.add(
+ method=responses.POST,
+ url=pattern,
+ json={"id": "6", "token": "6337ff461c94fd3fa32ba3b1ff4125"},
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_enable():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?(projects|groups)/1/runners")
+ rsps.add(
+ method=responses.POST,
+ url=pattern,
+ json=runner_shortinfo,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_delete():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?/runners/6")
+ rsps.add(
+ method=responses.GET,
+ url=pattern,
+ json=runner_detail,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.DELETE,
+ url=pattern,
+ status=204,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_disable():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?/(groups|projects)/1/runners/6")
+ rsps.add(
+ method=responses.DELETE,
+ url=pattern,
+ status=204,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_runner_verify():
+ with responses.RequestsMock() as rsps:
+ pattern = re.compile(r".*?/runners/verify")
+ rsps.add(
+ method=responses.POST,
+ url=pattern,
+ status=200,
+ )
+ yield rsps
+
+
+def test_owned_runners_list(gl: gitlab.Gitlab, resp_get_runners_list):
+ runners = gl.runners.list()
+ assert runners[0].active is True
+ assert runners[0].id == 6
+ assert runners[0].name == "test-name"
+ assert len(runners) == 1
+
+
+def test_project_runners_list(gl: gitlab.Gitlab, resp_get_runners_list):
+ runners = gl.projects.get(1, lazy=True).runners.list()
+ assert runners[0].active is True
+ assert runners[0].id == 6
+ assert runners[0].name == "test-name"
+ assert len(runners) == 1
+
+
+def test_group_runners_list(gl: gitlab.Gitlab, resp_get_runners_list):
+ runners = gl.groups.get(1, lazy=True).runners.list()
+ assert runners[0].active is True
+ assert runners[0].id == 6
+ assert runners[0].name == "test-name"
+ assert len(runners) == 1
+
+
+def test_all_runners_list(gl: gitlab.Gitlab, resp_get_runners_list):
+ runners = gl.runners.all()
+ assert runners[0].active is True
+ assert runners[0].id == 6
+ assert runners[0].name == "test-name"
+ assert len(runners) == 1
+
+
+def test_create_runner(gl: gitlab.Gitlab, resp_runner_register):
+ runner = gl.runners.create({"token": "token"})
+ assert runner.id == "6"
+ assert runner.token == "6337ff461c94fd3fa32ba3b1ff4125"
+
+
+def test_get_update_runner(gl: gitlab.Gitlab, resp_runner_detail):
+ runner = gl.runners.get(6)
+ assert runner.active is True
+ runner.tag_list.append("new")
+ runner.save()
+
+
+def test_remove_runner(gl: gitlab.Gitlab, resp_runner_delete):
+ runner = gl.runners.get(6)
+ runner.delete()
+ gl.runners.delete(6)
+
+
+def test_disable_project_runner(gl: gitlab.Gitlab, resp_runner_disable):
+ gl.projects.get(1, lazy=True).runners.delete(6)
+
+
+def test_disable_group_runner(gl: gitlab.Gitlab, resp_runner_disable):
+ gl.groups.get(1, lazy=True).runners.delete(6)
+
+
+def test_enable_project_runner(gl: gitlab.Gitlab, resp_runner_enable):
+ runner = gl.projects.get(1, lazy=True).runners.create({"runner_id": 6})
+ assert runner.active is True
+ assert runner.id == 6
+ assert runner.name == "test-name"
+
+
+def test_enable_group_runner(gl: gitlab.Gitlab, resp_runner_enable):
+ runner = gl.groups.get(1, lazy=True).runners.create({"runner_id": 6})
+ assert runner.active is True
+ assert runner.id == 6
+ assert runner.name == "test-name"
+
+
+def test_verify_runner(gl: gitlab.Gitlab, resp_runner_verify):
+ gl.runners.verify("token")
+
+
+def test_runner_jobs(gl: gitlab.Gitlab, resp_get_runners_jobs):
+ jobs = gl.runners.get(6, lazy=True).jobs.list()
+ assert jobs[0].duration == 120
+ assert jobs[0].name == "test"
+ assert jobs[0].user.get("name") == "John Doe2"
+ assert len(jobs) == 1
diff --git a/tests/unit/objects/test_services.py b/tests/unit/objects/test_services.py
new file mode 100644
index 0000000..5b2bcb8
--- /dev/null
+++ b/tests/unit/objects/test_services.py
@@ -0,0 +1,93 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/services.html
+"""
+
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectService
+
+
+@pytest.fixture
+def resp_service():
+ content = {
+ "id": 100152,
+ "title": "Pipelines emails",
+ "slug": "pipelines-email",
+ "created_at": "2019-01-14T08:46:43.637+01:00",
+ "updated_at": "2019-07-01T14:10:36.156+02:00",
+ "active": True,
+ "commit_events": True,
+ "push_events": True,
+ "issues_events": True,
+ "confidential_issues_events": True,
+ "merge_requests_events": True,
+ "tag_push_events": True,
+ "note_events": True,
+ "confidential_note_events": True,
+ "pipeline_events": True,
+ "wiki_page_events": True,
+ "job_events": True,
+ "comment_on_event_enabled": True,
+ "project_id": 1,
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/services",
+ json=[content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/services",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/services/pipelines-email",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ updated_content = dict(content)
+ updated_content["issues_events"] = False
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/services/pipelines-email",
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_active_services(project, resp_service):
+ services = project.services.list()
+ assert isinstance(services, list)
+ assert isinstance(services[0], ProjectService)
+ assert services[0].active
+ assert services[0].push_events
+
+
+def test_list_available_services(project, resp_service):
+ services = project.services.available()
+ assert isinstance(services, list)
+ assert isinstance(services[0], str)
+
+
+def test_get_service(project, resp_service):
+ service = project.services.get("pipelines-email")
+ assert isinstance(service, ProjectService)
+ assert service.push_events is True
+
+
+def test_update_service(project, resp_service):
+ service = project.services.get("pipelines-email")
+ service.issues_events = False
+ service.save()
+ assert service.issues_events is False
diff --git a/tests/unit/objects/test_snippets.py b/tests/unit/objects/test_snippets.py
new file mode 100644
index 0000000..2540fc3
--- /dev/null
+++ b/tests/unit/objects/test_snippets.py
@@ -0,0 +1,89 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/project_snippets.html
+ https://docs.gitlab.com/ee/api/snippets.html (todo)
+"""
+
+import pytest
+import responses
+
+title = "Example Snippet Title"
+visibility = "private"
+new_title = "new-title"
+
+
+@pytest.fixture
+def resp_snippet():
+ content = {
+ "title": title,
+ "description": "More verbose snippet description",
+ "file_name": "example.txt",
+ "content": "source code with multiple lines",
+ "visibility": visibility,
+ }
+
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/snippets",
+ json=[content],
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/snippets/1",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/snippets",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+
+ updated_content = dict(content)
+ updated_content["title"] = new_title
+ updated_content["visibility"] = visibility
+
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/snippets",
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_list_project_snippets(project, resp_snippet):
+ snippets = project.snippets.list()
+ assert len(snippets) == 1
+ assert snippets[0].title == title
+ assert snippets[0].visibility == visibility
+
+
+def test_get_project_snippet(project, resp_snippet):
+ snippet = project.snippets.get(1)
+ assert snippet.title == title
+ assert snippet.visibility == visibility
+
+
+def test_create_update_project_snippets(project, resp_snippet):
+ snippet = project.snippets.create(
+ {
+ "title": title,
+ "file_name": title,
+ "content": title,
+ "visibility": visibility,
+ }
+ )
+ assert snippet.title == title
+ assert snippet.visibility == visibility
+
+ snippet.title = new_title
+ snippet.save()
+ assert snippet.title == new_title
+ assert snippet.visibility == visibility
diff --git a/tests/unit/objects/test_submodules.py b/tests/unit/objects/test_submodules.py
new file mode 100644
index 0000000..69c1cd7
--- /dev/null
+++ b/tests/unit/objects/test_submodules.py
@@ -0,0 +1,46 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/repository_submodules.html
+"""
+import pytest
+import responses
+
+
+@pytest.fixture
+def resp_update_submodule():
+ content = {
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Message",
+ "author_name": "Author",
+ "author_email": "author@example.com",
+ "committer_name": "Author",
+ "committer_email": "author@example.com",
+ "created_at": "2018-09-20T09:26:24.000-07:00",
+ "message": "Message",
+ "parent_ids": ["ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"],
+ "committed_date": "2018-09-20T09:26:24.000-07:00",
+ "authored_date": "2018-09-20T09:26:24.000-07:00",
+ "status": None,
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.PUT,
+ url="http://localhost/api/v4/projects/1/repository/submodules/foo%2Fbar",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_update_submodule(project, resp_update_submodule):
+ ret = project.update_submodule(
+ submodule="foo/bar",
+ branch="master",
+ commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664",
+ commit_message="Message",
+ )
+ assert isinstance(ret, dict)
+ assert ret["message"] == "Message"
+ assert ret["id"] == "ed899a2f4b50b4370feeea94676502b42383c746"
diff --git a/tests/unit/objects/test_todos.py b/tests/unit/objects/test_todos.py
new file mode 100644
index 0000000..058fe33
--- /dev/null
+++ b/tests/unit/objects/test_todos.py
@@ -0,0 +1,62 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/todos.html
+"""
+
+import json
+import os
+
+import pytest
+import responses
+
+from gitlab.v4.objects import Todo
+
+with open(os.path.dirname(__file__) + "/../data/todo.json", "r") as json_file:
+ todo_content = json_file.read()
+ json_content = json.loads(todo_content)
+
+
+@pytest.fixture
+def resp_todo():
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/todos",
+ json=json_content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/todos/102/mark_as_done",
+ json=json_content[0],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_mark_all_as_done():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/todos/mark_as_done",
+ json={},
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+def test_todo(gl, resp_todo):
+ todo = gl.todos.list()[0]
+ assert isinstance(todo, Todo)
+ assert todo.id == 102
+ assert todo.target_type == "MergeRequest"
+ assert todo.target["assignee"]["username"] == "root"
+
+ todo.mark_as_done()
+
+
+def test_todo_mark_all_as_done(gl, resp_mark_all_as_done):
+ gl.todos.mark_all_as_done()
diff --git a/tests/unit/objects/test_users.py b/tests/unit/objects/test_users.py
new file mode 100644
index 0000000..e46a315
--- /dev/null
+++ b/tests/unit/objects/test_users.py
@@ -0,0 +1,217 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/users.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import User, UserMembership, UserStatus
+
+
+@pytest.fixture
+def resp_get_user():
+ content = {
+ "name": "name",
+ "id": 1,
+ "password": "password",
+ "username": "username",
+ "email": "email",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/users/1",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_user_memberships():
+ content = [
+ {
+ "source_id": 1,
+ "source_name": "Project one",
+ "source_type": "Project",
+ "access_level": "20",
+ },
+ {
+ "source_id": 3,
+ "source_name": "Group three",
+ "source_type": "Namespace",
+ "access_level": "20",
+ },
+ ]
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/users/1/memberships",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_activate():
+ with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/users/1/activate",
+ json={},
+ content_type="application/json",
+ status=201,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/users/1/deactivate",
+ json={},
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_user_status():
+ content = {
+ "message": "test",
+ "message_html": "<h1>Message</h1>",
+ "emoji": "thumbsup",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/users/1/status",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_user_identity(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url="http://localhost/api/v4/users/1/identities/test_provider",
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_follow_unfollow():
+ user = {
+ "id": 1,
+ "username": "john_smith",
+ "name": "John Smith",
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
+ "web_url": "http://localhost:3000/john_smith",
+ }
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/users/1/follow",
+ json=user,
+ content_type="application/json",
+ status=201,
+ )
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/users/1/unfollow",
+ json=user,
+ content_type="application/json",
+ status=201,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_followers_following():
+ content = [
+ {
+ "id": 2,
+ "name": "Lennie Donnelly",
+ "username": "evette.kilback",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon",
+ "web_url": "http://127.0.0.1:3000/evette.kilback",
+ },
+ {
+ "id": 4,
+ "name": "Serena Bradtke",
+ "username": "cammy",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon",
+ "web_url": "http://127.0.0.1:3000/cammy",
+ },
+ ]
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/users/1/followers",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/users/1/following",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_get_user(gl, resp_get_user):
+ user = gl.users.get(1)
+ assert isinstance(user, User)
+ assert user.name == "name"
+ assert user.id == 1
+
+
+def test_user_memberships(user, resp_get_user_memberships):
+ memberships = user.memberships.list()
+ assert isinstance(memberships[0], UserMembership)
+ assert memberships[0].source_type == "Project"
+
+
+def test_user_status(user, resp_get_user_status):
+ status = user.status.get()
+ assert isinstance(status, UserStatus)
+ assert status.message == "test"
+ assert status.emoji == "thumbsup"
+
+
+def test_user_activate_deactivate(user, resp_activate):
+ user.activate()
+ user.deactivate()
+
+
+def test_delete_user_identity(user, resp_delete_user_identity):
+ user.identityproviders.delete("test_provider")
+
+
+def test_user_follow_unfollow(user, resp_follow_unfollow):
+ user.follow()
+ user.unfollow()
+
+
+def test_list_followers(user, resp_followers_following):
+ followers = user.followers_users.list()
+ followings = user.following_users.list()
+ assert isinstance(followers[0], User)
+ assert followers[0].id == 2
+ assert isinstance(followings[0], User)
+ assert followings[1].id == 4
diff --git a/tests/unit/objects/test_variables.py b/tests/unit/objects/test_variables.py
new file mode 100644
index 0000000..fae37a8
--- /dev/null
+++ b/tests/unit/objects/test_variables.py
@@ -0,0 +1,192 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/instance_level_ci_variables.html
+https://docs.gitlab.com/ee/api/project_level_variables.html
+https://docs.gitlab.com/ee/api/group_level_variables.html
+"""
+
+import re
+
+import pytest
+import responses
+
+from gitlab.v4.objects import GroupVariable, ProjectVariable, Variable
+
+key = "TEST_VARIABLE_1"
+value = "TEST_1"
+new_value = "TEST_2"
+
+variable_content = {
+ "key": key,
+ "variable_type": "env_var",
+ "value": value,
+ "protected": False,
+ "masked": True,
+}
+variables_url = re.compile(
+ r"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables"
+)
+variables_key_url = re.compile(
+ rf"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables/{key}"
+)
+
+
+@pytest.fixture
+def resp_list_variables():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=variables_url,
+ json=[variable_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_variable():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=variables_key_url,
+ json=variable_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_create_variable():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url=variables_url,
+ json=variable_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_update_variable():
+ updated_content = dict(variable_content)
+ updated_content["value"] = new_value
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.PUT,
+ url=variables_key_url,
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_variable(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url=variables_key_url,
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+def test_list_instance_variables(gl, resp_list_variables):
+ variables = gl.variables.list()
+ assert isinstance(variables, list)
+ assert isinstance(variables[0], Variable)
+ assert variables[0].value == value
+
+
+def test_get_instance_variable(gl, resp_get_variable):
+ variable = gl.variables.get(key)
+ assert isinstance(variable, Variable)
+ assert variable.value == value
+
+
+def test_create_instance_variable(gl, resp_create_variable):
+ variable = gl.variables.create({"key": key, "value": value})
+ assert isinstance(variable, Variable)
+ assert variable.value == value
+
+
+def test_update_instance_variable(gl, resp_update_variable):
+ variable = gl.variables.get(key, lazy=True)
+ variable.value = new_value
+ variable.save()
+ assert variable.value == new_value
+
+
+def test_delete_instance_variable(gl, resp_delete_variable):
+ variable = gl.variables.get(key, lazy=True)
+ variable.delete()
+
+
+def test_list_project_variables(project, resp_list_variables):
+ variables = project.variables.list()
+ assert isinstance(variables, list)
+ assert isinstance(variables[0], ProjectVariable)
+ assert variables[0].value == value
+
+
+def test_get_project_variable(project, resp_get_variable):
+ variable = project.variables.get(key)
+ assert isinstance(variable, ProjectVariable)
+ assert variable.value == value
+
+
+def test_create_project_variable(project, resp_create_variable):
+ variable = project.variables.create({"key": key, "value": value})
+ assert isinstance(variable, ProjectVariable)
+ assert variable.value == value
+
+
+def test_update_project_variable(project, resp_update_variable):
+ variable = project.variables.get(key, lazy=True)
+ variable.value = new_value
+ variable.save()
+ assert variable.value == new_value
+
+
+def test_delete_project_variable(project, resp_delete_variable):
+ variable = project.variables.get(key, lazy=True)
+ variable.delete()
+
+
+def test_list_group_variables(group, resp_list_variables):
+ variables = group.variables.list()
+ assert isinstance(variables, list)
+ assert isinstance(variables[0], GroupVariable)
+ assert variables[0].value == value
+
+
+def test_get_group_variable(group, resp_get_variable):
+ variable = group.variables.get(key)
+ assert isinstance(variable, GroupVariable)
+ assert variable.value == value
+
+
+def test_create_group_variable(group, resp_create_variable):
+ variable = group.variables.create({"key": key, "value": value})
+ assert isinstance(variable, GroupVariable)
+ assert variable.value == value
+
+
+def test_update_group_variable(group, resp_update_variable):
+ variable = group.variables.get(key, lazy=True)
+ variable.value = new_value
+ variable.save()
+ assert variable.value == new_value
+
+
+def test_delete_group_variable(group, resp_delete_variable):
+ variable = group.variables.get(key, lazy=True)
+ variable.delete()
diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py
new file mode 100644
index 0000000..b3a58fc
--- /dev/null
+++ b/tests/unit/test_base.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import pickle
+
+import pytest
+
+from gitlab import base
+
+
+class FakeGitlab(object):
+ pass
+
+
+class FakeObject(base.RESTObject):
+ pass
+
+
+class FakeManager(base.RESTManager):
+ _obj_cls = FakeObject
+ _path = "/tests"
+
+
+@pytest.fixture
+def fake_gitlab():
+ return FakeGitlab()
+
+
+@pytest.fixture
+def fake_manager(fake_gitlab):
+ return FakeManager(fake_gitlab)
+
+
+class TestRESTManager:
+ def test_computed_path_simple(self):
+ class MGR(base.RESTManager):
+ _path = "/tests"
+ _obj_cls = object
+
+ mgr = MGR(FakeGitlab())
+ assert mgr._computed_path == "/tests"
+
+ def test_computed_path_with_parent(self):
+ class MGR(base.RESTManager):
+ _path = "/tests/%(test_id)s/cases"
+ _obj_cls = object
+ _from_parent_attrs = {"test_id": "id"}
+
+ class Parent(object):
+ id = 42
+
+ mgr = MGR(FakeGitlab(), parent=Parent())
+ assert mgr._computed_path == "/tests/42/cases"
+
+ def test_path_property(self):
+ class MGR(base.RESTManager):
+ _path = "/tests"
+ _obj_cls = object
+
+ mgr = MGR(FakeGitlab())
+ assert mgr.path == "/tests"
+
+
+class TestRESTObject:
+ def test_instantiate(self, fake_gitlab, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+
+ assert {"foo": "bar"} == obj._attrs
+ assert {} == obj._updated_attrs
+ assert obj._create_managers() is None
+ assert fake_manager == obj.manager
+ assert fake_gitlab == obj.manager.gitlab
+
+ def test_picklability(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+ original_obj_module = obj._module
+ pickled = pickle.dumps(obj)
+ unpickled = pickle.loads(pickled)
+ assert isinstance(unpickled, FakeObject)
+ assert hasattr(unpickled, "_module")
+ assert unpickled._module == original_obj_module
+ pickle.dumps(unpickled)
+
+ def test_attrs(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+
+ assert "bar" == obj.foo
+ with pytest.raises(AttributeError):
+ getattr(obj, "bar")
+
+ obj.bar = "baz"
+ assert "baz" == obj.bar
+ assert {"foo": "bar"} == obj._attrs
+ assert {"bar": "baz"} == obj._updated_attrs
+
+ def test_get_id(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+ obj.id = 42
+ assert 42 == obj.get_id()
+
+ obj.id = None
+ assert obj.get_id() is None
+
+ def test_custom_id_attr(self, fake_manager):
+ class OtherFakeObject(FakeObject):
+ _id_attr = "foo"
+
+ obj = OtherFakeObject(fake_manager, {"foo": "bar"})
+ assert "bar" == obj.get_id()
+
+ def test_update_attrs(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "bar"})
+ obj.bar = "baz"
+ obj._update_attrs({"foo": "foo", "bar": "bar"})
+ assert {"foo": "foo", "bar": "bar"} == obj._attrs
+ assert {} == obj._updated_attrs
+
+ def test_update_attrs_deleted(self, fake_manager):
+ obj = FakeObject(fake_manager, {"foo": "foo", "bar": "bar"})
+ obj.bar = "baz"
+ obj._update_attrs({"foo": "foo"})
+ assert {"foo": "foo"} == obj._attrs
+ assert {} == obj._updated_attrs
+
+ def test_dir_unique(self, fake_manager):
+ obj = FakeObject(fake_manager, {"manager": "foo"})
+ assert len(dir(obj)) == len(set(dir(obj)))
+
+ def test_create_managers(self, fake_gitlab, fake_manager):
+ class ObjectWithManager(FakeObject):
+ _managers = (("fakes", "FakeManager"),)
+
+ obj = ObjectWithManager(fake_manager, {"foo": "bar"})
+ obj.id = 42
+ assert isinstance(obj.fakes, FakeManager)
+ assert obj.fakes.gitlab == fake_gitlab
+ assert obj.fakes._parent == obj
+
+ def test_equality(self, fake_manager):
+ obj1 = FakeObject(fake_manager, {"id": "foo"})
+ obj2 = FakeObject(fake_manager, {"id": "foo", "other_attr": "bar"})
+ assert obj1 == obj2
+
+ def test_equality_custom_id(self, fake_manager):
+ class OtherFakeObject(FakeObject):
+ _id_attr = "foo"
+
+ obj1 = OtherFakeObject(fake_manager, {"foo": "bar"})
+ obj2 = OtherFakeObject(fake_manager, {"foo": "bar", "other_attr": "baz"})
+ assert obj1 == obj2
+
+ def test_inequality(self, fake_manager):
+ obj1 = FakeObject(fake_manager, {"id": "foo"})
+ obj2 = FakeObject(fake_manager, {"id": "bar"})
+ assert obj1 != obj2
+
+ def test_inequality_no_id(self, fake_manager):
+ obj1 = FakeObject(fake_manager, {"attr1": "foo"})
+ obj2 = FakeObject(fake_manager, {"attr1": "bar"})
+ assert obj1 != obj2
diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py
new file mode 100644
index 0000000..a9ca958
--- /dev/null
+++ b/tests/unit/test_cli.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016-2017 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import io
+import os
+import tempfile
+from contextlib import redirect_stderr # noqa: H302
+
+import pytest
+
+from gitlab import cli
+
+
+@pytest.mark.parametrize(
+ "what,expected_class",
+ [
+ ("class", "Class"),
+ ("test-class", "TestClass"),
+ ("test-longer-class", "TestLongerClass"),
+ ("current-user-gpg-key", "CurrentUserGPGKey"),
+ ("user-gpg-key", "UserGPGKey"),
+ ("ldap-group", "LDAPGroup"),
+ ],
+)
+def test_what_to_cls(what, expected_class):
+ def _namespace():
+ pass
+
+ ExpectedClass = type(expected_class, (), {})
+ _namespace.__dict__[expected_class] = ExpectedClass
+
+ assert cli.what_to_cls(what, _namespace) == ExpectedClass
+
+
+@pytest.mark.parametrize(
+ "class_name,expected_what",
+ [
+ ("Class", "class"),
+ ("TestClass", "test-class"),
+ ("TestUPPERCASEClass", "test-uppercase-class"),
+ ("UPPERCASETestClass", "uppercase-test-class"),
+ ("CurrentUserGPGKey", "current-user-gpg-key"),
+ ("UserGPGKey", "user-gpg-key"),
+ ("LDAPGroup", "ldap-group"),
+ ],
+)
+def test_cls_to_what(class_name, expected_what):
+ TestClass = type(class_name, (), {})
+
+ assert cli.cls_to_what(TestClass) == expected_what
+
+
+def test_die():
+ fl = io.StringIO()
+ with redirect_stderr(fl):
+ with pytest.raises(SystemExit) as test:
+ cli.die("foobar")
+ assert fl.getvalue() == "foobar\n"
+ assert test.value.code == 1
+
+
+def test_parse_value():
+ ret = cli._parse_value("foobar")
+ assert ret == "foobar"
+
+ ret = cli._parse_value(True)
+ assert ret is True
+
+ ret = cli._parse_value(1)
+ assert ret == 1
+
+ ret = cli._parse_value(None)
+ assert ret is None
+
+ fd, temp_path = tempfile.mkstemp()
+ os.write(fd, b"content")
+ os.close(fd)
+ ret = cli._parse_value("@%s" % temp_path)
+ assert ret == "content"
+ os.unlink(temp_path)
+
+ fl = io.StringIO()
+ with redirect_stderr(fl):
+ with pytest.raises(SystemExit) as exc:
+ cli._parse_value("@/thisfileprobablydoesntexist")
+ assert (
+ fl.getvalue() == "[Errno 2] No such file or directory:"
+ " '/thisfileprobablydoesntexist'\n"
+ )
+ assert exc.value.code == 1
+
+
+def test_base_parser():
+ parser = cli._get_base_parser()
+ args = parser.parse_args(["-v", "-g", "gl_id", "-c", "foo.cfg", "-c", "bar.cfg"])
+ assert args.verbose
+ assert args.gitlab == "gl_id"
+ assert args.config_file == ["foo.cfg", "bar.cfg"]
+
+
+def test_v4_parse_args():
+ parser = cli._get_parser()
+ args = parser.parse_args(["project", "list"])
+ assert args.what == "project"
+ assert args.whaction == "list"
+
+
+def test_v4_parser():
+ parser = cli._get_parser()
+ subparsers = next(
+ action
+ for action in parser._actions
+ if isinstance(action, argparse._SubParsersAction)
+ )
+ assert subparsers is not None
+ assert "project" in subparsers.choices
+
+ user_subparsers = next(
+ action
+ for action in subparsers.choices["project"]._actions
+ if isinstance(action, argparse._SubParsersAction)
+ )
+ assert user_subparsers is not None
+ assert "list" in user_subparsers.choices
+ assert "get" in user_subparsers.choices
+ assert "delete" in user_subparsers.choices
+ assert "update" in user_subparsers.choices
+ assert "create" in user_subparsers.choices
+ assert "archive" in user_subparsers.choices
+ assert "unarchive" in user_subparsers.choices
+
+ actions = user_subparsers.choices["create"]._option_string_actions
+ assert not actions["--description"].required
+
+ user_subparsers = next(
+ action
+ for action in subparsers.choices["group"]._actions
+ if isinstance(action, argparse._SubParsersAction)
+ )
+ actions = user_subparsers.choices["create"]._option_string_actions
+ assert actions["--name"].required
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
new file mode 100644
index 0000000..cd61b8d
--- /dev/null
+++ b/tests/unit/test_config.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016-2017 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import io
+import os
+from textwrap import dedent
+
+import mock
+import pytest
+
+from gitlab import config, USER_AGENT
+
+custom_user_agent = "my-package/1.0.0"
+
+valid_config = u"""[global]
+default = one
+ssl_verify = true
+timeout = 2
+
+[one]
+url = http://one.url
+private_token = ABCDEF
+
+[two]
+url = https://two.url
+private_token = GHIJKL
+ssl_verify = false
+timeout = 10
+
+[three]
+url = https://three.url
+private_token = MNOPQR
+ssl_verify = /path/to/CA/bundle.crt
+per_page = 50
+
+[four]
+url = https://four.url
+oauth_token = STUV
+"""
+
+custom_user_agent_config = """[global]
+default = one
+user_agent = {}
+
+[one]
+url = http://one.url
+private_token = ABCDEF
+""".format(
+ custom_user_agent
+)
+
+no_default_config = u"""[global]
+[there]
+url = http://there.url
+private_token = ABCDEF
+"""
+
+missing_attr_config = u"""[global]
+[one]
+url = http://one.url
+
+[two]
+private_token = ABCDEF
+
+[three]
+meh = hem
+
+[four]
+url = http://four.url
+private_token = ABCDEF
+per_page = 200
+"""
+
+
+@mock.patch.dict(os.environ, {"PYTHON_GITLAB_CFG": "/some/path"})
+def test_env_config_present():
+ assert ["/some/path"] == config._env_config()
+
+
+@mock.patch.dict(os.environ, {}, clear=True)
+def test_env_config_missing():
+ assert [] == config._env_config()
+
+
+@mock.patch("os.path.exists")
+def test_missing_config(path_exists):
+ path_exists.return_value = False
+ with pytest.raises(config.GitlabConfigMissingError):
+ config.GitlabConfigParser("test")
+
+
+@mock.patch("os.path.exists")
+@mock.patch("builtins.open")
+def test_invalid_id(m_open, path_exists):
+ fd = io.StringIO(no_default_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ path_exists.return_value = True
+ config.GitlabConfigParser("there")
+ with pytest.raises(config.GitlabIDError):
+ config.GitlabConfigParser()
+
+ fd = io.StringIO(valid_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ with pytest.raises(config.GitlabDataError):
+ config.GitlabConfigParser(gitlab_id="not_there")
+
+
+@mock.patch("os.path.exists")
+@mock.patch("builtins.open")
+def test_invalid_data(m_open, path_exists):
+ fd = io.StringIO(missing_attr_config)
+ fd.close = mock.Mock(return_value=None, side_effect=lambda: fd.seek(0))
+ m_open.return_value = fd
+ path_exists.return_value = True
+
+ config.GitlabConfigParser("one")
+ config.GitlabConfigParser("one")
+ with pytest.raises(config.GitlabDataError):
+ config.GitlabConfigParser(gitlab_id="two")
+ with pytest.raises(config.GitlabDataError):
+ config.GitlabConfigParser(gitlab_id="three")
+ with pytest.raises(config.GitlabDataError) as emgr:
+ config.GitlabConfigParser("four")
+ assert "Unsupported per_page number: 200" == emgr.value.args[0]
+
+
+@mock.patch("os.path.exists")
+@mock.patch("builtins.open")
+def test_valid_data(m_open, path_exists):
+ fd = io.StringIO(valid_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ path_exists.return_value = True
+
+ cp = config.GitlabConfigParser()
+ assert "one" == cp.gitlab_id
+ assert "http://one.url" == cp.url
+ assert "ABCDEF" == cp.private_token
+ assert cp.oauth_token is None
+ assert 2 == cp.timeout
+ assert cp.ssl_verify is True
+ assert cp.per_page is None
+
+ fd = io.StringIO(valid_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ cp = config.GitlabConfigParser(gitlab_id="two")
+ assert "two" == cp.gitlab_id
+ assert "https://two.url" == cp.url
+ assert "GHIJKL" == cp.private_token
+ assert cp.oauth_token is None
+ assert 10 == cp.timeout
+ assert cp.ssl_verify is False
+
+ fd = io.StringIO(valid_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ cp = config.GitlabConfigParser(gitlab_id="three")
+ assert "three" == cp.gitlab_id
+ assert "https://three.url" == cp.url
+ assert "MNOPQR" == cp.private_token
+ assert cp.oauth_token is None
+ assert 2 == cp.timeout
+ assert "/path/to/CA/bundle.crt" == cp.ssl_verify
+ assert 50 == cp.per_page
+
+ fd = io.StringIO(valid_config)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ cp = config.GitlabConfigParser(gitlab_id="four")
+ assert "four" == cp.gitlab_id
+ assert "https://four.url" == cp.url
+ assert cp.private_token is None
+ assert "STUV" == cp.oauth_token
+ assert 2 == cp.timeout
+ assert cp.ssl_verify is True
+
+
+@mock.patch("os.path.exists")
+@mock.patch("builtins.open")
+def test_data_from_helper(m_open, path_exists, tmp_path):
+ helper = tmp_path / "helper.sh"
+ helper.write_text(
+ dedent(
+ """\
+ #!/bin/sh
+ echo "secret"
+ """
+ )
+ )
+ helper.chmod(0o755)
+
+ fd = io.StringIO(
+ dedent(
+ """\
+ [global]
+ default = helper
+
+ [helper]
+ url = https://helper.url
+ oauth_token = helper: %s
+ """
+ )
+ % helper
+ )
+
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+ cp = config.GitlabConfigParser(gitlab_id="helper")
+ assert "helper" == cp.gitlab_id
+ assert "https://helper.url" == cp.url
+ assert cp.private_token is None
+ assert "secret" == cp.oauth_token
+
+
+@mock.patch("os.path.exists")
+@mock.patch("builtins.open")
+@pytest.mark.parametrize(
+ "config_string,expected_agent",
+ [
+ (valid_config, USER_AGENT),
+ (custom_user_agent_config, custom_user_agent),
+ ],
+)
+def test_config_user_agent(m_open, path_exists, config_string, expected_agent):
+ fd = io.StringIO(config_string)
+ fd.close = mock.Mock(return_value=None)
+ m_open.return_value = fd
+
+ cp = config.GitlabConfigParser()
+ assert cp.user_agent == expected_agent
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
new file mode 100644
index 0000000..57b394b
--- /dev/null
+++ b/tests/unit/test_exceptions.py
@@ -0,0 +1,18 @@
+import pytest
+
+from gitlab import exceptions
+
+
+def test_error_raises_from_http_error():
+ """Methods decorated with @on_http_error should raise from GitlabHttpError."""
+
+ class TestError(Exception):
+ pass
+
+ @exceptions.on_http_error(TestError)
+ def raise_error_from_http_error():
+ raise exceptions.GitlabHttpError
+
+ with pytest.raises(TestError) as context:
+ raise_error_from_http_error()
+ assert isinstance(context.value.__cause__, exceptions.GitlabHttpError)
diff --git a/tests/unit/test_gitlab.py b/tests/unit/test_gitlab.py
new file mode 100644
index 0000000..acb8752
--- /dev/null
+++ b/tests/unit/test_gitlab.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 Mika Mäenpää <mika.j.maenpaa@tut.fi>,
+# Tampere University of Technology
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or`
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import pickle
+
+import pytest
+from httmock import HTTMock, response, urlmatch, with_httmock # noqa
+
+from gitlab import Gitlab, GitlabList, USER_AGENT
+from gitlab.v4.objects import CurrentUser
+
+username = "username"
+user_id = 1
+
+
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/user", method="get")
+def resp_get_user(url, request):
+ headers = {"content-type": "application/json"}
+ content = '{{"id": {0:d}, "username": "{1:s}"}}'.format(user_id, username).encode(
+ "utf-8"
+ )
+ return response(200, content, headers, None, 5, request)
+
+
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get")
+def resp_page_1(url, request):
+ headers = {
+ "content-type": "application/json",
+ "X-Page": 1,
+ "X-Next-Page": 2,
+ "X-Per-Page": 1,
+ "X-Total-Pages": 2,
+ "X-Total": 2,
+ "Link": ("<http://localhost/api/v4/tests?per_page=1&page=2>;" ' rel="next"'),
+ }
+ content = '[{"a": "b"}]'
+ return response(200, content, headers, None, 5, request)
+
+
+@urlmatch(
+ scheme="http",
+ netloc="localhost",
+ path="/api/v4/tests",
+ method="get",
+ query=r".*page=2",
+)
+def resp_page_2(url, request):
+ headers = {
+ "content-type": "application/json",
+ "X-Page": 2,
+ "X-Next-Page": 2,
+ "X-Per-Page": 1,
+ "X-Total-Pages": 2,
+ "X-Total": 2,
+ }
+ content = '[{"c": "d"}]'
+ return response(200, content, headers, None, 5, request)
+
+
+def test_gitlab_build_list(gl):
+ with HTTMock(resp_page_1):
+ obj = gl.http_list("/tests", as_list=False)
+ assert len(obj) == 2
+ assert obj._next_url == "http://localhost/api/v4/tests?per_page=1&page=2"
+ assert obj.current_page == 1
+ assert obj.prev_page is None
+ assert obj.next_page == 2
+ assert obj.per_page == 1
+ assert obj.total_pages == 2
+ assert obj.total == 2
+
+ with HTTMock(resp_page_2):
+ test_list = list(obj)
+ assert len(test_list) == 2
+ assert test_list[0]["a"] == "b"
+ assert test_list[1]["c"] == "d"
+
+
+@with_httmock(resp_page_1, resp_page_2)
+def test_gitlab_all_omitted_when_as_list(gl):
+ result = gl.http_list("/tests", as_list=False, all=True)
+ assert isinstance(result, GitlabList)
+
+
+def test_gitlab_strip_base_url(gl_trailing):
+ assert gl_trailing.url == "http://localhost"
+
+
+def test_gitlab_strip_api_url(gl_trailing):
+ assert gl_trailing.api_url == "http://localhost/api/v4"
+
+
+def test_gitlab_build_url(gl_trailing):
+ r = gl_trailing._build_url("/projects")
+ assert r == "http://localhost/api/v4/projects"
+
+
+def test_gitlab_pickability(gl):
+ original_gl_objects = gl._objects
+ pickled = pickle.dumps(gl)
+ unpickled = pickle.loads(pickled)
+ assert isinstance(unpickled, Gitlab)
+ assert hasattr(unpickled, "_objects")
+ assert unpickled._objects == original_gl_objects
+
+
+@with_httmock(resp_get_user)
+def test_gitlab_token_auth(gl, callback=None):
+ gl.auth()
+ assert gl.user.username == username
+ assert gl.user.id == user_id
+ assert isinstance(gl.user, CurrentUser)
+
+
+def test_gitlab_from_config(default_config):
+ config_path = default_config
+ Gitlab.from_config("one", [config_path])
+
+
+def test_gitlab_subclass_from_config(default_config):
+ class MyGitlab(Gitlab):
+ pass
+
+ config_path = default_config
+ gl = MyGitlab.from_config("one", [config_path])
+ assert isinstance(gl, MyGitlab)
+
+
+@pytest.mark.parametrize(
+ "kwargs,expected_agent",
+ [
+ ({}, USER_AGENT),
+ ({"user_agent": "my-package/1.0.0"}, "my-package/1.0.0"),
+ ],
+)
+def test_gitlab_user_agent(kwargs, expected_agent):
+ gl = Gitlab("http://localhost", **kwargs)
+ assert gl.headers["User-Agent"] == expected_agent
diff --git a/tests/unit/test_gitlab_auth.py b/tests/unit/test_gitlab_auth.py
new file mode 100644
index 0000000..314fbed
--- /dev/null
+++ b/tests/unit/test_gitlab_auth.py
@@ -0,0 +1,85 @@
+import pytest
+import requests
+
+from gitlab import Gitlab
+
+
+def test_invalid_auth_args():
+ with pytest.raises(ValueError):
+ Gitlab(
+ "http://localhost",
+ api_version="4",
+ private_token="private_token",
+ oauth_token="bearer",
+ )
+ with pytest.raises(ValueError):
+ Gitlab(
+ "http://localhost",
+ api_version="4",
+ oauth_token="bearer",
+ http_username="foo",
+ http_password="bar",
+ )
+ with pytest.raises(ValueError):
+ Gitlab(
+ "http://localhost",
+ api_version="4",
+ private_token="private_token",
+ http_password="bar",
+ )
+ with pytest.raises(ValueError):
+ Gitlab(
+ "http://localhost",
+ api_version="4",
+ private_token="private_token",
+ http_username="foo",
+ )
+
+
+def test_private_token_auth():
+ gl = Gitlab("http://localhost", private_token="private_token", api_version="4")
+ assert gl.private_token == "private_token"
+ assert gl.oauth_token is None
+ assert gl.job_token is None
+ assert gl._http_auth is None
+ assert "Authorization" not in gl.headers
+ assert gl.headers["PRIVATE-TOKEN"] == "private_token"
+ assert "JOB-TOKEN" not in gl.headers
+
+
+def test_oauth_token_auth():
+ gl = Gitlab("http://localhost", oauth_token="oauth_token", api_version="4")
+ assert gl.private_token is None
+ assert gl.oauth_token == "oauth_token"
+ assert gl.job_token is None
+ assert gl._http_auth is None
+ assert gl.headers["Authorization"] == "Bearer oauth_token"
+ assert "PRIVATE-TOKEN" not in gl.headers
+ assert "JOB-TOKEN" not in gl.headers
+
+
+def test_job_token_auth():
+ gl = Gitlab("http://localhost", job_token="CI_JOB_TOKEN", api_version="4")
+ assert gl.private_token is None
+ assert gl.oauth_token is None
+ assert gl.job_token == "CI_JOB_TOKEN"
+ assert gl._http_auth is None
+ assert "Authorization" not in gl.headers
+ assert "PRIVATE-TOKEN" not in gl.headers
+ assert gl.headers["JOB-TOKEN"] == "CI_JOB_TOKEN"
+
+
+def test_http_auth():
+ gl = Gitlab(
+ "http://localhost",
+ private_token="private_token",
+ http_username="foo",
+ http_password="bar",
+ api_version="4",
+ )
+ assert gl.private_token == "private_token"
+ assert gl.oauth_token is None
+ assert gl.job_token is None
+ assert isinstance(gl._http_auth, requests.auth.HTTPBasicAuth)
+ assert gl.headers["PRIVATE-TOKEN"] == "private_token"
+ assert "Authorization" not in gl.headers
diff --git a/tests/unit/test_gitlab_http_methods.py b/tests/unit/test_gitlab_http_methods.py
new file mode 100644
index 0000000..f1bc9cd
--- /dev/null
+++ b/tests/unit/test_gitlab_http_methods.py
@@ -0,0 +1,234 @@
+import pytest
+import requests
+from httmock import HTTMock, response, urlmatch
+
+from gitlab import GitlabHttpError, GitlabList, GitlabParsingError
+
+
+def test_build_url(gl):
+ r = gl._build_url("http://localhost/api/v4")
+ assert r == "http://localhost/api/v4"
+ r = gl._build_url("https://localhost/api/v4")
+ assert r == "https://localhost/api/v4"
+ r = gl._build_url("/projects")
+ assert r == "http://localhost/api/v4/projects"
+
+
+def test_http_request(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '[{"name": "project1"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ http_r = gl.http_request("get", "/projects")
+ http_r.json()
+ assert http_r.status_code == 200
+
+
+def test_http_request_404(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {"Here is wh it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_request("get", "/not_there")
+
+
+def test_get_request(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_get("/projects")
+ assert isinstance(result, dict)
+ assert result["name"] == "project1"
+
+
+def test_get_request_raw(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/octet-stream"}
+ content = "content"
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_get("/projects")
+ assert result.content.decode("utf-8") == "content"
+
+
+def test_get_request_404(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {"Here is wh it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_get("/not_there")
+
+
+def test_get_request_invalid_data(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabParsingError):
+ gl.http_get("/projects")
+
+
+def test_list_request(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json", "X-Total": 1}
+ content = '[{"name": "project1"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_list("/projects", as_list=True)
+ assert isinstance(result, list)
+ assert len(result) == 1
+
+ with HTTMock(resp_cont):
+ result = gl.http_list("/projects", as_list=False)
+ assert isinstance(result, GitlabList)
+ assert len(result) == 1
+
+ with HTTMock(resp_cont):
+ result = gl.http_list("/projects", all=True)
+ assert isinstance(result, list)
+ assert len(result) == 1
+
+
+def test_list_request_404(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {"Here is why it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_list("/not_there")
+
+
+def test_list_request_invalid_data(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabParsingError):
+ gl.http_list("/projects")
+
+
+def test_post_request(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_post("/projects")
+ assert isinstance(result, dict)
+ assert result["name"] == "project1"
+
+
+def test_post_request_404(gl):
+ @urlmatch(
+ scheme="http", netloc="localhost", path="/api/v4/not_there", method="post"
+ )
+ def resp_cont(url, request):
+ content = {"Here is wh it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_post("/not_there")
+
+
+def test_post_request_invalid_data(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabParsingError):
+ gl.http_post("/projects")
+
+
+def test_put_request(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_put("/projects")
+ assert isinstance(result, dict)
+ assert result["name"] == "project1"
+
+
+def test_put_request_404(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="put")
+ def resp_cont(url, request):
+ content = {"Here is wh it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_put("/not_there")
+
+
+def test_put_request_invalid_data(gl):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put")
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabParsingError):
+ gl.http_put("/projects")
+
+
+def test_delete_request(gl):
+ @urlmatch(
+ scheme="http", netloc="localhost", path="/api/v4/projects", method="delete"
+ )
+ def resp_cont(url, request):
+ headers = {"content-type": "application/json"}
+ content = "true"
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = gl.http_delete("/projects")
+ assert isinstance(result, requests.Response)
+ assert result.json() is True
+
+
+def test_delete_request_404(gl):
+ @urlmatch(
+ scheme="http", netloc="localhost", path="/api/v4/not_there", method="delete"
+ )
+ def resp_cont(url, request):
+ content = {"Here is wh it failed"}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ with pytest.raises(GitlabHttpError):
+ gl.http_delete("/not_there")
diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py
new file mode 100644
index 0000000..a2e5ff5
--- /dev/null
+++ b/tests/unit/test_types.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2018 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from gitlab import types
+
+
+def test_gitlab_attribute_get():
+ o = types.GitlabAttribute("whatever")
+ assert o.get() == "whatever"
+
+ o.set_from_cli("whatever2")
+ assert o.get() == "whatever2"
+ assert o.get_for_api() == "whatever2"
+
+ o = types.GitlabAttribute()
+ assert o._value is None
+
+
+def test_list_attribute_input():
+ o = types.ListAttribute()
+ o.set_from_cli("foo,bar,baz")
+ assert o.get() == ["foo", "bar", "baz"]
+
+ o.set_from_cli("foo")
+ assert o.get() == ["foo"]
+
+
+def test_list_attribute_empty_input():
+ o = types.ListAttribute()
+ o.set_from_cli("")
+ assert o.get() == []
+
+ o.set_from_cli(" ")
+ assert o.get() == []
+
+
+def test_list_attribute_get_for_api_from_cli():
+ o = types.ListAttribute()
+ o.set_from_cli("foo,bar,baz")
+ assert o.get_for_api() == "foo,bar,baz"
+
+
+def test_list_attribute_get_for_api_from_list():
+ o = types.ListAttribute(["foo", "bar", "baz"])
+ assert o.get_for_api() == "foo,bar,baz"
+
+
+def test_list_attribute_get_for_api_from_int_list():
+ o = types.ListAttribute([1, 9, 7])
+ assert o.get_for_api() == "1,9,7"
+
+
+def test_list_attribute_does_not_split_string():
+ o = types.ListAttribute("foo")
+ assert o.get_for_api() == "foo"
+
+
+def test_lowercase_string_attribute_get_for_api():
+ o = types.LowercaseStringAttribute("FOO")
+ assert o.get_for_api() == "foo"
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 0000000..dbe0838
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2019 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from gitlab import utils
+
+
+def test_clean_str_id():
+ src = "nothing_special"
+ dest = "nothing_special"
+ assert dest == utils.clean_str_id(src)
+
+ src = "foo#bar/baz/"
+ dest = "foo%23bar%2Fbaz%2F"
+ assert dest == utils.clean_str_id(src)
+
+ src = "foo%bar/baz/"
+ dest = "foo%25bar%2Fbaz%2F"
+ assert dest == utils.clean_str_id(src)
+
+
+def test_sanitized_url():
+ src = "http://localhost/foo/bar"
+ dest = "http://localhost/foo/bar"
+ assert dest == utils.sanitized_url(src)
+
+ src = "http://localhost/foo.bar.baz"
+ dest = "http://localhost/foo%2Ebar%2Ebaz"
+ assert dest == utils.sanitized_url(src)