diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md index ac26d2a3d173443f4744176b6fe54d358f1821df..eb9e8e8adc0c37bb36336ed2eab4f8d8cb36093b 100644 --- a/doc/api/issue_links.md +++ b/doc/api/issue_links.md @@ -63,6 +63,106 @@ Parameters: ] ``` +## Get an issue link + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88228) in GitLab 15.1. + +Gets details about an issue link. + +```plaintext +GET /projects/:id/issues/:issue_iid/links/:issue_link_id +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|-----------------|----------------|------------------------|-----------------------------------------------------------------------------| +| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). | +| `issue_iid` | integer | **{check-circle}** Yes | Internal ID of a project's issue. | +| `issue_link_id` | integer/string | **{check-circle}** Yes | ID of an issue relationship. | + +Response body attributes: + +| Attribute | Type | Description | +|:---------------|:-------|:------------------------------------------------------------------------------------------| +| `source_issue` | object | Details of the source issue of the relationship. | +| `target_issue` | object | Details of the target issue of the relationship. | +| `link_type` | string | Type of the relationship. Possible values are `relates_to`, `blocks` and `is_blocked_by`. | + +Example request: + +```shell +curl --request GET --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/84/issues/14/links/1" +``` + +Example response: + +```json +{ + "source_issue" : { + "id" : 83, + "iid" : 11, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/11", + "confidential": false, + "weight": null + }, + "target_issue" : { + "id" : 84, + "iid" : 14, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/14", + "confidential": false, + "weight": null + }, + "link_type": "relates_to" +} +``` + ## Create an issue link Creates a two-way relation between two issues. The user must be allowed to diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index cf075af837339cc825fe989ac8da2bd4bab2f158..c07c2c1994eb1000da4a655c8d39bde4decd6f9c 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -61,6 +61,22 @@ class IssueLinks < ::API::Base end # rubocop: enable CodeReuse/ActiveRecord + desc 'Get issues relation' do + detail 'This feature was introduced in GitLab 15.1.' + success Entities::IssueLink + end + params do + requires :issue_link_id, type: Integer, desc: 'The ID of an issue link' + end + get ':id/issues/:issue_iid/links/:issue_link_id' do + issue = find_project_issue(params[:issue_iid]) + issue_link = IssueLink.for_source_or_target(issue).find(declared_params[:issue_link_id]) + + find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s) + + present issue_link, with: Entities::IssueLink + end + desc 'Remove issues relation' do success Entities::IssueLink end diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb index 81dd4c3dfa05a977d90a244ae483ed0618d4af79..90238c8bf765bbed5eaca19ed2516d7ee07e620a 100644 --- a/spec/requests/api/issue_links_spec.rb +++ b/spec/requests/api/issue_links_spec.rb @@ -156,6 +156,87 @@ def expect_link_response(link_type: 'relates_to') end end + describe 'GET /links/:issue_link_id' do + def perform_request(issue_link_id, user = nil, params = {}) + get api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link_id}", user), params: params + end + + context 'when unauthenticated' do + it 'returns 401' do + issue_link = create(:issue_link) + + perform_request(issue_link.id) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when authenticated' do + context 'when issue link does not exist' do + it 'returns 404' do + perform_request(non_existing_record_id, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + let_it_be(:target_issue) { create(:issue, project: project) } + + context 'when issue link does not belong to the specified issue' do + it 'returns 404' do + other_issue = create(:issue, project: project) + # source is different than the given API route issue + issue_link = create(:issue_link, source: other_issue, target: target_issue) + + perform_request(issue_link.id, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user has ability to read the issue link' do + it 'returns 200' do + issue_link = create(:issue_link, source: issue, target: target_issue) + + perform_request(issue_link.id, user) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/issue_link') + end + end + end + + context 'when user cannot read issue link' do + let(:private_project) { create(:project) } + let(:public_project) { create(:project, :public) } + let(:public_issue) { create(:issue, project: public_project) } + + context 'when the issue link targets an issue in a non-accessible project' do + it 'returns 404' do + private_issue = create(:issue, project: private_project) + issue_link = create(:issue_link, source: public_issue, target: private_issue) + + perform_request(issue_link.id, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when issue link targets a non-accessible issue' do + it 'returns 404' do + confidential_issue = create(:issue, :confidential, project: public_project) + issue_link = create(:issue_link, source: public_issue, target: confidential_issue) + + perform_request(issue_link.id, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + end + describe 'DELETE /links/:issue_link_id' do context 'when unauthenticated' do it 'returns 401' do