Commit a0076bb1 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '37248-add-api-to-monitor-issue-creation' into 'master'

Add REST and GraphQL API to retrieve specific issue

Closes #37248

See merge request gitlab-org/gitlab!35176
parents cb6518ff 6a42fc41
......@@ -70,9 +70,19 @@ module Types
description: 'Text to echo back',
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
DesignManagementObject.new(nil)
end
def issue(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue)
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 {
node: Issue
}
"""
Identifier of Issue
"""
scalar IssueID
"""
Autogenerated input type of IssueMoveList
"""
......@@ -12507,6 +12512,16 @@ type Query {
"""
instanceSecurityDashboard: InstanceSecurityDashboard
"""
Find an issue
"""
issue(
"""
The global ID of the Issue
"""
id: IssueID!
): Issue
"""
Find an iteration
"""
......
......@@ -19963,6 +19963,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "IssueID",
"description": "Identifier of Issue",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "IssueMoveListInput",
......@@ -36856,6 +36866,33 @@
"isDeprecated": false,
"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",
"description": "Find an iteration",
......@@ -564,6 +564,320 @@ the issue still exists.
## 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.
If the project is private or the issue is confidential, you need to provide credentials to authorize.
......
......@@ -114,6 +114,19 @@ module API
present issues, options
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
params do
......
......@@ -20,6 +20,7 @@ RSpec.describe GitlabSchema.types['Query'] do
milestone
user
users
issue
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
......@@ -53,4 +54,12 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
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
# 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
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
context 'when unauthenticated' do
it 'returns an array of all issues' do
......@@ -128,6 +168,11 @@ RSpec.describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id])
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
it 'returns authentication error without any scope' do
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