API: Implement project issues iid param with IssuesFinder and add tests
- Use IssuesFinder for the /issues API resouce - Tests for iid filter in project issues API resource - Tests for No Milestone filter in issues API resources The "No Milestone" case was not previously tested, and the `/issues` resource did not support the the `milestone` parameter. - Return issues where all labels match from the issues and project issues API resources, like the group issues resource already does. See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6825#note_17474533 Signed-off-by: Rémy Coutable <remy@rymai.me>
This commit is contained in:
parent
5c253116ae
commit
7ef1c6408e
4 changed files with 110 additions and 24 deletions
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: 'API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`'
|
||||||
|
merge_request: 8457
|
||||||
|
author: Panagiotis Atmatzidis, David Eisner
|
|
@ -23,12 +23,15 @@ GET /issues?state=closed
|
||||||
GET /issues?labels=foo
|
GET /issues?labels=foo
|
||||||
GET /issues?labels=foo,bar
|
GET /issues?labels=foo,bar
|
||||||
GET /issues?labels=foo,bar&state=opened
|
GET /issues?labels=foo,bar&state=opened
|
||||||
|
GET /issues?milestone=1.0.0
|
||||||
|
GET /issues?milestone=1.0.0&state=opened
|
||||||
```
|
```
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| 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 with any of the labels will be returned |
|
| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
|
||||||
|
| `milestone` | string| no | The milestone title |
|
||||||
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
|
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
|
||||||
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
|
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,31 @@ module API
|
||||||
before { authenticate! }
|
before { authenticate! }
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
# TODO: Remove in 9.0 and switch to IssueFinder-based label filtering
|
def find_issues(args = {})
|
||||||
def filter_issues_labels(issues, labels)
|
args = params.merge(args)
|
||||||
issues.includes(:labels).where('labels.title' => labels.split(','))
|
|
||||||
|
args.delete(:id)
|
||||||
|
args[:milestone_title] = args.delete(:milestone)
|
||||||
|
|
||||||
|
match_all_labels = args.delete(:match_all_labels)
|
||||||
|
labels = args.delete(:labels)
|
||||||
|
args[:label_name] = labels if match_all_labels
|
||||||
|
|
||||||
|
args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
|
||||||
|
|
||||||
|
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
|
||||||
|
|
||||||
|
# TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
|
||||||
|
if !match_all_labels && labels.present?
|
||||||
|
issues = issues.includes(:labels).where('labels.title' => labels.split(','))
|
||||||
|
end
|
||||||
|
|
||||||
|
issues.reorder(args[:order_by] => args[:sort])
|
||||||
end
|
end
|
||||||
|
|
||||||
params :issues_params do
|
params :issues_params do
|
||||||
optional :labels, type: String, desc: 'Comma-separated list of label names'
|
optional :labels, type: String, 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',
|
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.'
|
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
|
||||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||||
|
@ -40,9 +58,7 @@ module API
|
||||||
use :issues_params
|
use :issues_params
|
||||||
end
|
end
|
||||||
get do
|
get do
|
||||||
issues = IssuesFinder.new(current_user, scope: 'all', author_id: current_user.id, state: params[:state]).execute.inc_notes_with_associations
|
issues = find_issues(scope: 'authored')
|
||||||
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
|
|
||||||
issues = issues.reorder(params[:order_by] => params[:sort])
|
|
||||||
|
|
||||||
present paginate(issues), with: Entities::Issue, current_user: current_user
|
present paginate(issues), with: Entities::Issue, current_user: current_user
|
||||||
end
|
end
|
||||||
|
@ -61,15 +77,10 @@ module API
|
||||||
use :issues_params
|
use :issues_params
|
||||||
end
|
end
|
||||||
get ":id/issues" do
|
get ":id/issues" do
|
||||||
group = find_group!(params.delete(:id))
|
group = find_group!(params[:id])
|
||||||
|
|
||||||
params[:group_id] = group.id
|
issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
|
||||||
params[:milestone_title] = params.delete(:milestone)
|
|
||||||
params[:label_name] = params.delete(:labels)
|
|
||||||
|
|
||||||
issues = IssuesFinder.new(current_user, params).execute
|
|
||||||
|
|
||||||
issues = issues.reorder(params[:order_by] => params[:sort])
|
|
||||||
present paginate(issues), with: Entities::Issue, current_user: current_user
|
present paginate(issues), with: Entities::Issue, current_user: current_user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -84,19 +95,13 @@ module API
|
||||||
params do
|
params do
|
||||||
optional :state, type: String, values: %w[opened closed all], default: 'all',
|
optional :state, type: String, values: %w[opened closed all], default: 'all',
|
||||||
desc: 'Return opened, closed, or all issues'
|
desc: 'Return opened, closed, or all issues'
|
||||||
optional :iid, type: Integer, desc: 'The IID of the issue'
|
optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
|
||||||
use :issues_params
|
use :issues_params
|
||||||
end
|
end
|
||||||
get ":id/issues" do
|
get ":id/issues" do
|
||||||
project = find_project(params[:id])
|
project = find_project(params[:id])
|
||||||
params[:state] ||= 'opened'
|
|
||||||
params[:project_id] = project.id
|
|
||||||
params[:milestone_title] = params.delete(:milestone)
|
|
||||||
params[:label_name] = params.delete(:labels)
|
|
||||||
|
|
||||||
issues = IssuesFinder.new(current_user, params).execute
|
issues = find_issues(project_id: project.id)
|
||||||
|
|
||||||
issues = issues.reorder(params[:order_by] => params[:sort])
|
|
||||||
|
|
||||||
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
|
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,6 +107,7 @@ describe API::Issues, api: true do
|
||||||
|
|
||||||
it 'returns an array of labeled issues when at least one label matches' do
|
it 'returns an array of labeled issues when at least one label matches' do
|
||||||
get api("/issues?labels=#{label.title},foo,bar", user)
|
get api("/issues?labels=#{label.title},foo,bar", user)
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(json_response).to be_an Array
|
expect(json_response).to be_an Array
|
||||||
expect(json_response.length).to eq(1)
|
expect(json_response.length).to eq(1)
|
||||||
|
@ -136,6 +137,51 @@ describe API::Issues, api: true do
|
||||||
expect(json_response.length).to eq(0)
|
expect(json_response.length).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array if no issue matches milestone' do
|
||||||
|
get api("/issues?milestone=#{empty_milestone.title}", user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array if milestone does not exist' do
|
||||||
|
get api("/issues?milestone=foo", user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array of issues in given milestone' do
|
||||||
|
get api("/issues?milestone=#{milestone.title}", user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(2)
|
||||||
|
expect(json_response.first['id']).to eq(issue.id)
|
||||||
|
expect(json_response.second['id']).to eq(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(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response.first['id']).to eq(closed_issue.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array of issues with no milestone' do
|
||||||
|
get api("/issues?milestone=#{Milestone::None.title}", author)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response.first['id']).to eq(confidential_issue.id)
|
||||||
|
end
|
||||||
|
|
||||||
it 'sorts by created_at descending by default' do
|
it 'sorts by created_at descending by default' do
|
||||||
get api('/issues', user)
|
get api('/issues', user)
|
||||||
response_dates = json_response.map { |issue| issue['created_at'] }
|
response_dates = json_response.map { |issue| issue['created_at'] }
|
||||||
|
@ -318,6 +364,15 @@ describe API::Issues, api: true do
|
||||||
expect(json_response.first['id']).to eq(group_closed_issue.id)
|
expect(json_response.first['id']).to eq(group_closed_issue.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an array of issues with no milestone' do
|
||||||
|
get api("#{base_url}?milestone=#{Milestone::None.title}", user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response.first['id']).to eq(group_confidential_issue.id)
|
||||||
|
end
|
||||||
|
|
||||||
it 'sorts by created_at descending by default' do
|
it 'sorts by created_at descending by default' do
|
||||||
get api(base_url, user)
|
get api(base_url, user)
|
||||||
response_dates = json_response.map { |issue| issue['created_at'] }
|
response_dates = json_response.map { |issue| issue['created_at'] }
|
||||||
|
@ -357,7 +412,6 @@ describe API::Issues, api: true do
|
||||||
|
|
||||||
describe "GET /projects/:id/issues" do
|
describe "GET /projects/:id/issues" do
|
||||||
let(:base_url) { "/projects/#{project.id}" }
|
let(:base_url) { "/projects/#{project.id}" }
|
||||||
let(:title) { milestone.title }
|
|
||||||
|
|
||||||
it "returns 404 on private projects for other users" do
|
it "returns 404 on private projects for other users" do
|
||||||
private_project = create(:empty_project, :private)
|
private_project = create(:empty_project, :private)
|
||||||
|
@ -433,8 +487,9 @@ describe API::Issues, api: true do
|
||||||
expect(json_response.first['labels']).to eq([label.title])
|
expect(json_response.first['labels']).to eq([label.title])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an array of labeled project issues when at least one label matches' do
|
it 'returns an array of labeled project issues where all labels match' do
|
||||||
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
|
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(json_response).to be_an Array
|
expect(json_response).to be_an Array
|
||||||
expect(json_response.length).to eq(1)
|
expect(json_response.length).to eq(1)
|
||||||
|
@ -463,7 +518,8 @@ describe API::Issues, api: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an array of issues in given milestone' do
|
it 'returns an array of issues in given milestone' do
|
||||||
get api("#{base_url}/issues?milestone=#{title}", user)
|
get api("#{base_url}/issues?milestone=#{milestone.title}", user)
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
expect(json_response).to be_an Array
|
expect(json_response).to be_an Array
|
||||||
expect(json_response.length).to eq(2)
|
expect(json_response.length).to eq(2)
|
||||||
|
@ -480,6 +536,15 @@ describe API::Issues, api: true do
|
||||||
expect(json_response.first['id']).to eq(closed_issue.id)
|
expect(json_response.first['id']).to eq(closed_issue.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an array of issues with no milestone' do
|
||||||
|
get api("#{base_url}/issues?milestone=#{Milestone::None.title}", user)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response.first['id']).to eq(confidential_issue.id)
|
||||||
|
end
|
||||||
|
|
||||||
it 'sorts by created_at descending by default' do
|
it 'sorts by created_at descending by default' do
|
||||||
get api("#{base_url}/issues", user)
|
get api("#{base_url}/issues", user)
|
||||||
response_dates = json_response.map { |issue| issue['created_at'] }
|
response_dates = json_response.map { |issue| issue['created_at'] }
|
||||||
|
@ -547,12 +612,21 @@ describe API::Issues, api: true do
|
||||||
|
|
||||||
it 'returns a project issue by iid' do
|
it 'returns a project issue by iid' do
|
||||||
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
|
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
|
||||||
|
|
||||||
expect(response.status).to eq 200
|
expect(response.status).to eq 200
|
||||||
|
expect(json_response.length).to eq 1
|
||||||
expect(json_response.first['title']).to eq issue.title
|
expect(json_response.first['title']).to eq issue.title
|
||||||
expect(json_response.first['id']).to eq issue.id
|
expect(json_response.first['id']).to eq issue.id
|
||||||
expect(json_response.first['iid']).to eq issue.iid
|
expect(json_response.first['iid']).to eq issue.iid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array for an unknown project issue iid' do
|
||||||
|
get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
|
||||||
|
|
||||||
|
expect(response.status).to eq 200
|
||||||
|
expect(json_response.length).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
it "returns 404 if issue id not found" do
|
it "returns 404 if issue id not found" do
|
||||||
get api("/projects/#{project.id}/issues/54321", user)
|
get api("/projects/#{project.id}/issues/54321", user)
|
||||||
expect(response).to have_http_status(404)
|
expect(response).to have_http_status(404)
|
||||||
|
|
Loading…
Reference in a new issue