From 0169c459e2d062d12390d54270310bbb7c7f319a Mon Sep 17 00:00:00 2001 From: Sachin Singh Date: Wed, 22 Jan 2025 22:36:33 +0000 Subject: [PATCH] feat(api): add support for external status check --- docs/api-objects.rst | 1 + docs/gl_objects/status_checks.rst | 57 ++++++++++ gitlab/v4/objects/__init__.py | 1 + gitlab/v4/objects/merge_requests.py | 2 + gitlab/v4/objects/projects.py | 2 + gitlab/v4/objects/status_checks.py | 52 ++++++++++ tests/functional/api/test_projects.py | 16 +++ tests/unit/objects/test_status_checks.py | 127 +++++++++++++++++++++++ 8 files changed, 258 insertions(+) create mode 100644 docs/gl_objects/status_checks.rst create mode 100644 gitlab/v4/objects/status_checks.py create mode 100644 tests/unit/objects/test_status_checks.py diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 4868983e6..c8d4b7891 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -63,6 +63,7 @@ API examples gl_objects/settings gl_objects/snippets gl_objects/statistics + gl_objects/status_checks gl_objects/system_hooks gl_objects/templates gl_objects/todos diff --git a/docs/gl_objects/status_checks.rst b/docs/gl_objects/status_checks.rst new file mode 100644 index 000000000..71d0c1abf --- /dev/null +++ b/docs/gl_objects/status_checks.rst @@ -0,0 +1,57 @@ +####################### +External Status Checks +####################### + +Manage external status checks for projects and merge requests. + + +Project external status checks +=============================== + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectExternalStatusCheck` + + :class:`gitlab.v4.objects.ProjectExternalStatusCheckManager` + + :attr:`gitlab.v4.objects.Project.external_status_checks` + +* GitLab API: https://docs.gitlab.com/ee/api/status_checks.html + +Examples +--------- + +List external status checks for a project:: + + status_checks = project.external_status_checks.list() + +Create an external status check with shared secret:: + + status_checks = project.external_status_checks.create({ + "name": "mr_blocker", + "external_url": "https://example.com/mr-status-check", + "shared_secret": "secret-string" + }) + +Create an external status check with shared secret for protected branches:: + + protected_branch = project.protectedbranches.get('main') + + status_check = project.external_status_checks.create({ + "name": "mr_blocker", + "external_url": "https://example.com/mr-status-check", + "shared_secret": "secret-string", + "protected_branch_ids": [protected_branch.id] + }) + + +Update an external status check:: + + status_check.external_url = "https://example.com/mr-blocker" + status_check.save() + +Delete an external status check:: + + status_check.delete(status_check_id) + diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py index 3aa992203..7932080ac 100644 --- a/gitlab/v4/objects/__init__.py +++ b/gitlab/v4/objects/__init__.py @@ -68,6 +68,7 @@ from .sidekiq import * from .snippets import * from .statistics import * +from .status_checks import * from .tags import * from .templates import * from .todos import * diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py index e29ab2b28..30ddc11f5 100644 --- a/gitlab/v4/objects/merge_requests.py +++ b/gitlab/v4/objects/merge_requests.py @@ -44,6 +44,7 @@ from .notes import ProjectMergeRequestNoteManager # noqa: F401 from .pipelines import ProjectMergeRequestPipelineManager # noqa: F401 from .reviewers import ProjectMergeRequestReviewerDetailManager +from .status_checks import ProjectMergeRequestStatusCheckManager __all__ = [ "MergeRequest", @@ -167,6 +168,7 @@ class ProjectMergeRequest( resourcemilestoneevents: ProjectMergeRequestResourceMilestoneEventManager resourcestateevents: ProjectMergeRequestResourceStateEventManager reviewer_details: ProjectMergeRequestReviewerDetailManager + status_checks: ProjectMergeRequestStatusCheckManager @cli.register_custom_action(cls_names="ProjectMergeRequest") @exc.on_http_error(exc.GitlabMROnBuildSuccessError) diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index daf69c923..b2e86a65d 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -104,6 +104,7 @@ ProjectAdditionalStatisticsManager, ProjectIssuesStatisticsManager, ) +from .status_checks import ProjectExternalStatusCheckManager # noqa: F401 from .tags import ProjectProtectedTagManager, ProjectTagManager # noqa: F401 from .templates import ( # noqa: F401 ProjectDockerfileTemplateManager, @@ -253,6 +254,7 @@ class Project( secure_files: ProjectSecureFileManager services: ProjectServiceManager snippets: ProjectSnippetManager + external_status_checks: ProjectExternalStatusCheckManager storage: "ProjectStorageManager" tags: ProjectTagManager triggers: ProjectTriggerManager diff --git a/gitlab/v4/objects/status_checks.py b/gitlab/v4/objects/status_checks.py new file mode 100644 index 000000000..045b57260 --- /dev/null +++ b/gitlab/v4/objects/status_checks.py @@ -0,0 +1,52 @@ +from gitlab.base import RESTManager, RESTObject +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + ObjectDeleteMixin, + SaveMixin, + UpdateMethod, + UpdateMixin, +) +from gitlab.types import ArrayAttribute, RequiredOptional + +__all__ = [ + "ProjectExternalStatusCheck", + "ProjectExternalStatusCheckManager", + "ProjectMergeRequestStatusCheck", + "ProjectMergeRequestStatusCheckManager", +] + + +class ProjectExternalStatusCheck(SaveMixin, ObjectDeleteMixin, RESTObject): + pass + + +class ProjectExternalStatusCheckManager( + ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager +): + _path = "/projects/{project_id}/external_status_checks" + _obj_cls = ProjectExternalStatusCheck + _from_parent_attrs = {"project_id": "id"} + _create_attrs = RequiredOptional( + required=("name", "external_url"), + optional=("shared_secret", "protected_branch_ids"), + ) + _update_attrs = RequiredOptional( + optional=("name", "external_url", "shared_secret", "protected_branch_ids") + ) + _types = {"protected_branch_ids": ArrayAttribute} + + +class ProjectMergeRequestStatusCheck(SaveMixin, RESTObject): + pass + + +class ProjectMergeRequestStatusCheckManager(ListMixin, RESTManager): + _path = "/projects/{project_id}/merge_requests/{merge_request_iid}/status_checks" + _obj_cls = ProjectMergeRequestStatusCheck + _from_parent_attrs = {"project_id": "project_id", "merge_request_iid": "iid"} + _update_attrs = RequiredOptional( + required=("sha", "external_status_check_id", "status") + ) + _update_method = UpdateMethod.POST diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index c96d93a13..18c850680 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -399,3 +399,19 @@ def test_project_transfer(gl, project, group): project = gl.projects.get(project.id) assert project.namespace["path"] == gl.user.username + + +@pytest.mark.gitlab_premium +def test_project_external_status_check_create(gl, project): + status_check = project.external_status_checks.create( + {"name": "MR blocker", "external_url": "https://example.com/mr-blocker"} + ) + assert status_check.name == "MR blocker" + assert status_check.external_url == "https://example.com/mr-blocker" + + +@pytest.mark.gitlab_premium +def test_project_external_status_check_list(gl, project): + status_checks = project.external_status_checks.list() + + assert len(status_checks) == 1 diff --git a/tests/unit/objects/test_status_checks.py b/tests/unit/objects/test_status_checks.py new file mode 100644 index 000000000..14d1e73d4 --- /dev/null +++ b/tests/unit/objects/test_status_checks.py @@ -0,0 +1,127 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/status_checks.html +""" + +import pytest +import responses + + +@pytest.fixture +def external_status_check(): + return { + "id": 1, + "name": "MR blocker", + "project_id": 1, + "external_url": "https://example.com/mr-blocker", + "hmac": True, + "protected_branches": [ + { + "id": 1, + "project_id": 1, + "name": "main", + "created_at": "2020-10-12T14:04:50.787Z", + "updated_at": "2020-10-12T14:04:50.787Z", + "code_owner_approval_required": False, + } + ], + } + + +@pytest.fixture +def updated_external_status_check(): + return { + "id": 1, + "name": "Updated MR blocker", + "project_id": 1, + "external_url": "https://example.com/mr-blocker", + "hmac": True, + "protected_branches": [ + { + "id": 1, + "project_id": 1, + "name": "main", + "created_at": "2020-10-12T14:04:50.787Z", + "updated_at": "2020-10-12T14:04:50.787Z", + "code_owner_approval_required": False, + } + ], + } + + +@pytest.fixture +def resp_list_external_status_checks(external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=[external_status_check], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_external_status_checks(external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=external_status_check, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_external_status_checks(updated_external_status_check): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/groups/1/external_status_checks", + json=updated_external_status_check, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_external_status_checks(): + content = [] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/external_status_checks/1", + status=204, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/external_status_checks", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_external_status_checks(gl, resp_list_external_status_checks): + status_checks = gl.projects.get(1, lazy=True).external_status_checks.list() + assert len(status_checks) == 1 + assert status_checks[0].name == "MR blocker" + + +def test_create_external_status_checks(gl, resp_create_external_status_checks): + access_token = gl.projects.get(1, lazy=True).external_status_checks.create( + {"name": "MR blocker", "external_url": "https://example.com/mr-blocker"} + ) + assert access_token.name == "MR blocker" + assert access_token.external_url == "https://example.com/mr-blocker" + + +def test_delete_external_status_checks(gl, resp_delete_external_status_checks): + gl.projects.get(1, lazy=True).external_status_checks.delete(1) + status_checks = gl.projects.get(1, lazy=True).external_status_checks.list() + assert len(status_checks) == 0