Commit 6a42fc41 authored by Ethan Urie's avatar Ethan Urie Committed by Douglas Barbosa Alexandre

Add `issues/:id` endpoint

Also added specs.
parent 787ad7eb
...@@ -70,9 +70,19 @@ module Types ...@@ -70,9 +70,19 @@ module Types
description: 'Text to echo back', description: 'Text to echo back',
resolver: Resolvers::EchoResolver resolver: Resolvers::EchoResolver
field :issue, Types::IssueType,
null: true,
description: 'Find an issue' do
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue'
end
def design_management def design_management
DesignManagementObject.new(nil) DesignManagementObject.new(nil)
end end
def issue(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue)
end
end end
end end
......
---
title: Add ability to get an Issue using GraphQL and REST API
merge_request: 35176
author:
type: added
...@@ -7222,6 +7222,11 @@ type IssueEdge { ...@@ -7222,6 +7222,11 @@ type IssueEdge {
node: Issue node: Issue
} }
"""
Identifier of Issue
"""
scalar IssueID
""" """
Autogenerated input type of IssueMoveList Autogenerated input type of IssueMoveList
""" """
...@@ -12507,6 +12512,16 @@ type Query { ...@@ -12507,6 +12512,16 @@ type Query {
""" """
instanceSecurityDashboard: InstanceSecurityDashboard instanceSecurityDashboard: InstanceSecurityDashboard
"""
Find an issue
"""
issue(
"""
The global ID of the Issue
"""
id: IssueID!
): Issue
""" """
Find an iteration Find an iteration
""" """
......
...@@ -19963,6 +19963,16 @@ ...@@ -19963,6 +19963,16 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "IssueID",
"description": "Identifier of Issue",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "IssueMoveListInput", "name": "IssueMoveListInput",
...@@ -36856,6 +36866,33 @@ ...@@ -36856,6 +36866,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "issue",
"description": "Find an issue",
"args": [
{
"name": "id",
"description": "The global ID of the Issue",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "IssueID",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "iteration", "name": "iteration",
"description": "Find an iteration", "description": "Find an iteration",
...@@ -564,6 +564,320 @@ the issue still exists. ...@@ -564,6 +564,320 @@ the issue still exists.
## Single issue ## Single issue
Only for administrators. Get a single issue.
The preferred way to do this is by using [personal access tokens](../user/profile/personal_access_tokens.md).
```plaintext
GET /issues/:id
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer | yes | The ID of the issue |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/issues/41"
```
Example response:
```json
{
"id" : 1,
"milestone" : {
"due_date" : null,
"project_id" : 4,
"state" : "closed",
"description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
"iid" : 3,
"id" : 11,
"title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z",
"updated_at" : "2016-01-04T15:31:39.788Z",
"closed_at" : "2016-01-05T15:31:46.176Z"
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"subscribed": false,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/my-group/my-project/issues/1",
"references": {
"short": "#1",
"relative": "#1",
"full": "my-group/my-project#1"
},
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
},
"confidential": false,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
},
"task_completion_status":{
"count":0,
"completed_count":0
},
"weight": null,
"has_tasks": false,
"_links": {
"self": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1",
"notes": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/notes",
"award_emoji": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/award_emoji",
"project": "http://gitlab.dummy:3000/api/v4/projects/1"
},
"references": {
"short": "#1",
"relative": "#1",
"full": "gitlab-org/gitlab-test#1"
},
"subscribed": true,
"moved_to_id": null,
"epic_iid": null,
"epic": null
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
the `weight` parameter:
```json
{
"project_id" : 4,
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"weight": null,
...
}
```
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) will additionally see
the `epic` property:
```javascript
{
"project_id" : 4,
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"epic": {
"epic_iid" : 5, //deprecated, use `iid` of the `epic` attribute
"epic": {
"id" : 42,
"iid" : 5,
"title": "My epic epic",
"url" : "/groups/h5bp/-/epics/5",
"group_id": 8
},
// ...
}
```
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042). This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
**Note**: The `epic_iid` attribute is deprecated and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/-/issues/35157).
Please use `iid` of the `epic` attribute instead.
## Single Issue
Only for administrators. Get a single issue.
The preferred way to do this is by using [personal access tokens](../user/profile/personal_access_tokens.md).
```plaintext
GET /issues/:id
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer | yes | The ID of the issue |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/issues/41"
```
Example response:
```json
{
"id" : 1,
"milestone" : {
"due_date" : null,
"project_id" : 4,
"state" : "closed",
"description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
"iid" : 3,
"id" : 11,
"title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z",
"updated_at" : "2016-01-04T15:31:39.788Z",
"closed_at" : "2016-01-05T15:31:46.176Z"
},
"author" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"subscribed": false,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/my-group/my-project/issues/1",
"references": {
"short": "#1",
"relative": "#1",
"full": "my-group/my-project#1"
},
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
},
"confidential": false,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
},
"task_completion_status":{
"count":0,
"completed_count":0
},
"weight": null,
"has_tasks": false,
"_links": {
"self": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1",
"notes": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/notes",
"award_emoji": "http://gitlab.dummy:3000/api/v4/projects/1/issues/1/award_emoji",
"project": "http://gitlab.dummy:3000/api/v4/projects/1"
},
"references": {
"short": "#1",
"relative": "#1",
"full": "gitlab-org/gitlab-test#1"
},
"subscribed": true,
"moved_to_id": null,
"epic_iid": null,
"epic": null
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
the `weight` parameter:
```json
{
"project_id" : 4,
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"weight": null,
...
}
```
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) will additionally see
the `epic` property:
```javascript
{
"project_id" : 4,
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"epic": {
"epic_iid" : 5, //deprecated, use `iid` of the `epic` attribute
"epic": {
"id" : 42,
"iid" : 5,
"title": "My epic epic",
"url" : "/groups/h5bp/-/epics/5",
"group_id": 8
},
// ...
}
```
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042). This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
**Note**: The `epic_iid` attribute is deprecated and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/-/issues/35157).
Please use `iid` of the `epic` attribute instead.
## Single Project Issue
Get a single project issue. Get a single project issue.
If the project is private or the issue is confidential, you need to provide credentials to authorize. If the project is private or the issue is confidential, you need to provide credentials to authorize.
......
...@@ -114,6 +114,19 @@ module API ...@@ -114,6 +114,19 @@ module API
present issues, options present issues, options
end end
desc "Get specified issue (admin only)" do
success Entities::Issue
end
params do
requires :id, type: String, desc: 'The ID of the Issue'
end
get ":id" do
authenticated_as_admin!
issue = Issue.find(params['id'])
present issue, with: Entities::Issue, current_user: current_user, project: issue.project
end
end end
params do params do
......
...@@ -20,6 +20,7 @@ RSpec.describe GitlabSchema.types['Query'] do ...@@ -20,6 +20,7 @@ RSpec.describe GitlabSchema.types['Query'] do
milestone milestone
user user
users users
issue
] ]
expect(described_class).to have_graphql_fields(*expected_fields).at_least expect(described_class).to have_graphql_fields(*expected_fields).at_least
...@@ -53,4 +54,12 @@ RSpec.describe GitlabSchema.types['Query'] do ...@@ -53,4 +54,12 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_resolver(Resolvers::MetadataResolver) is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
end end
end end
describe 'issue field' do
subject { described_class.fields['issue'] }
it 'returns issue' do
is_expected.to have_graphql_type(Types::IssueType)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.issue(id)' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:current_user) { create(:user) }
let(:issue_data) { graphql_data['issue'] }
let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
let(:issue_fields) { all_graphql_fields_for('Issue'.classify) }
let(:query) do
graphql_query_for('issue', issue_params, issue_fields)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when the user does not have access to the issue' do
it 'returns nil' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
post_graphql(query)
expect(issue_data).to be nil
end
end
context 'when the user does have access' do
before do
project.add_guest(current_user)
end
it 'returns the issue' do
post_graphql(query, current_user: current_user)
expect(issue_data).to include(
'title' => issue.title,
'description' => issue.description
)
end
context 'selecting any single field' do
where(:field) do
scalar_fields_of('Issue').map { |name| [name] }
end
with_them do
it_behaves_like 'a working graphql query' do
let(:issue_fields) do
field
end
before do
post_graphql(query, current_user: current_user)
end
it "returns the Issue and field #{params['field']}" do
expect(issue_data.keys).to eq([field])
end
end
end
end
context 'selecting multiple fields' do
let(:issue_fields) { %w(title description) }
it 'returns the Issue with the specified fields' do
post_graphql(query, current_user: current_user)
expect(issue_data.keys).to eq( %w(title description) )
expect(issue_data['title']).to eq(issue.title)
expect(issue_data['description']).to eq(issue.description)
end
end
context 'when passed a non-Issue gid' do
let(:mr) {create(:merge_request)}
it 'returns an error' do
gid = mr.to_global_id.to_s
issue_params['id'] = gid
post_graphql(query, current_user: current_user)
expect(graphql_errors).not_to be nil
expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of Issue")
end
end
end
context 'when there is a confidential issue' do
let!(:confidential_issue) do
create(:issue, :confidential, project: project)
end
let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } }
context 'when the user cannot see confidential issues' do
it 'returns nil ' do
post_graphql(query, current_user: current_user)
expect(issue_data).to be nil
end
end
context 'when the user can see confidential issues' do
it 'returns the confidential issue' do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
expect(graphql_data.count).to eq(1)
expect(issue_data['confidential']).to be(true)
end
end
end
end
...@@ -87,6 +87,46 @@ RSpec.describe API::Issues do ...@@ -87,6 +87,46 @@ RSpec.describe API::Issues do
end end
end end
describe 'GET /issues/:id' do
context 'when unauthorized' do
it 'returns unauthorized' do
get api("/issues/#{issue.id}" )
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authorized' do
context 'as a normal user' do
it 'returns forbidden' do
get api("/issues/#{issue.id}", user )
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'as an admin' do
context 'when issue exists' do
it 'returns the issue' do
get api("/issues/#{issue.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig('author', 'id')).to eq(issue.author.id)
expect(json_response['description']).to eq(issue.description)
end
end
context 'when issue does not exist' do
it 'returns 404' do
get api("/issues/0", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
describe 'GET /issues' do describe 'GET /issues' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns an array of all issues' do it 'returns an array of all issues' do
...@@ -128,6 +168,11 @@ RSpec.describe API::Issues do ...@@ -128,6 +168,11 @@ RSpec.describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id]) expect_paginated_array_response([issue.id, closed_issue.id])
end end
it 'responds with a 401 instead of the specified issue' do
get api("/issues/#{issue.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'issues_statistics' do context 'issues_statistics' do
it 'returns authentication error without any scope' do it 'returns authentication error without any scope' do
get api('/issues_statistics') get api('/issues_statistics')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment