summaryrefslogtreecommitdiff
path: root/tests/unit/objects
diff options
context:
space:
mode:
authorJohn L. Villalovos <john@sodarock.com>2021-05-26 21:04:31 -0700
committerJohn L. Villalovos <john@sodarock.com>2021-05-26 21:04:31 -0700
commit1ac0722bc086b18c070132a0eb53747bbdf2ce0a (patch)
treedb9dbb8da13907eb6ee293adcaf6c9bdb1dd7d52 /tests/unit/objects
parent90ecf2f91129ffa0cfb5db58300fbd11638d4ecc (diff)
downloadgitlab-1ac0722bc086b18c070132a0eb53747bbdf2ce0a.tar.gz
chore: move 'gitlab/tests/' dir to 'tests/unit/'
Move the 'gitlab/tests/' directory to 'tests/unit/' so we have all the tests located under the 'tests/' directory.
Diffstat (limited to 'tests/unit/objects')
-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
40 files changed, 4083 insertions, 0 deletions
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()