diff --git a/docs/gl_objects/status_checks.rst b/docs/gl_objects/status_checks.rst index 062231216..41383394b 100644 --- a/docs/gl_objects/status_checks.rst +++ b/docs/gl_objects/status_checks.rst @@ -24,11 +24,11 @@ Examples List external status checks for a project:: - status_checks = project.external_status_checks.list(get_all=True) + external_status_checks = project.external_status_checks.list() Create an external status check with shared secret:: - status_checks = project.external_status_checks.create({ + external_status_checks = project.external_status_checks.create({ "name": "mr_blocker", "external_url": "https://example.com/mr-status-check", "shared_secret": "secret-string" @@ -38,7 +38,7 @@ Create an external status check with shared secret for protected branches:: protected_branch = project.protectedbranches.get('main') - status_check = project.external_status_checks.create({ + external_status_check = project.external_status_checks.create({ "name": "mr_blocker", "external_url": "https://example.com/mr-status-check", "shared_secret": "secret-string", @@ -48,10 +48,25 @@ Create an external status check with shared secret for protected branches:: Update an external status check:: - status_check.external_url = "https://example.com/mr-blocker" - status_check.save() + external_status_check.external_url = "https://example.com/mr-blocker" + external_status_check.save() Delete an external status check:: - status_check.delete(status_check_id) + external_status_check.delete(externa_status_check_id) +List external status check for a project merge request:: + + merge_request = project.mergerequests.get(1) + + merge_request.external_status_checks.list() + +Set external status check for a project merge request:: + + merge_request = project.mergerequests.get(1) + + merge_request.external_status_check_response.update({ + "external_status_check_id": external_status_check_id, + "status": "passed", + "sha": merge_request.sha + }) diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py index 4ebd03f5b..4c0cf841f 100644 --- a/gitlab/v4/objects/merge_requests.py +++ b/gitlab/v4/objects/merge_requests.py @@ -46,7 +46,10 @@ from .notes import ProjectMergeRequestNoteManager # noqa: F401 from .pipelines import ProjectMergeRequestPipelineManager # noqa: F401 from .reviewers import ProjectMergeRequestReviewerDetailManager -from .status_checks import ProjectMergeRequestStatusCheckManager +from .status_checks import ( + ProjectMergeRequestStatusCheckManager, + ProjectMergeRequestStatusCheckResponseManager, +) __all__ = [ "MergeRequest", @@ -170,7 +173,8 @@ class ProjectMergeRequest( resourcemilestoneevents: ProjectMergeRequestResourceMilestoneEventManager resourcestateevents: ProjectMergeRequestResourceStateEventManager reviewer_details: ProjectMergeRequestReviewerDetailManager - status_checks: ProjectMergeRequestStatusCheckManager + external_status_checks: ProjectMergeRequestStatusCheckManager + external_status_check_response: ProjectMergeRequestStatusCheckResponseManager @cli.register_custom_action(cls_names="ProjectMergeRequest") @exc.on_http_error(exc.GitlabMROnBuildSuccessError) diff --git a/gitlab/v4/objects/status_checks.py b/gitlab/v4/objects/status_checks.py index e54b7444e..b5dbb03f1 100644 --- a/gitlab/v4/objects/status_checks.py +++ b/gitlab/v4/objects/status_checks.py @@ -1,4 +1,6 @@ -from gitlab.base import RESTObject +from typing import Any, Dict, Optional + +from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, @@ -27,6 +29,7 @@ class ProjectExternalStatusCheckManager( CreateMixin[ProjectExternalStatusCheck], UpdateMixin[ProjectExternalStatusCheck], DeleteMixin[ProjectExternalStatusCheck], + RESTManager[ProjectExternalStatusCheck], ): _path = "/projects/{project_id}/external_status_checks" _obj_cls = ProjectExternalStatusCheck @@ -41,15 +44,41 @@ class ProjectExternalStatusCheckManager( _types = {"protected_branch_ids": ArrayAttribute} -class ProjectMergeRequestStatusCheck(SaveMixin, RESTObject): +class ProjectMergeRequestStatusCheckResponse(SaveMixin, RESTObject): pass -class ProjectMergeRequestStatusCheckManager(ListMixin[ProjectMergeRequestStatusCheck]): - _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"} +class ProjectMergeRequestStatusCheckResponseManager( + UpdateMixin[ProjectMergeRequestStatusCheckResponse], + RESTManager[ProjectMergeRequestStatusCheckResponse], +): + _path = "/projects/{project_id}/merge_requests/{mr_iid}/status_check_responses" + _obj_cls = ProjectMergeRequestStatusCheckResponse + _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} _update_attrs = RequiredOptional( required=("sha", "external_status_check_id", "status") ) _update_method = UpdateMethod.POST + + def update( # type: ignore[override] + self, new_data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> Dict[str, Any]: + """Update a Label on the server. + + Args: + **kwargs: Extra options to send to the server (e.g. sudo) + """ + return super().update(id=None, new_data=new_data, **kwargs) + + +class ProjectMergeRequestStatusCheck(RESTObject): + pass + + +class ProjectMergeRequestStatusCheckManager( + ListMixin[ProjectMergeRequestStatusCheck], + RESTManager[ProjectMergeRequestStatusCheck], +): + _path = "/projects/{project_id}/merge_requests/{mr_iid}/status_checks" + _obj_cls = ProjectMergeRequestStatusCheck + _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} diff --git a/tests/functional/api/test_merge_requests.py b/tests/functional/api/test_merge_requests.py index 8357a817d..f7339ad3a 100644 --- a/tests/functional/api/test_merge_requests.py +++ b/tests/functional/api/test_merge_requests.py @@ -300,3 +300,46 @@ def test_merge_request_merge_ref_should_fail(project, merge_request) -> None: with pytest.raises(gitlab.exceptions.GitlabGetError): response = merge_request.merge_ref() assert "commit_id" not in response + + +@pytest.mark.gitlab_premium +def test_merge_request_external_status_check_set_status(project, merge_request): + project.external_status_checks.create( + { + "name": "external_status_check", + "external_url": "https://example.com/mr-blocker", + } + ) + + mr_external_status_checks = merge_request.external_status_checks.list() + assert len(mr_external_status_checks) == 1 + + expected_external_status_check = None + + for mr_external_status_check in mr_external_status_checks: + if mr_external_status_check.name == "external_status_check": + expected_external_status_check = mr_external_status_check + break + + assert expected_external_status_check is not None + + # set the external status check value to 'passed' + merge_request.external_status_check_response.update( + { + "external_status_check_id": expected_external_status_check.id, + "status": "passed", + "sha": merge_request.sha, + } + ) + + time.sleep(2) + + # Check the status again to validate the passed status + mr_status_checks = merge_request.external_status_checks.list() + + for mr_status_check in mr_status_checks: + if mr_status_check.name == "external_status_check": + expected_status_check = mr_status_check + break + + assert expected_status_check.status == "passed" diff --git a/tests/unit/objects/test_status_checks.py b/tests/unit/objects/test_status_checks.py index 14d1e73d4..eda2c7e56 100644 --- a/tests/unit/objects/test_status_checks.py +++ b/tests/unit/objects/test_status_checks.py @@ -5,6 +5,49 @@ import pytest import responses +mr_content = { + "id": 1, + "iid": 1, + "project_id": 1, + "title": "test1", + "description": "fixed login page css paddings", + "state": "merged", + "sha": "somerandomstring", + "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", + }, + "reviewers": [ + { + "id": 2, + "name": "Sam Bauch", + "username": "kenyatta_oconnell", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon", + "web_url": "http://gitlab.example.com//kenyatta_oconnell", + } + ], +} + +external_status_checks_content = [ + { + "id": 2, + "name": "Service 2", + "external_url": "https://gitlab.example.com/test-endpoint-2", + "status": "pending", + }, + { + "id": 1, + "name": "Service 1", + "external_url": "https://gitlab.example.com/test-endpoint-1", + "status": "pending", + }, +] + @pytest.fixture def external_status_check(): @@ -104,6 +147,54 @@ def resp_delete_external_status_checks(): content_type="application/json", status=200, ) + + yield rsps + + +@pytest.fixture +def resp_list_merge_requests_status_checks(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1", + json=mr_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/status_checks", + json=external_status_checks_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_list_merge_requests_status_checks_set_value(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1", + json=mr_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/status_checks", + json=external_status_checks_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/merge_requests/1/status_check_responses", + json={"status": "passed"}, + content_type="application/json", + status=200, + ) yield rsps @@ -125,3 +216,30 @@ 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 + + +def test_get_merge_request_external_status_checks( + gl, resp_list_merge_requests_status_checks +): + merge_request = gl.projects.get(1, lazy=True).mergerequests.get(1) + external_status_checks = merge_request.external_status_checks.list() + assert len(external_status_checks) == 2 + + +def test_get_merge_request_external_status_checks_set_value( + gl, resp_list_merge_requests_status_checks_set_value +): + merge_request = gl.projects.get(1, lazy=True).mergerequests.get(1) + external_status_checks = merge_request.external_status_checks.list() + + assert len(external_status_checks) == 2 + for external_status_check in external_status_checks: + if external_status_check.name == "Service 2": + response = merge_request.external_status_check_response.update( + { + "external_status_check_id": external_status_check.id, + "status": "passed", + "sha": merge_request.sha, + } + ) + response["status"] == "passed"