Commit 23660f3a authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-57402-add-issues-statistics-api-endpoints' into 'master'

Add issues_statistics api endpoints

See merge request gitlab-org/gitlab-ce!27366
parents f1456594 9ff6edf6
---
title: Add issues_statistics api endpoints and extend issues search api
merge_request: 27366
author:
type: added
......@@ -37,23 +37,26 @@ GET /issues?confidential=true
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search issues against their `title` and `description` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
| `created_before` | datetime | no | Return issues created on or before the given time |
| `updated_after` | datetime | no | Return issues updated on or after the given time |
| `updated_before` | datetime | no | Return issues updated on or before the given time |
| `confidential ` | Boolean | no | Filter confidential or public issues. |
| `confidential ` | Boolean | no | Filter confidential or public issues. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues
......@@ -109,7 +112,7 @@ Example response:
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
"labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
......@@ -122,8 +125,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"has_tasks": true,
"task_status": "10 of 15 tasks completed",
"confidential": false,
"discussion_locked": false
"discussion_locked": false,
"_links":{
"self":"http://example.com/api/v4/projects/1/issues/76",
"notes":"`http://example.com/`api/v4/projects/1/issues/76/notes",
"award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji",
"project":"http://example.com/api/v4/projects/1"
},
"subscribed": false
}
]
```
......@@ -158,11 +170,14 @@ GET /groups/:id/issues?confidential=true
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
......@@ -221,7 +236,7 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
......@@ -240,8 +255,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"has_tasks": true,
"task_status": "10 of 15 tasks completed",
"confidential": false,
"discussion_locked": false
"discussion_locked": false,
"_links":{
"self":"http://example.com/api/v4/projects/4/issues/41",
"notes":"`http://example.com/`api/v4/projects/4/issues/41/notes",
"award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://example.com/api/v4/projects/4"
},
"subscribed": false
}
]
```
......@@ -277,10 +301,13 @@ GET /projects/:id/issues?confidential=true
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
......@@ -340,7 +367,7 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
......@@ -366,8 +393,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"has_tasks": true,
"task_status": "10 of 15 tasks completed",
"confidential": false,
"discussion_locked": false
"discussion_locked": false,
"_links":{
"self":"http://example.com/api/v4/projects/4/issues/41",
"notes":"`http://example.com/`api/v4/projects/4/issues/41/notes",
"award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://example.com/api/v4/projects/4"
},
"subscribed": false
}
]
```
......
# Issues Statistics API
Every API call to issues_statistics must be authenticated.
If a user is not a member of a project and the project is private, a `GET`
request on that project will result to a `404` status code.
## Get issues statistics
Gets issues count statistics on all issues the authenticated user has access to. By default it
returns only issues created by the current user. To get all issues,
use parameter `scope=all`.
```
GET /issues_statistics
GET /issues_statistics?labels=foo
GET /issues_statistics?labels=foo,bar
GET /issues_statistics?labels=foo,bar&state=opened
GET /issues_statistics?milestone=1.0.0
GET /issues_statistics?milestone=1.0.0&state=opened
GET /issues_statistics?iids[]=42&iids[]=43
GET /issues_statistics?author_id=5
GET /issues_statistics?assignee_id=5
GET /issues_statistics?my_reaction_emoji=star
GET /issues_statistics?search=foo&in=title
GET /issues_statistics?confidential=true
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me` |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `search` | string | no | Search issues against their `title` and `description` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
| `created_before` | datetime | no | Return issues created on or before the given time |
| `updated_after` | datetime | no | Return issues updated on or after the given time |
| `updated_before` | datetime | no | Return issues updated on or before the given time |
| `confidential ` | Boolean | no | Filter confidential or public issues. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues_statistics
```
Example response:
```json
{
"statistics": {
"counts": {
"all": 20,
"closed": 5,
"opened": 15
}
}
}
```
## Get group issues statistics
Gets issues count statistics for given group.
```
GET /groups/:id/issues_statistics
GET /groups/:id/issues_statistics?labels=foo
GET /groups/:id/issues_statistics?labels=foo,bar
GET /groups/:id/issues_statistics?labels=foo,bar&state=opened
GET /groups/:id/issues_statistics?milestone=1.0.0
GET /groups/:id/issues_statistics?milestone=1.0.0&state=opened
GET /groups/:id/issues_statistics?iids[]=42&iids[]=43
GET /groups/:id/issues_statistics?search=issue+title+or+description
GET /groups/:id/issues_statistics?author_id=5
GET /groups/:id/issues_statistics?assignee_id=5
GET /groups/:id/issues_statistics?my_reaction_emoji=star
GET /groups/:id/issues_statistics?confidential=true
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search group issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
| `created_before` | datetime | no | Return issues created on or before the given time |
| `updated_after` | datetime | no | Return issues updated on or after the given time |
| `updated_before` | datetime | no | Return issues updated on or before the given time |
| `confidential ` | Boolean | no | Filter confidential or public issues. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues_statistics
```
Example response:
```json
{
"statistics": {
"counts": {
"all": 20,
"closed": 5,
"opened": 15
}
}
}
```
## Get project issues statistics
Gets issues count statistics for given project.
```
GET /projects/:id/issues_statistics
GET /projects/:id/issues_statistics?labels=foo
GET /projects/:id/issues_statistics?labels=foo,bar
GET /projects/:id/issues_statistics?labels=foo,bar&state=opened
GET /projects/:id/issues_statistics?milestone=1.0.0
GET /projects/:id/issues_statistics?milestone=1.0.0&state=opened
GET /projects/:id/issues_statistics?iids[]=42&iids[]=43
GET /projects/:id/issues_statistics?search=issue+title+or+description
GET /projects/:id/issues_statistics?author_id=5
GET /projects/:id/issues_statistics?assignee_id=5
GET /projects/:id/issues_statistics?my_reaction_emoji=star
GET /projects/:id/issues_statistics?confidential=true
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search project issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
| `created_before` | datetime | no | Return issues created on or before the given time |
| `updated_after` | datetime | no | Return issues updated on or after the given time |
| `updated_before` | datetime | no | Return issues updated on or before the given time |
| `confidential ` | Boolean | no | Filter confidential or public issues. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues_statistics
```
Example response:
```json
{
"statistics": {
"counts": {
"all": 20,
"closed": 5,
"opened": 15
}
}
}
```
......@@ -542,10 +542,15 @@ module API
class IssueBasic < ProjectEntity
expose :closed_at
expose :closed_by, using: Entities::UserBasic
expose :labels do |issue|
# Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort
expose :labels do |issue, options|
if options[:with_labels_details]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
issue.labels.map(&:title).sort
end
end
expose :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic
......@@ -573,6 +578,14 @@ module API
class Issue < IssueBasic
include ::API::Helpers::RelatedResourcesHelpers
expose(:has_tasks) do |issue, _|
!issue.task_list_items.empty?
end
expose :task_status, if: -> (issue, _) do
!issue.task_list_items.empty?
end
expose :_links do
expose :self do |issue|
expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
......
......@@ -18,6 +18,39 @@ module API
:title
]
end
def issue_finder(args = {})
args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] ||= args.delete(:milestone)
args[:label_name] ||= args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
IssuesFinder.new(current_user, args)
end
def find_issues(args = {})
finder = issue_finder(args)
issues = finder.execute.with_api_entity_associations
issues.reorder(order_options_with_tie_breaker) # rubocop: disable CodeReuse/ActiveRecord
end
def issues_statistics(args = {})
finder = issue_finder(args)
counter = Gitlab::IssuablesCountForState.new(finder)
{
statistics: {
counts: {
all: counter[:all],
closed: counter[:closed],
opened: counter[:opened]
}
}
}
end
end
end
end
......@@ -3,27 +3,12 @@
module API
class Issues < Grape::API
include PaginationParams
helpers Helpers::IssuesHelpers
helpers ::Gitlab::IssuableMetadata
before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def find_issues(args = {})
args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
.with_api_entity_associations
issues.reorder(order_options_with_tie_breaker)
end
# rubocop: enable CodeReuse/ActiveRecord
if Gitlab.ee?
params :issues_params_ee do
optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue'
......@@ -34,13 +19,9 @@ module API
end
end
params :issues_params do
params :issues_stats_params do
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
optional :iids, type: Array[Integer], desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
......@@ -49,18 +30,39 @@ module API
optional :created_before, type: DateTime, desc: 'Return issues created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username'
mutually_exclusive :author_id, :author_username
optional :assignee_id, types: [Integer, String], integer_none_any: true,
desc: 'Return issues which are assigned to the user with the given ID'
optional :assignee_username, type: Array[String], check_assignees_count: true,
coerce_with: Validations::CheckAssigneesCount.coerce,
desc: 'Return issues which are assigned to the user with the given username'
mutually_exclusive :assignee_id, :assignee_username
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :confidential, type: Boolean, desc: 'Filter confidential or public issues'
use :pagination
use :issues_params_ee if Gitlab.ee?
end
params :issues_params do
optional :with_labels_details, type: Boolean, desc: 'Return more label data than just lable title', default: false
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
use :issues_stats_params
use :pagination
end
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
......@@ -75,13 +77,23 @@ module API
end
end
desc "Get currently authenticated user's issues statistics"
params do
use :issues_stats_params
optional :scope, type: String, values: %w[created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get '/issues_statistics' do
authenticate! unless params[:scope] == 'all'
present issues_statistics, with: Grape::Presenters::Presenter
end
resource :issues do
desc "Get currently authenticated user's issues" do
success Entities::IssueBasic
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
......@@ -91,7 +103,8 @@ module API
issues = paginate(find_issues)
options = {
with: Entities::IssueBasic,
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
......@@ -105,11 +118,9 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group issues' do
success Entities::IssueBasic
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
......@@ -118,13 +129,24 @@ module API
issues = paginate(find_issues(group_id: group.id, include_subgroups: true))
options = {
with: Entities::IssueBasic,
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
present issues, options
end
desc 'Get statistics for the list of group issues'
params do
use :issues_stats_params
end
get ":id/issues_statistics" do
group = find_group!(params[:id])
present issues_statistics(group_id: group.id, include_subgroups: true), with: Grape::Presenters::Presenter
end
end
params do
......@@ -134,11 +156,9 @@ module API
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
success Entities::IssueBasic
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
......@@ -147,7 +167,8 @@ module API
issues = paginate(find_issues(project_id: project.id))
options = {
with: Entities::IssueBasic,
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
project: user_project,
issuable_metadata: issuable_meta_data(issues, 'Issue')
......@@ -156,6 +177,16 @@ module API
present issues, options
end
desc 'Get statistics for the list of project issues'
params do
use :issues_stats_params
end
get ":id/issues_statistics" do
project = find_project!(params[:id])
present issues_statistics(project_id: project.id), with: Grape::Presenters::Presenter
end
desc 'Get a single project issue' do
success Entities::Issue
end
......
# frozen_string_literal: true
module API
module Validations
class CheckAssigneesCount < Grape::Validations::Base
def self.coerce
lambda do |value|
case value
when String, Array
Array.wrap(value)
else
[]
end
end
end
def validate_param!(attr_name, params)
return if param_allowed?(attr_name, params)
raise Grape::Exceptions::Validation,
params: [@scope.full_name(attr_name)],
message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
end
private
def param_allowed?(attr_name, params)
params[attr_name].size <= 1
end
end
end
end
{
"type": "object",
"required": [
"id",
"name",
"color",
"description",
"text_color"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
},
"description": { "type": ["string", "null"] },
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
}
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe 'GET /groups/:id/issues' do
let!(:group) { create(:group) }
let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
let!(:group_closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: group_project,
state: :closed,
milestone: group_milestone,
updated_at: 3.hours.ago,
created_at: 1.day.ago
end
let!(:group_confidential_issue) do
create :issue,
:confidential,
project: group_project,
author: author,
assignees: [assignee],
updated_at: 2.hours.ago,
created_at: 2.days.ago
end
let!(:group_issue) do
create :issue,
author: user,
assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description,
created_at: 5.days.ago
end
let!(:group_label) do
create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
end
let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
let!(:group_empty_milestone) do
create(:milestone, title: '4.0.0', project: group_project)
end
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
let(:base_url) { "/groups/#{group.id}/issues" }
shared_examples 'group issues statistics' do
it 'returns issues statistics' do
get api("/groups/#{group.id}/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(200)
expect(json_response['statistics']).not_to be_nil
expect(json_response['statistics']['counts']['all']).to eq counts[:all]
expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
end
end
context 'when group has subgroups', :nested_groups do
let(:subgroup_1) { create(:group, parent: group) }
let(:subgroup_2) { create(:group, parent: subgroup_1) }
let(:subgroup_1_project) { create(:project, :public, namespace: subgroup_1) }
let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
let!(:issue_1) { create(:issue, project: subgroup_1_project) }
let!(:issue_2) { create(:issue, project: subgroup_2_project) }
context 'when user is unauthenticated' do
it 'also returns subgroups public projects issues' do
get api(base_url)
expect_paginated_array_response([issue_1.id, group_closed_issue.id, group_issue.id])
end
it 'also returns subgroups public projects issues filtered by milestone' do
get api(base_url), params: { milestone: group_milestone.title }
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: group_milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: group_milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
end
end
context 'when user is a group member' do
before do
group.add_developer(user)
end
it 'also returns subgroups projects issues' do
get api(base_url, user)
expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
it 'also returns subgroups public projects issues filtered by milestone' do
get api(base_url, user), params: { milestone: group_milestone.title }
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 5, closed: 1, opened: 4 } }
it_behaves_like 'group issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 5, closed: 1, opened: 4 } }
it_behaves_like 'group issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 5, closed: 1, opened: 4 } }
it_behaves_like 'group issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 5, closed: 1, opened: 4 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: group_milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: group_milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
end
end
end
context 'when user is unauthenticated' do
it 'lists all issues in public projects' do
get api(base_url)
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
it 'also returns subgroups public projects issues filtered by milestone' do
get api(base_url), params: { milestone: group_milestone.title }
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: group_milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: group_milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
end
end
context 'when user is a group member' do
before do
group_project.add_reporter(user)
end
it 'returns all group issues (including opened and closed)' do
get api(base_url, admin)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
it 'returns group issues without confidential issues for non project members' do
get api(base_url, non_member), params: { state: :opened }
expect_paginated_array_response(group_issue.id)
end
it 'returns group confidential issues for author' do
get api(base_url, author), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group confidential issues for assignee' do
get api(base_url, assignee), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group issues with confidential issues for project members' do
get api(base_url, user), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group confidential issues for admin' do
get api(base_url, admin), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns only confidential issues' do
get api(base_url, user), params: { confidential: true }
expect_paginated_array_response(group_confidential_issue.id)
end
it 'returns only public issues' do
get api(base_url, user), params: { confidential: false }
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
it 'returns an array of labeled group issues' do
get api(base_url, user), params: { labels: group_label.title }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues where all labels match' do
get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
expect_paginated_array_response([])
end
it 'returns an array of labeled group issues where all labels match with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
expect_paginated_array_response([])
end
it 'returns issues matching given search string for title' do
get api(base_url, user), params: { search: group_issue.title }
expect_paginated_array_response(group_issue.id)
end
it 'returns issues matching given search string for description' do
get api(base_url, user), params: { search: group_issue.description }
expect_paginated_array_response(group_issue.id)
end
context 'with labeled issues' do
let(:label_b) { create(:label, title: 'foo', project: group_project) }
let(:label_c) { create(:label, title: 'bar', project: group_project) }
before do
create(:label_link, label: label_b, target: group_issue)
create(:label_link, label: label_c, target: group_issue)
get api(base_url, user), params: params
end
let(:issue) { group_issue }
let(:label) { group_label }
it_behaves_like 'labeled issues with labels and label_name params'
end
it 'returns an array of issues found by iids' do
get api(base_url, user), params: { iids: [group_issue.iid] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api(base_url, user), params: { iids: [0] }
expect_paginated_array_response([])
end
it 'returns an empty array if no group issue matches labels' do
get api(base_url, user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an array of group issues with any label' do
get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with any label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with no label' do
get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an array of group issues with no label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an empty array if no issue matches milestone' do
get api(base_url, user), params: { milestone: group_empty_milestone.title }
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api(base_url, user), params: { milestone: 'foo' }
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
expect_paginated_array_response(group_issue.id)
end
it 'returns an array of issues matching state in milestone' do
get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
expect_paginated_array_response(group_closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api(base_url, user), params: { milestone: no_milestone_title }
expect(response).to have_gitlab_http_status(200)
expect_paginated_array_response(group_confidential_issue.id)
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api(base_url, user)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:group_issue2) do
create :issue,
author: user,
assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description,
created_at: group_issue.created_at
end
it 'page breaks first page correctly' do
get api("#{base_url}?per_page=3", user)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
end
it 'page breaks second page correctly' do
get api("#{base_url}?per_page=3&page=2", user)
expect_paginated_array_response([group_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api("#{base_url}?sort=asc", user)
expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}?order_by=updated_at", user)
group_issue.touch(:updated_at)
expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: group_milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: group_milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: group_milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'group issues statistics'
end
context 'sort does not affect statistics ' do
let(:params) { { state: :opened, order_by: 'updated_at' } }
let(:counts) { { all: 3, closed: 1, opened: 2 } }
it_behaves_like 'group issues statistics'
end
end
context 'filtering by assignee_username' do
let(:another_assignee) { create(:assignee) }
let!(:issue1) { create(:issue, author: user2, project: group_project, created_at: 3.days.ago) }
let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) }
it 'returns issues with by assignee_username' do
get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
it 'returns issues by assignee_username as string' do
get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
it 'returns error when multiple assignees are passed' do
get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("allows one value, but found 2")
end
it 'returns error when assignee_username and assignee_id are passed together' do
get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("mutually exclusive")
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
set(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
shared_examples 'project issues statistics' do
it 'returns project issues statistics' do
get api("/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(200)
expect(json_response['statistics']).not_to be_nil
expect(json_response['statistics']['counts']['all']).to eq counts[:all]
expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
context 'when unauthenticated' do
it 'returns public project issues' do
get api("/projects/#{project.id}/issues")
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'sort does not affect statistics ' do
let(:params) { { state: :opened, order_by: 'updated_at' } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
end
end
it 'avoids N+1 queries' do
get api("/projects/#{project.id}/issues", user)
create_list(:issue, 3, project: project)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/issues", user)
end.count
expect do
get api("/projects/#{project.id}/issues", user)
end.not_to exceed_all_query_limit(control_count)
end
it 'returns 404 when project does not exist' do
get api('/projects/1000/issues', non_member)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 on private projects for other users' do
private_project = create(:project, :private)
create(:issue, project: private_project)
get api("/projects/#{private_project.id}/issues", non_member)
expect(response).to have_gitlab_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
restricted_project = create(:project, :public, :issues_private)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
expect_paginated_array_response([])
end
it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns only confidential issues' do
get api("#{base_url}/issues", author), params: { confidential: true }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns only public issues' do
get api("#{base_url}/issues", author), params: { confidential: false }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns an array of labeled project issues' do
get api("#{base_url}/issues", user), params: { labels: label.title }
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled project issues with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
end
context 'with labeled issues' do
let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) }
before do
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api('/issues', user), params: params
end
it_behaves_like 'labeled issues with labels and label_name params'
end
it 'returns issues matching given search string for title' do
get api("#{base_url}/issues?search=#{issue.title}", user)
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for description' do
get api("#{base_url}/issues?search=#{issue.description}", user)
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues found by iids' do
get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
expect_paginated_array_response(issue.id)
end
it 'returns an empty array if iid does not exist' do
get api("#{base_url}/issues", user), params: { iids: [0] }
expect_paginated_array_response([])
end
it 'returns an empty array if not all labels matches' do
get api("#{base_url}/issues?labels=#{label.title},foo", user)
expect_paginated_array_response([])
end
it 'returns an array of project issues with any label' do
get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with any label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with no label' do
get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an array of project issues with no label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api("#{base_url}/issues", user), params: { milestone: :foo }
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api("#{base_url}/issues", user), params: { milestone: milestone.title }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns an array of issues matching state in milestone' do
get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns an array of issues with any milestone' do
get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:closed_issue2) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: closed_issue.created_at,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
it 'page breaks first page correctly' do
get api("#{base_url}/issues?per_page=3", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
end
it 'page breaks second page correctly' do
get api("#{base_url}/issues?per_page=3&page=2", user)
expect_paginated_array_response([closed_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api("#{base_url}/issues", user), params: { sort: :asc }
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}/issues", user), params: { order_by: :updated_at }
issue.touch(:updated_at)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
context 'sort does not affect statistics ' do
let(:params) { { state: :opened, order_by: 'updated_at' } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'project issues statistics'
end
end
context 'filtering by assignee_username' do
let(:another_assignee) { create(:assignee) }
let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
it 'returns issues by assignee_username' do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
it 'returns issues by assignee_username as string' do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
it 'returns error when multiple assignees are passed' do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("allows one value, but found 2")
end
it 'returns error when assignee_username and assignee_id are passed together' do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("mutually exclusive")
end
end
end
describe 'GET /projects/:id/issues/:issue_iid' do
context 'when unauthenticated' do
it 'returns public issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}")
expect(response).to have_gitlab_http_status(200)
end
end
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
expect(json_response['title']).to eq(issue.title)
expect(json_response['description']).to eq(issue.description)
expect(json_response['state']).to eq(issue.state)
expect(json_response['closed_at']).to be_falsy
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignees']).to be_a Array
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
end
it 'exposes the closed_at attribute' do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['closed_at']).to be_present
end
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
links = json_response['_links']
expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
end
end
it 'returns a project issue by internal id' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
it 'returns 404 if issue id not found' do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue ID is used' do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_gitlab_http_status(404)
end
context 'confidential issues' do
it 'returns 404 for non project members' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 for project members with guest role' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
expect(response).to have_gitlab_http_status(404)
end
it 'returns confidential issue for project members' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it 'returns confidential issue for author' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it 'returns confidential issue for assignee' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it 'returns confidential issue for admin' do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
end
end
describe 'GET :id/issues/:issue_iid/closed_by' do
let(:merge_request) do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "closes #{issue.to_reference}")
end
before do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
context 'when unauthenticated' do
it 'return public project issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
expect_paginated_array_response(merge_request.id)
end
end
it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
expect_paginated_array_response(merge_request.id)
end
context 'when no merge requests will close issue' do
it 'returns empty array' do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
expect_paginated_array_response([])
end
end
it "returns 404 when issue doesn't exists" do
get api("/projects/#{project.id}/issues/0/closed_by", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET :id/issues/:issue_iid/related_merge_requests' do
def get_related_merge_requests(project_id, issue_iid, user = nil)
get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
end
def create_referencing_mr(user, project, issue)
attributes = {
author: user,
source_project: project,
target_project: project,
source_branch: 'master',
target_branch: 'test',
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
end
end
let!(:related_mr) { create_referencing_mr(user, project, issue) }
context 'when unauthenticated' do
it 'return list of referenced merge requests from issue' do
get_related_merge_requests(project.id, issue.iid)
expect_paginated_array_response(related_mr.id)
end
it 'renders 404 if project is not visible' do
private_project = create(:project, :private)
private_issue = create(:issue, project: private_project)
create_referencing_mr(user, private_project, private_issue)
get_related_merge_requests(private_project.id, private_issue.iid)
expect(response).to have_gitlab_http_status(404)
end
end
it 'returns merge requests that mentioned a issue' do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: 'Some description')
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id)
end
it 'returns merge requests cross-project wide' do
project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
merge_request = create_referencing_mr(user, project2, issue)
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response([related_mr.id, merge_request.id])
end
it 'does not generate references to projects with no access' do
private_project = create(:project, :private)
create_referencing_mr(private_project.creator, private_project, issue)
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id)
end
context 'no merge request mentioned a issue' do
it 'returns empty array' do
get_related_merge_requests(project.id, closed_issue.iid, user)
expect_paginated_array_response([])
end
end
it "returns 404 when issue doesn't exists" do
get_related_merge_requests(project.id, 0, user)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
context 'when unauthenticated' do
it 'returns unauthorized' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
expect(response).to have_gitlab_http_status(401)
end
end
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
it 'returns unauthorized for non-admin users' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
end
end
describe 'GET projects/:id/issues/:issue_iid/participants' do
it_behaves_like 'issuable participants endpoint' do
let(:entity) { issue }
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
set(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
shared_examples 'issues statistics' do
it 'returns issues statistics' do
get api("/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(200)
expect(json_response['statistics']).not_to be_nil
expect(json_response['statistics']['counts']['all']).to eq counts[:all]
expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
end
end
describe 'GET /issues' do
context 'when unauthenticated' do
it 'returns an array of all issues' do
get api('/issues'), params: { scope: 'all' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
it 'returns authentication error without any scope' do
get api('/issues')
expect(response).to have_http_status(401)
end
it 'returns authentication error when scope is assigned-to-me' do
get api('/issues'), params: { scope: 'assigned-to-me' }
expect(response).to have_http_status(401)
end
it 'returns authentication error when scope is created-by-me' do
get api('/issues'), params: { scope: 'created-by-me' }
expect(response).to have_http_status(401)
end
it 'returns an array of issues matching state in milestone' do
get api('/issues'), params: { milestone: 'foo', scope: 'all' }
expect(response).to have_http_status(200)
expect_paginated_array_response([])
end
it 'returns an array of issues matching state in milestone' do
get api('/issues'), params: { milestone: milestone.title, scope: 'all' }
expect(response).to have_http_status(200)
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'issues_statistics' do
it 'returns authentication error without any scope' do
get api('/issues_statistics')
expect(response).to have_http_status(401)
end
it 'returns authentication error when scope is assigned_to_me' do
get api('/issues_statistics'), params: { scope: 'assigned_to_me' }
expect(response).to have_http_status(401)
end
it 'returns authentication error when scope is created_by_me' do
get api('/issues_statistics'), params: { scope: 'created_by_me' }
expect(response).to have_http_status(401)
end
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'sort does not affect statistics ' do
let(:params) { { state: :opened, order_by: 'updated_at' } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
end
end
context 'when authenticated' do
it 'returns an array of issues' do
get api('/issues', user)
expect_paginated_array_response([issue.id, closed_issue.id])
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
end
it 'returns an array of closed issues' do
get api('/issues', user), params: { state: :closed }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of opened issues' do
get api('/issues', user), params: { state: :opened }
expect_paginated_array_response(issue.id)
end
it 'returns an array of all issues' do
get api('/issues', user), params: { state: :all }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues assigned to me' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user2), params: { scope: 'assigned_to_me' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues assigned to me (kebab-case)' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user2), params: { scope: 'assigned-to-me' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues authored by the given author id' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues assigned to the given assignee id' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues authored by the given author id and assigned to the given assignee id' do
issue2 = create(:issue, author: user2, assignees: [user2], project: project)
get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with no assignee' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with no assignee' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with any assignee' do
# This issue without assignee should not be returned
create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns only confidential issues' do
get api('/issues', user), params: { confidential: true, scope: 'all' }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns only public issues' do
get api('/issues', user), params: { confidential: false }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
create(:award_emoji, awardable: issue2, user: user2, name: 'star')
create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
expect_paginated_array_response([issue2.id, issue.id])
end
it 'returns issues not reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
create(:award_emoji, awardable: issue2, user: user2, name: 'star')
get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues matching given search string for title' do
get api('/issues', user), params: { search: issue.title }
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for title and scoped in title' do
get api('/issues', user), params: { search: issue.title, in: 'title' }
expect_paginated_array_response(issue.id)
end
it 'returns an empty array if no issue matches given search string for title and scoped in description' do
get api('/issues', user), params: { search: issue.title, in: 'description' }
expect_paginated_array_response([])
end
it 'returns issues matching given search string for description' do
get api('/issues', user), params: { search: issue.description }
expect_paginated_array_response(issue.id)
end
context 'filtering before a specific date' do
let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
it 'returns issues created before a specific date' do
get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
expect_paginated_array_response(issue2.id)
end
it 'returns issues updated before a specific date' do
get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
expect_paginated_array_response(issue2.id)
end
end
context 'filtering after a specific date' do
let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
it 'returns issues created after a specific date' do
get api("/issues?created_after=#{issue2.created_at}", user)
expect_paginated_array_response(issue2.id)
end
it 'returns issues updated after a specific date' do
get api("/issues?updated_after=#{issue2.updated_at}", user)
expect_paginated_array_response(issue2.id)
end
end
context 'filter by labels or label_name param' do
context 'N+1' do
let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) }
before do
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
end
it 'tests N+1' do
control = ActiveRecord::QueryRecorder.new do
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
end
label_d = create(:label, title: 'dar', project: project)
label_e = create(:label, title: 'ear', project: project)
create(:label_link, label: label_d, target: issue)
create(:label_link, label: label_e, target: issue)
expect do
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
end.not_to exceed_query_limit(control)
expect(issue.labels.count).to eq(5)
end
end
it 'returns an array of labeled issues' do
get api('/issues', user), params: { labels: label.title }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled issues with labels param as array' do
get api('/issues', user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
end
context 'with labeled issues' do
let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) }
before do
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api('/issues', user), params: params
end
it_behaves_like 'labeled issues with labels and label_name params'
end
it 'returns an empty array if no issue matches labels' do
get api('/issues', user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an empty array if no issue matches labels with labels param as array' do
get api('/issues', user), params: { labels: %w(foo bar) }
expect_paginated_array_response([])
end
it 'returns an array of labeled issues matching given state' do
get api('/issues', user), params: { labels: label.title, state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an array of labeled issues matching given state with labels param as array' do
get api('/issues', user), params: { labels: [label.title], state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an empty array if no issue matches labels and state filters' do
get api('/issues', user), params: { labels: label.title, state: :closed }
expect_paginated_array_response([])
end
it 'returns an array of issues with any label' do
get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with any label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with no label' do
get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter' do
get api('/issues', user), params: { labels: 'No Label' }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
get api('/issues', user), params: { labels: ['No Label'] }
expect_paginated_array_response(closed_issue.id)
end
end
it 'returns an empty array if no issue matches milestone' do
get api("/issues?milestone=#{empty_milestone.title}", user)
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api('/issues?milestone=foo', user)
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api("/issues?milestone=#{milestone.title}", user)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns an array of issues in given milestone_title param' do
get api("/issues?milestone_title=#{milestone.title}", user)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns an array of issues matching state in milestone' do
get api("/issues?milestone=#{milestone.title}&state=closed", user)
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("/issues?milestone=#{no_milestone_title}", author)
expect_paginated_array_response(confidential_issue.id)
end
it 'returns an array of issues with no milestone using milestone_title param' do
get api("/issues?milestone_title=#{no_milestone_title}", author)
expect_paginated_array_response(confidential_issue.id)
end
it 'returns an array of issues found by iids' do
get api('/issues', user), params: { iids: [closed_issue.iid] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api('/issues', user), params: { iids: [0] }
expect_paginated_array_response([])
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api('/issues', user)
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:closed_issue2) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: closed_issue.created_at,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
it 'page breaks first page correctly' do
get api('/issues?per_page=2', user)
expect_paginated_array_response([issue.id, closed_issue2.id])
end
it 'page breaks second page correctly' do
get api('/issues?per_page=2&page=2', user)
expect_paginated_array_response([closed_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api('/issues?sort=asc', user)
expect_paginated_array_response([closed_issue.id, issue.id])
end
it 'sorts by updated_at descending when requested' do
get api('/issues?order_by=updated_at', user)
issue.touch(:updated_at)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api('/issues?order_by=updated_at&sort=asc', user)
issue.touch(:updated_at)
expect_paginated_array_response([closed_issue.id, issue.id])
end
it 'matches V4 response schema' do
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
it 'returns a related merge request count of 0 if there are no related merge requests' do
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
expect(json_response.first).to include('merge_requests_count' => 0)
end
it 'returns a related merge request count > 0 if there are related merge requests' do
create(:merge_requests_closing_issues, issue: issue)
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
expect(json_response.first).to include('merge_requests_count' => 1)
end
context 'issues_statistics' do
context 'no state is treated as all state' do
let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'statistics when all state is passed' do
let(:params) { { state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'closed state is treated as all state' do
let(:params) { { state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'opened state is treated as all state' do
let(:params) { { state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and no state treated as all state' do
let(:params) { { milestone: milestone.title } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and all state' do
let(:params) { { milestone: milestone.title, state: :all } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and closed state treated as all state' do
let(:params) { { milestone: milestone.title, state: :closed } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'when filtering by milestone and opened state treated as all state' do
let(:params) { { milestone: milestone.title, state: :opened } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
context 'sort does not affect statistics ' do
let(:params) { { state: :opened, order_by: 'updated_at' } }
let(:counts) { { all: 2, closed: 1, opened: 1 } }
it_behaves_like 'issues statistics'
end
end
context 'filtering by assignee_username' do
let(:another_assignee) { create(:assignee) }
let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
it 'returns issues with by assignee_username' do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
it 'returns issues by assignee_username as string' do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
it 'returns error when multiple assignees are passed' do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("allows one value, but found 2")
end
it 'returns error when assignee_username and assignee_id are passed together' do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to include("mutually exclusive")
end
end
end
end
describe 'DELETE /projects/:id/issues/:issue_iid' do
it 'rejects a non member from deleting an issue' do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
expect(response).to have_gitlab_http_status(403)
end
it 'rejects a developer from deleting an issue' do
delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
expect(response).to have_gitlab_http_status(403)
end
context 'when the user is project owner' do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace) }
it 'deletes the issue if an admin requests it' do
delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
end
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
delete api("/projects/#{project.id}/issues/123", user)
expect(response).to have_gitlab_http_status(404)
end
end
it 'returns 404 when using the issue ID instead of IID' do
delete api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'time tracking endpoints' do
let(:issuable) { issue }
include_examples 'time tracking endpoints', 'issue'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
set(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe 'POST /projects/:id/issues' do
context 'support for deprecated assignee_id' do
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: user2.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new project issue when assignee_id is empty' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: '' }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']).to be_nil
end
end
context 'single assignee restrictions' do
it 'creates a new project issue with no more than one assignee' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignees'].count).to eq(1)
end
end
context 'user does not have permissions to create issue' do
let(:not_member) { create(:user) }
before do
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
end
it 'renders 403' do
post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(403)
end
end
context 'an internal ID is provided' do
context 'by an admin' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", admin),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by an owner' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by a group owner' do
let(:group) { create(:group) }
let(:group_project) { create(:project, :public, namespace: group) }
it 'sets the internal ID on the new issue' do
group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by another user' do
it 'ignores the given internal ID' do
post api("/projects/#{project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).not_to eq 9001
end
end
end
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new project issue with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new confidential project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: true }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a new confidential project issue with a different param' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'y' }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a public issue when confidential param is false' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: false }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
it 'creates a public issue when confidential param is invalid' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'foo' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
it 'returns a 400 bad request if title not given' do
post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
}
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
context 'resolving discussions' do
let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
project.add_maintainer(user)
end
context 'resolving all discussions in a merge request' do
before do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'New Issue',
merge_request_to_resolve_discussions_of: merge_request.iid
}
end
it_behaves_like 'creating an issue resolving discussions through the API'
end
context 'resolving a single discussion' do
before do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'New Issue',
merge_request_to_resolve_discussions_of: merge_request.iid,
discussion_to_resolve: discussion.id
}
end
it_behaves_like 'creating an issue resolving discussions through the API'
end
end
context 'with due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', due_date: due_date }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['due_date']).to eq(due_date)
end
end
context 'setting created_at' do
let(:creation_time) { 2.weeks.ago }
let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
context 'by an admin' do
it 'sets the creation time on the new issue' do
post api("/projects/#{project.id}/issues", admin), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'by a project owner' do
it 'sets the creation time on the new issue' do
post api("/projects/#{project.id}/issues", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'by a group owner' do
it 'sets the creation time on the new issue' do
group = create(:group)
group_project = create(:project, :public, namespace: group)
group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'by another user' do
it 'ignores the given creation time' do
post api("/projects/#{project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
end
end
end
context 'the user can only read the issue' do
it 'cannot create new labels' do
expect do
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
end.not_to change { project.labels.count }
end
it 'cannot create new labels with labels param as array' do
expect do
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
end.not_to change { project.labels.count }
end
end
end
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
end
let(:params) do
{
title: 'new issue',
description: 'content here',
labels: 'label, label2'
}
end
it 'does not create a new project issue' do
expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('new issue')
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
end
end
describe '/projects/:id/issues/:issue_iid/move' do
let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: project.id }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
it 'moves the issue to another namespace if I am admin' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
context 'when using the issue ID instead of iid' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/123/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/0/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: 0 }
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue ID is used instead of the iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/unsubscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if using the issue ID instead of iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
set(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
it 'updates a project issue' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'returns 404 error if issue iid not found' do
put api("/projects/#{project.id}/issues/44444", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 error if issue id is used instead of the iid' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(404)
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: {
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: {
title: 'updated title',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'confidential issues' do
it 'returns 403 for non project members' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(403)
end
it 'returns 403 for project members with guest role' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(403)
end
it 'updates a confidential issue for project members' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'updates a confidential issue for author' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'updates a confidential issue for admin' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'sets an issue to confidential' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { confidential: true }
expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_truthy
end
it 'makes a confidential issue public' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { confidential: false }
expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_falsy
end
it 'does not update a confidential issue with wrong confidential flag' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { confidential: 'foo' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
let(:params) do
{
title: 'updated title',
description: 'content here',
labels: 'label, label2'
}
end
it 'does not create a new project issue' do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('updated title')
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_id: 0 }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']).to be_nil
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_id: user2.id }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [0] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees']).to be_empty
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
context 'single assignee restrictions' do
it 'updates an issue with several assignees but only one has been applied' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [user2.id, guest.id] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].size).to eq(1)
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'removes all labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'foo,bar' }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: %w(foo bar) }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
it 'updates a project issue' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label2', state_event: 'close' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq 'closed'
end
it 'reopens a project isssue' do
put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq 'opened'
end
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label3', state_event: 'close', updated_at: update_time }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
end
require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
set(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignees: [assignee],
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { "None" }
let(:any_milestone_title) { "Any" }
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
end
describe "GET /issues" do
context "when unauthenticated" do
it "returns an array of all issues" do
get api("/issues"), params: { scope: 'all' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
it "returns authentication error without any scope" do
get api("/issues")
expect(response).to have_http_status(401)
end
it "returns authentication error when scope is assigned-to-me" do
get api("/issues"), params: { scope: 'assigned-to-me' }
expect(response).to have_http_status(401)
end
it "returns authentication error when scope is created-by-me" do
get api("/issues"), params: { scope: 'created-by-me' }
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "returns an array of issues" do
get api("/issues", user)
expect_paginated_array_response([issue.id, closed_issue.id])
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
end
it 'returns an array of closed issues' do
get api('/issues', user), params: { state: :closed }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of opened issues' do
get api('/issues', user), params: { state: :opened }
expect_paginated_array_response(issue.id)
end
it 'returns an array of all issues' do
get api('/issues', user), params: { state: :all }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues assigned to me' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user2), params: { scope: 'assigned_to_me' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues assigned to me (kebab-case)' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user2), params: { scope: 'assigned-to-me' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues authored by the given author id' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues assigned to the given assignee id' do
issue2 = create(:issue, assignees: [user2], project: project)
get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues authored by the given author id and assigned to the given assignee id' do
issue2 = create(:issue, author: user2, assignees: [user2], project: project)
get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with no assignee' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with no assignee' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
expect_paginated_array_response(issue2.id)
end
it 'returns issues with any assignee' do
# This issue without assignee should not be returned
create(:issue, author: user2, project: project)
get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns only confidential issues' do
get api('/issues', user), params: { confidential: true, scope: 'all' }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns only public issues' do
get api('/issues', user), params: { confidential: false }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
create(:award_emoji, awardable: issue2, user: user2, name: 'star')
create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
expect_paginated_array_response([issue2.id, issue.id])
end
it 'returns issues not reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
create(:award_emoji, awardable: issue2, user: user2, name: 'star')
get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns issues matching given search string for title' do
get api("/issues", user), params: { search: issue.title }
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for title and scoped in title' do
get api("/issues", user), params: { search: issue.title, in: 'title' }
expect_paginated_array_response(issue.id)
end
it 'returns an empty array if no issue matches given search string for title and scoped in description' do
get api("/issues", user), params: { search: issue.title, in: 'description' }
expect_paginated_array_response([])
end
it 'returns issues matching given search string for description' do
get api("/issues", user), params: { search: issue.description }
expect_paginated_array_response(issue.id)
end
context 'filtering before a specific date' do
let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
it 'returns issues created before a specific date' do
get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
expect_paginated_array_response(issue2.id)
end
it 'returns issues updated before a specific date' do
get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
expect_paginated_array_response(issue2.id)
end
end
context 'filtering after a specific date' do
let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
it 'returns issues created after a specific date' do
get api("/issues?created_after=#{issue2.created_at}", user)
expect_paginated_array_response(issue2.id)
end
it 'returns issues updated after a specific date' do
get api("/issues?updated_after=#{issue2.updated_at}", user)
expect_paginated_array_response(issue2.id)
end
end
it 'returns an array of labeled issues' do
get api('/issues', user), params: { labels: label.title }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled issues with labels param as array' do
get api('/issues', user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled issues when all labels matches' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api('/issues', user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an empty array if no issue matches labels' do
get api('/issues', user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an empty array if no issue matches labels with labels param as array' do
get api('/issues', user), params: { labels: %w(foo bar) }
expect_paginated_array_response([])
end
it 'returns an array of labeled issues matching given state' do
get api('/issues', user), params: { labels: label.title, state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an array of labeled issues matching given state with labels param as array' do
get api('/issues', user), params: { labels: [label.title], state: :opened }
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an empty array if no issue matches labels and state filters' do
get api('/issues', user), params: { labels: label.title, state: :closed }
expect_paginated_array_response([])
end
it 'returns an array of issues with any label' do
get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with any label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues with no label' do
get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label with labels param as array' do
get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter' do
get api('/issues', user), params: { labels: 'No Label' }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
get api('/issues', user), params: { labels: ['No Label'] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an empty array if no issue matches milestone' do
get api("/issues?milestone=#{empty_milestone.title}", user)
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api("/issues?milestone=foo", user)
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api("/issues?milestone=#{milestone.title}", user)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns an array of issues matching state in milestone' do
get api("/issues?milestone=#{milestone.title}"\
'&state=closed', user)
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("/issues?milestone=#{no_milestone_title}", author)
expect_paginated_array_response(confidential_issue.id)
end
it 'returns an array of issues found by iids' do
get api('/issues', user), params: { iids: [closed_issue.iid] }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api("/issues", user), params: { iids: [0] }
expect_paginated_array_response([])
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api('/issues', user)
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:closed_issue2) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: closed_issue.created_at,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
it 'page breaks first page correctly' do
get api('/issues?per_page=2', user)
expect_paginated_array_response([issue.id, closed_issue2.id])
end
it 'page breaks second page correctly' do
get api('/issues?per_page=2&page=2', user)
expect_paginated_array_response([closed_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api('/issues?sort=asc', user)
expect_paginated_array_response([closed_issue.id, issue.id])
end
it 'sorts by updated_at descending when requested' do
get api('/issues?order_by=updated_at', user)
issue.touch(:updated_at)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api('/issues?order_by=updated_at&sort=asc', user)
issue.touch(:updated_at)
expect_paginated_array_response([closed_issue.id, issue.id])
end
it 'matches V4 response schema' do
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
it 'returns a related merge request count of 0 if there are no related merge requests' do
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
expect(json_response.first).to include('merge_requests_count' => 0)
end
it 'returns a related merge request count > 0 if there are related merge requests' do
create(:merge_requests_closing_issues, issue: issue)
get api('/issues', user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
expect(json_response.first).to include('merge_requests_count' => 1)
end
end
end
describe "GET /groups/:id/issues" do
let!(:group) { create(:group) }
let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
let!(:group_closed_issue) do
create :closed_issue,
author: user,
assignees: [user],
project: group_project,
state: :closed,
milestone: group_milestone,
updated_at: 3.hours.ago,
created_at: 1.day.ago
end
let!(:group_confidential_issue) do
create :issue,
:confidential,
project: group_project,
author: author,
assignees: [assignee],
updated_at: 2.hours.ago,
created_at: 2.days.ago
end
let!(:group_issue) do
create :issue,
author: user,
assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description,
created_at: 5.days.ago
end
let!(:group_label) do
create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
end
let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
let!(:group_empty_milestone) do
create(:milestone, title: '4.0.0', project: group_project)
end
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
let(:base_url) { "/groups/#{group.id}/issues" }
context 'when group has subgroups', :nested_groups do
let(:subgroup_1) { create(:group, parent: group) }
let(:subgroup_2) { create(:group, parent: subgroup_1) }
let(:subgroup_1_project) { create(:project, namespace: subgroup_1) }
let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
let!(:issue_1) { create(:issue, project: subgroup_1_project) }
let!(:issue_2) { create(:issue, project: subgroup_2_project) }
before do
group.add_developer(user)
end
it 'also returns subgroups projects issues' do
get api(base_url, user)
expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
end
context 'when user is unauthenticated' do
it 'lists all issues in public projects' do
get api(base_url)
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
end
context 'when user is a group member' do
before do
group_project.add_reporter(user)
end
it 'returns all group issues (including opened and closed)' do
get api(base_url, admin)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
it 'returns group issues without confidential issues for non project members' do
get api(base_url, non_member), params: { state: :opened }
expect_paginated_array_response(group_issue.id)
end
it 'returns group confidential issues for author' do
get api(base_url, author), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group confidential issues for assignee' do
get api(base_url, assignee), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group issues with confidential issues for project members' do
get api(base_url, user), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns group confidential issues for admin' do
get api(base_url, admin), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
it 'returns only confidential issues' do
get api(base_url, user), params: { confidential: true }
expect_paginated_array_response(group_confidential_issue.id)
end
it 'returns only public issues' do
get api(base_url, user), params: { confidential: false }
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
it 'returns an array of labeled group issues' do
get api(base_url, user), params: { labels: group_label.title }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues where all labels match' do
get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
expect_paginated_array_response([])
end
it 'returns an array of labeled group issues where all labels match with labels param as array' do
get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
expect_paginated_array_response([])
end
it 'returns issues matching given search string for title' do
get api(base_url, user), params: { search: group_issue.title }
expect_paginated_array_response(group_issue.id)
end
it 'returns issues matching given search string for description' do
get api(base_url, user), params: { search: group_issue.description }
expect_paginated_array_response(group_issue.id)
end
it 'returns an array of labeled issues when all labels matches' do
label_b = create(:label, title: 'foo', project: group_project)
label_c = create(:label, title: 'bar', project: group_project)
create(:label_link, label: label_b, target: group_issue)
create(:label_link, label: label_c, target: group_issue)
get api(base_url, user), params: { labels: "#{group_label.title},#{label_b.title},#{label_c.title}" }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: group_project)
label_c = create(:label, title: 'bar', project: group_project)
create(:label_link, label: label_b, target: group_issue)
create(:label_link, label: label_c, target: group_issue)
get api(base_url, user), params: { labels: [group_label.title, label_b.title, label_c.title] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
it 'returns an array of issues found by iids' do
get api(base_url, user), params: { iids: [group_issue.iid] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api(base_url, user), params: { iids: [0] }
expect_paginated_array_response([])
end
it 'returns an empty array if no group issue matches labels' do
get api(base_url, user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an array of group issues with any label' do
get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with any label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of group issues with no label' do
get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an array of group issues with no label with labels param as array' do
get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
end
it 'returns an empty array if no issue matches milestone' do
get api(base_url, user), params: { milestone: group_empty_milestone.title }
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api(base_url, user), params: { milestone: 'foo' }
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
expect_paginated_array_response(group_issue.id)
end
it 'returns an array of issues matching state in milestone' do
get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
expect_paginated_array_response(group_closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api(base_url, user), params: { milestone: no_milestone_title }
expect(response).to have_gitlab_http_status(200)
expect_paginated_array_response(group_confidential_issue.id)
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api(base_url, user)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:group_issue2) do
create :issue,
author: user,
assignees: [user],
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description,
created_at: group_issue.created_at
end
it 'page breaks first page correctly' do
get api("#{base_url}?per_page=3", user)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
end
it 'page breaks second page correctly' do
get api("#{base_url}?per_page=3&page=2", user)
expect_paginated_array_response([group_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api("#{base_url}?sort=asc", user)
expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}?order_by=updated_at", user)
group_issue.touch(:updated_at)
expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
context 'when unauthenticated' do
it 'returns public project issues' do
get api("/projects/#{project.id}/issues")
expect_paginated_array_response([issue.id, closed_issue.id])
end
end
it 'avoids N+1 queries' do
get api("/projects/#{project.id}/issues", user)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{project.id}/issues", user)
end.count
create_list(:issue, 3, project: project)
expect do
get api("/projects/#{project.id}/issues", user)
end.not_to exceed_all_query_limit(control_count)
end
it 'returns 404 when project does not exist' do
get api('/projects/1000/issues', non_member)
expect(response).to have_gitlab_http_status(404)
end
it "returns 404 on private projects for other users" do
private_project = create(:project, :private)
create(:issue, project: private_project)
get api("/projects/#{private_project.id}/issues", non_member)
expect(response).to have_gitlab_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
restricted_project = create(:project, :public, :issues_private)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
expect_paginated_array_response([])
end
it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns only confidential issues' do
get api("#{base_url}/issues", author), params: { confidential: true }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns only public issues' do
get api("#{base_url}/issues", author), params: { confidential: false }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'returns an array of labeled project issues' do
get api("#{base_url}/issues", user), params: { labels: label.title }
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled project issues with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [label.title] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled issues when all labels matches' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api("#{base_url}/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
expect_paginated_array_response(issue.id)
end
it 'returns an array of labeled issues when all labels matches with labels param as array' do
label_b = create(:label, title: 'foo', project: project)
label_c = create(:label, title: 'bar', project: project)
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
get api("#{base_url}/issues", user), params: { labels: [label.title, label_b.title, label_c.title] }
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for title' do
get api("#{base_url}/issues?search=#{issue.title}", user)
expect_paginated_array_response(issue.id)
end
it 'returns issues matching given search string for description' do
get api("#{base_url}/issues?search=#{issue.description}", user)
expect_paginated_array_response(issue.id)
end
it 'returns an array of issues found by iids' do
get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
expect_paginated_array_response(issue.id)
end
it 'returns an empty array if iid does not exist' do
get api("#{base_url}/issues", user), params: { iids: [0] }
expect_paginated_array_response([])
end
it 'returns an empty array if not all labels matches' do
get api("#{base_url}/issues?labels=#{label.title},foo", user)
expect_paginated_array_response([])
end
it 'returns an array of project issues with any label' do
get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with any label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
expect_paginated_array_response(issue.id)
end
it 'returns an array of project issues with no label' do
get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an array of project issues with no label with labels param as array' do
get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
expect_paginated_array_response([confidential_issue.id, closed_issue.id])
end
it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
expect_paginated_array_response([])
end
it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
expect_paginated_array_response([])
end
it 'returns an empty array if milestone does not exist' do
get api("#{base_url}/issues", user), params: { milestone: :foo }
expect_paginated_array_response([])
end
it 'returns an array of issues in given milestone' do
get api("#{base_url}/issues", user), params: { milestone: milestone.title }
expect_paginated_array_response([issue.id, closed_issue.id])
end
it 'returns an array of issues matching state in milestone' do
get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
expect_paginated_array_response(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
expect_paginated_array_response(confidential_issue.id)
end
it 'returns an array of issues with any milestone' do
get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
expect_paginated_array_response([issue.id, closed_issue.id])
end
context 'without sort params' do
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
context 'with 2 issues with same created_at' do
let!(:closed_issue2) do
create :closed_issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: closed_issue.created_at,
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
it 'page breaks first page correctly' do
get api("#{base_url}/issues?per_page=3", user)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
end
it 'page breaks second page correctly' do
get api("#{base_url}/issues?per_page=3&page=2", user)
expect_paginated_array_response([closed_issue.id])
end
end
end
it 'sorts ascending when requested' do
get api("#{base_url}/issues", user), params: { sort: :asc }
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
it 'sorts by updated_at descending when requested' do
get api("#{base_url}/issues", user), params: { order_by: :updated_at }
issue.touch(:updated_at)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
it 'sorts by updated_at ascending when requested' do
get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
end
describe "GET /projects/:id/issues/:issue_iid" do
context 'when unauthenticated' do
it 'returns public issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}")
expect(response).to have_gitlab_http_status(200)
end
end
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
expect(json_response['title']).to eq(issue.title)
expect(json_response['description']).to eq(issue.description)
expect(json_response['state']).to eq(issue.state)
expect(json_response['closed_at']).to be_falsy
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignees']).to be_a Array
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
end
it "exposes the 'closed_at' attribute" do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['closed_at']).to be_present
end
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
links = json_response['_links']
expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
end
end
it "returns a project issue by internal id" do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the issue ID is used" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_gitlab_http_status(404)
end
context 'confidential issues' do
it "returns 404 for non project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
expect(response).to have_gitlab_http_status(404)
end
it "returns 404 for project members with guest role" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
expect(response).to have_gitlab_http_status(404)
end
it "returns confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for author" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for assignee" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for admin" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
end
end
describe "POST /projects/:id/issues" do
context 'support for deprecated assignee_id' do
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: user2.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new project issue when assignee_id is empty' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: '' }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']).to be_nil
end
end
context 'single assignee restrictions' do
it 'creates a new project issue with no more than one assignee' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignees'].count).to eq(1)
end
end
context 'user does not have permissions to create issue' do
let(:not_member) { create(:user) }
before do
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
end
it 'renders 403' do
post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(403)
end
end
context 'an internal ID is provided' do
context 'by an admin' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", admin),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by an owner' do
it 'sets the internal ID on the new issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by a group owner' do
let(:group) { create(:group) }
let(:group_project) { create(:project, :public, namespace: group) }
it 'sets the internal ID on the new issue' do
group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).to eq 9001
end
end
context 'by another user' do
it 'ignores the given internal ID' do
post api("/projects/#{project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(201)
expect(json_response['iid']).not_to eq 9001
end
end
end
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new project issue with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new confidential project issue' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: true }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a new confidential project issue with a different param' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'y' }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a public issue when confidential param is false' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: false }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
it 'creates a public issue when confidential param is invalid' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'foo' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
it "returns a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
}
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
context 'resolving discussions' do
let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
project.add_maintainer(user)
end
context 'resolving all discussions in a merge request' do
before do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'New Issue',
merge_request_to_resolve_discussions_of: merge_request.iid
}
end
it_behaves_like 'creating an issue resolving discussions through the API'
end
context 'resolving a single discussion' do
before do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'New Issue',
merge_request_to_resolve_discussions_of: merge_request.iid,
discussion_to_resolve: discussion.id
}
end
it_behaves_like 'creating an issue resolving discussions through the API'
end
end
context 'with due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', due_date: due_date }
expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['due_date']).to eq(due_date)
end
end
context 'setting created_at' do
let(:creation_time) { 2.weeks.ago }
let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
context 'by an admin' do
before do
post api("/projects/#{project.id}/issues", admin), params: params
end
it 'sets the creation time on the new issue' do
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
it 'sets the system notes timestamp based on creation time' do
issue = Issue.find(json_response['id'])
expect(issue.resource_label_events.last.created_at).to be_like_time(creation_time)
end
end
context 'by a project owner' do
it 'sets the creation time on the new issue' do
post api("/projects/#{project.id}/issues", user), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'by a group owner' do
it 'sets the creation time on the new issue' do
group = create(:group)
group_project = create(:project, :public, namespace: group)
group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'by another user' do
it 'ignores the given creation time' do
post api("/projects/#{project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
end
end
end
context 'the user can only read the issue' do
it 'cannot create new labels' do
expect do
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
end.not_to change { project.labels.count }
end
it 'cannot create new labels with labels param as array' do
expect do
post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
end.not_to change { project.labels.count }
end
end
end
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
end
let(:params) do
{
title: 'new issue',
description: 'content here',
labels: 'label, label2'
}
end
it "does not create a new project issue" do
expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('new issue')
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
end
end
describe "PUT /projects/:id/issues/:issue_iid to update only title" do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "returns 404 error if issue iid not found" do
put api("/projects/#{project.id}/issues/44444", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(404)
end
it "returns 404 error if issue id is used instead of the iid" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(404)
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: {
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: {
title: 'updated title',
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'confidential issues' do
it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(403)
end
it "returns 403 for project members with guest role" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(403)
end
it "updates a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for author" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for admin" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'sets an issue to confidential' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { confidential: true }
expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_truthy
end
it 'makes a confidential issue public' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { confidential: false }
expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_falsy
end
it 'does not update a confidential issue with wrong confidential flag' do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
params: { confidential: 'foo' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
let(:params) do
{
title: 'updated title',
description: 'content here',
labels: 'label, label2'
}
end
it "does not create a new project issue" do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('updated title')
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_id: 0 }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']).to be_nil
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_id: user2.id }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [0] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees']).to be_empty
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
context 'single assignee restrictions' do
it 'updates an issue with several assignees but only one has been applied' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { assignee_ids: [user2.id, guest.id] }
expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].size).to eq(1)
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'updated title' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'removes all labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'foo,bar' }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: %w(foo bar) }
end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
end
describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label2', state_event: "close" }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
end
it 'reopens a project isssue' do
put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq 'opened'
end
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { labels: 'label3', state_event: 'close', updated_at: update_time }
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
describe "DELETE /projects/:id/issues/:issue_iid" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
expect(response).to have_gitlab_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
expect(response).to have_gitlab_http_status(403)
end
context "when the user is project owner" do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
end
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
delete api("/projects/#{project.id}/issues/123", user)
expect(response).to have_gitlab_http_status(404)
end
end
it 'returns 404 when using the issue ID instead of IID' do
delete api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe '/projects/:id/issues/:issue_iid/move' do
let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: project.id }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
it 'moves the issue to another namespace if I am admin' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
context 'when using the issue ID instead of iid' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/123/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/0/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: 0 }
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue ID is used instead of the iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/unsubscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if using the issue ID instead of iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'time tracking endpoints' do
let(:issuable) { issue }
include_examples 'time tracking endpoints', 'issue'
end
describe 'GET :id/issues/:issue_iid/closed_by' do
let(:merge_request) do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "closes #{issue.to_reference}")
end
before do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
context 'when unauthenticated' do
it 'return public project issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
expect_paginated_array_response(merge_request.id)
end
end
it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
expect_paginated_array_response(merge_request.id)
end
context 'when no merge requests will close issue' do
it 'returns empty array' do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
expect_paginated_array_response([])
end
end
it "returns 404 when issue doesn't exists" do
get api("/projects/#{project.id}/issues/0/closed_by", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET :id/issues/:issue_iid/related_merge_requests' do
def get_related_merge_requests(project_id, issue_iid, user = nil)
get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
end
def create_referencing_mr(user, project, issue)
attributes = {
author: user,
source_project: project,
target_project: project,
source_branch: "master",
target_branch: "test",
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
end
end
let!(:related_mr) { create_referencing_mr(user, project, issue) }
context 'when unauthenticated' do
it 'return list of referenced merge requests from issue' do
get_related_merge_requests(project.id, issue.iid)
expect_paginated_array_response(related_mr.id)
end
it 'renders 404 if project is not visible' do
private_project = create(:project, :private)
private_issue = create(:issue, project: private_project)
create_referencing_mr(user, private_project, private_issue)
get_related_merge_requests(private_project.id, private_issue.iid)
expect(response).to have_gitlab_http_status(404)
end
end
it 'returns merge requests that mentioned a issue' do
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "Some description")
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id)
end
it 'returns merge requests cross-project wide' do
project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
merge_request = create_referencing_mr(user, project2, issue)
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response([related_mr.id, merge_request.id])
end
it 'does not generate references to projects with no access' do
private_project = create(:project, :private)
create_referencing_mr(private_project.creator, private_project, issue)
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id)
end
context 'merge request closes an issue' do
let!(:closing_issue_mr_rel) do
create(:merge_requests_closing_issues, issue: issue, merge_request: related_mr)
end
it 'returns closing MR only once' do
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response([related_mr.id])
end
end
context 'no merge request mentioned a issue' do
it 'returns empty array' do
get_related_merge_requests(project.id, closed_issue.iid, user)
expect_paginated_array_response([])
end
end
it "returns 404 when issue doesn't exists" do
get_related_merge_requests(project.id, 0, user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
context 'when unauthenticated' do
it "returns unauthorized" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
expect(response).to have_gitlab_http_status(401)
end
end
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
it "returns unauthorized for non-admin users" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
end
end
describe 'GET projects/:id/issues/:issue_iid/participants' do
it_behaves_like 'issuable participants endpoint' do
let(:entity) { issue }
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
end
# frozen_string_literal: true
shared_examples 'labeled issues with labels and label_name params' do
shared_examples 'returns label names' do
it 'returns label names' do
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
end
shared_examples 'returns basic label entity' do
it 'returns basic label entity' do
expect_paginated_array_response(issue.id)
expect(json_response.first['labels'].pluck('name')).to eq([label_c.title, label_b.title, label.title])
expect(json_response.first['labels'].first).to match_schema('/public_api/v4/label_basic')
end
end
context 'array of labeled issues when all labels match' do
let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}" } }
it_behaves_like 'returns label names'
end
context 'array of labeled issues when all labels match with labels param as array' do
let(:params) { { labels: [label.title, label_b.title, label_c.title] } }
it_behaves_like 'returns label names'
end
context 'when with_labels_details provided' do
context 'array of labeled issues when all labels match' do
let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: true } }
it_behaves_like 'returns basic label entity'
end
context 'array of labeled issues when all labels match with labels param as array' do
let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_details: true } }
it_behaves_like 'returns basic label entity'
end
end
end
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