From 1ac0722bc086b18c070132a0eb53747bbdf2ce0a Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 26 May 2021 21:04:31 -0700 Subject: 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. --- tests/unit/objects/__init__.py | 0 tests/unit/objects/conftest.py | 70 +++++ tests/unit/objects/test_appearance.py | 65 +++++ tests/unit/objects/test_applications.py | 44 +++ tests/unit/objects/test_audit_events.py | 109 +++++++ tests/unit/objects/test_badges.py | 210 ++++++++++++++ tests/unit/objects/test_bridges.py | 109 +++++++ tests/unit/objects/test_commits.py | 115 ++++++++ tests/unit/objects/test_deploy_tokens.py | 45 +++ tests/unit/objects/test_deployments.py | 50 ++++ tests/unit/objects/test_environments.py | 30 ++ tests/unit/objects/test_groups.py | 97 +++++++ tests/unit/objects/test_hooks.py | 29 ++ tests/unit/objects/test_issues.py | 69 +++++ tests/unit/objects/test_job_artifacts.py | 30 ++ tests/unit/objects/test_jobs.py | 96 +++++++ tests/unit/objects/test_members.py | 58 ++++ tests/unit/objects/test_mro.py | 122 ++++++++ tests/unit/objects/test_packages.py | 186 ++++++++++++ tests/unit/objects/test_personal_access_tokens.py | 46 +++ tests/unit/objects/test_pipeline_schedules.py | 62 ++++ tests/unit/objects/test_pipelines.py | 94 ++++++ tests/unit/objects/test_project_access_tokens.py | 113 ++++++++ tests/unit/objects/test_project_import_export.py | 112 ++++++++ .../test_project_merge_request_approvals.py | 317 +++++++++++++++++++++ tests/unit/objects/test_project_statistics.py | 28 ++ tests/unit/objects/test_projects.py | 262 +++++++++++++++++ tests/unit/objects/test_releases.py | 131 +++++++++ tests/unit/objects/test_remote_mirrors.py | 72 +++++ tests/unit/objects/test_repositories.py | 49 ++++ tests/unit/objects/test_resource_label_events.py | 105 +++++++ .../unit/objects/test_resource_milestone_events.py | 73 +++++ tests/unit/objects/test_resource_state_events.py | 104 +++++++ tests/unit/objects/test_runners.py | 282 ++++++++++++++++++ tests/unit/objects/test_services.py | 93 ++++++ tests/unit/objects/test_snippets.py | 89 ++++++ tests/unit/objects/test_submodules.py | 46 +++ tests/unit/objects/test_todos.py | 62 ++++ tests/unit/objects/test_users.py | 217 ++++++++++++++ tests/unit/objects/test_variables.py | 192 +++++++++++++ 40 files changed, 4083 insertions(+) create mode 100644 tests/unit/objects/__init__.py create mode 100644 tests/unit/objects/conftest.py create mode 100644 tests/unit/objects/test_appearance.py create mode 100644 tests/unit/objects/test_applications.py create mode 100644 tests/unit/objects/test_audit_events.py create mode 100644 tests/unit/objects/test_badges.py create mode 100644 tests/unit/objects/test_bridges.py create mode 100644 tests/unit/objects/test_commits.py create mode 100644 tests/unit/objects/test_deploy_tokens.py create mode 100644 tests/unit/objects/test_deployments.py create mode 100644 tests/unit/objects/test_environments.py create mode 100644 tests/unit/objects/test_groups.py create mode 100644 tests/unit/objects/test_hooks.py create mode 100644 tests/unit/objects/test_issues.py create mode 100644 tests/unit/objects/test_job_artifacts.py create mode 100644 tests/unit/objects/test_jobs.py create mode 100644 tests/unit/objects/test_members.py create mode 100644 tests/unit/objects/test_mro.py create mode 100644 tests/unit/objects/test_packages.py create mode 100644 tests/unit/objects/test_personal_access_tokens.py create mode 100644 tests/unit/objects/test_pipeline_schedules.py create mode 100644 tests/unit/objects/test_pipelines.py create mode 100644 tests/unit/objects/test_project_access_tokens.py create mode 100644 tests/unit/objects/test_project_import_export.py create mode 100644 tests/unit/objects/test_project_merge_request_approvals.py create mode 100644 tests/unit/objects/test_project_statistics.py create mode 100644 tests/unit/objects/test_projects.py create mode 100644 tests/unit/objects/test_releases.py create mode 100644 tests/unit/objects/test_remote_mirrors.py create mode 100644 tests/unit/objects/test_repositories.py create mode 100644 tests/unit/objects/test_resource_label_events.py create mode 100644 tests/unit/objects/test_resource_milestone_events.py create mode 100644 tests/unit/objects/test_resource_state_events.py create mode 100644 tests/unit/objects/test_runners.py create mode 100644 tests/unit/objects/test_services.py create mode 100644 tests/unit/objects/test_snippets.py create mode 100644 tests/unit/objects/test_submodules.py create mode 100644 tests/unit/objects/test_todos.py create mode 100644 tests/unit/objects/test_users.py create mode 100644 tests/unit/objects/test_variables.py (limited to 'tests/unit/objects') diff --git a/tests/unit/objects/__init__.py b/tests/unit/objects/__init__.py new file mode 100644 index 0000000..e69de29 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": "

Message

", + "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() -- cgit v1.2.1