2019-09-04 21:01:54 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe API::Issues do
|
2020-03-13 15:44:24 +05:30
|
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
let_it_be(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
|
|
|
|
let_it_be(:private_mrs_project) do
|
2019-09-04 21:01:54 +05:30
|
|
|
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
let(:user2) { create(:user) }
|
|
|
|
let(:non_member) { create(:user) }
|
|
|
|
let_it_be(:guest) { create(:user) }
|
|
|
|
let_it_be(:author) { create(:author) }
|
|
|
|
let_it_be(:assignee) { create(:assignee) }
|
|
|
|
let(:admin) { create(:user, :admin) }
|
2019-09-04 21:01:54 +05:30
|
|
|
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
|
2020-03-13 15:44:24 +05:30
|
|
|
let_it_be(:label) do
|
2019-09-04 21:01:54 +05:30
|
|
|
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) }
|
2020-03-13 15:44:24 +05:30
|
|
|
let_it_be(:empty_milestone) do
|
2019-09-04 21:01:54 +05:30
|
|
|
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' }
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
before_all do
|
2019-09-04 21:01:54 +05:30
|
|
|
project.add_reporter(user)
|
|
|
|
project.add_guest(guest)
|
|
|
|
private_mrs_project.add_reporter(user)
|
|
|
|
private_mrs_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
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
expect(json_response).to be_an Array
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns authentication error without any scope' do
|
|
|
|
get api('/issues')
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns authentication error when scope is assigned-to-me' do
|
|
|
|
get api('/issues'), params: { scope: 'assigned-to-me' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns authentication error when scope is created-by-me' do
|
|
|
|
get api('/issues'), params: { scope: 'created-by-me' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues matching state in milestone' do
|
|
|
|
get api('/issues'), params: { milestone: 'foo', scope: 'all' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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')
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns authentication error when scope is assigned_to_me' do
|
|
|
|
get api('/issues_statistics'), params: { scope: 'assigned_to_me' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns authentication error when scope is created_by_me' do
|
|
|
|
get api('/issues_statistics'), params: { scope: 'created_by_me' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
2019-09-04 21:01:54 +05:30
|
|
|
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')
|
2019-10-12 21:52:04 +05:30
|
|
|
# Calculating the value of subscribed field triggers Markdown
|
|
|
|
# processing. We can't do that for multiple issues / merge
|
|
|
|
# requests in a single API request.
|
|
|
|
expect(json_response.last).not_to have_key('subscribed')
|
2019-09-04 21:01:54 +05:30
|
|
|
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: '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) }
|
2019-12-04 20:38:33 +05:30
|
|
|
let(:issue2) { create(:issue, author: user, project: project) }
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
before do
|
2019-12-04 20:38:33 +05:30
|
|
|
create(:label_link, label: label, target: issue2)
|
2019-09-04 21:01:54 +05:30
|
|
|
create(:label_link, label: label_b, target: issue)
|
2019-12-04 20:38:33 +05:30
|
|
|
create(:label_link, label: label_b, target: issue2)
|
2019-09-04 21:01:54 +05:30
|
|
|
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
|
2020-04-22 19:07:51 +05:30
|
|
|
get api('/issues', user), params: { labels: IssuableFinder::Params::FILTER_ANY }
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
expect_paginated_array_response(issue.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues with any label with labels param as array' do
|
2020-04-22 19:07:51 +05:30
|
|
|
get api('/issues', user), params: { labels: [IssuableFinder::Params::FILTER_ANY] }
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
expect_paginated_array_response(issue.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues with no label' do
|
2020-04-22 19:07:51 +05:30
|
|
|
get api('/issues', user), params: { labels: IssuableFinder::Params::FILTER_NONE }
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
expect_paginated_array_response(closed_issue.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues with no label with labels param as array' do
|
2020-04-22 19:07:51 +05:30
|
|
|
get api('/issues', user), params: { labels: [IssuableFinder::Params::FILTER_NONE] }
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
expect_paginated_array_response(closed_issue.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
context 'filter by milestone' do
|
|
|
|
it 'returns an empty array if no issue matches milestone' do
|
|
|
|
get api("/issues?milestone=#{empty_milestone.title}", user)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response([])
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an empty array if milestone does not exist' do
|
|
|
|
get api('/issues?milestone=foo', user)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response([])
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an array of issues in given milestone' do
|
|
|
|
get api("/issues?milestone=#{milestone.title}", user)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response([issue.id, closed_issue.id])
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an array of issues in given milestone_title param' do
|
|
|
|
get api("/issues?milestone_title=#{milestone.title}", user)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response([issue.id, closed_issue.id])
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an array of issues matching state in milestone' do
|
|
|
|
get api("/issues?milestone=#{milestone.title}&state=closed", user)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response(closed_issue.id)
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an array of issues with no milestone' do
|
|
|
|
get api("/issues?milestone=#{no_milestone_title}", author)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response(confidential_issue.id)
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
it 'returns an array of issues with no milestone using milestone_title param' do
|
|
|
|
get api("/issues?milestone_title=#{no_milestone_title}", author)
|
2019-09-04 21:01:54 +05:30
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
expect_paginated_array_response(confidential_issue.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'negated' do
|
|
|
|
it 'returns all issues if milestone does not exist' do
|
|
|
|
get api('/issues?not[milestone]=foo', user)
|
|
|
|
|
|
|
|
expect_paginated_array_response([issue.id, closed_issue.id])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns all issues that do not belong to a milestone but have a milestone' do
|
|
|
|
get api("/issues?not[milestone]=#{empty_milestone.title}", user)
|
|
|
|
|
|
|
|
expect_paginated_array_response([issue.id, closed_issue.id])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues with any milestone' do
|
|
|
|
get api("/issues?not[milestone]=#{no_milestone_title}", user)
|
|
|
|
|
|
|
|
expect_paginated_array_response([issue.id, closed_issue.id])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an array of issues matching state not in milestone' do
|
|
|
|
get api("/issues?not[milestone]=#{empty_milestone.title}&state=closed", user)
|
|
|
|
|
|
|
|
expect_paginated_array_response(closed_issue.id)
|
|
|
|
end
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
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
|
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
context 'with issues list sort options' do
|
|
|
|
it 'accepts only predefined order by params' do
|
|
|
|
API::Helpers::IssuesHelpers.sort_options.each do |sort_opt|
|
|
|
|
get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' }
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-12-04 20:38:33 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'fails to sort with non predefined options' do
|
|
|
|
%w(milestone title abracadabra).each do |sort_opt|
|
|
|
|
get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' }
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
2019-12-04 20:38:33 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-04 21:01:54 +05:30
|
|
|
it 'matches V4 response schema' do
|
|
|
|
get api('/issues', user)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2019-09-04 21:01:54 +05:30
|
|
|
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' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
2019-09-04 21:01:54 +05:30
|
|
|
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' }
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
2019-09-04 21:01:54 +05:30
|
|
|
expect(json_response["error"]).to include("mutually exclusive")
|
|
|
|
end
|
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
|
|
|
|
context 'filtering by non_archived' do
|
2020-05-24 23:13:21 +05:30
|
|
|
let_it_be(:archived_project) { create(:project, :archived, creator_id: user.id, namespace: user.namespace) }
|
|
|
|
let_it_be(:archived_issue) { create(:issue, author: user, project: archived_project) }
|
|
|
|
let_it_be(:active_issue) { create(:issue, author: user, project: project) }
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
it 'returns issues from non archived projects by default' do
|
|
|
|
get api('/issues', user)
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
expect_paginated_array_response(active_issue.id, issue.id, closed_issue.id)
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
it 'returns issues from archived project with non_archived set as false' do
|
|
|
|
get api("/issues", user), params: { non_archived: false }
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
expect_paginated_array_response(active_issue.id, archived_issue.id, issue.id, closed_issue.id)
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context "when returns issue merge_requests_count for different access levels" do
|
|
|
|
let!(:merge_request1) do
|
|
|
|
create(:merge_request,
|
|
|
|
:simple,
|
|
|
|
author: user,
|
|
|
|
source_project: private_mrs_project,
|
|
|
|
target_project: private_mrs_project,
|
|
|
|
description: "closes #{issue.to_reference(private_mrs_project)}")
|
|
|
|
end
|
|
|
|
let!(:merge_request2) do
|
|
|
|
create(:merge_request,
|
|
|
|
:simple,
|
|
|
|
author: user,
|
|
|
|
source_project: project,
|
|
|
|
target_project: project,
|
|
|
|
description: "closes #{issue.to_reference}")
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'accessible merge requests count' do
|
|
|
|
let(:api_url) { "/issues" }
|
|
|
|
let(:target_issue) { issue }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
describe 'GET /projects/:id/issues/:issue_iid' do
|
|
|
|
it 'exposes full reference path' do
|
|
|
|
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(json_response['references']['short']).to eq("##{issue.iid}")
|
|
|
|
expect(json_response['references']['relative']).to eq("##{issue.iid}")
|
|
|
|
expect(json_response['references']['full']).to eq("#{project.parent.path}/#{project.path}##{issue.iid}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
describe 'PUT /projects/:id/issues/:issue_id' do
|
|
|
|
it_behaves_like 'issuable update endpoint' do
|
|
|
|
let(:entity) { issue }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-04 21:01:54 +05:30
|
|
|
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)
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'rejects a developer from deleting an issue' do
|
|
|
|
delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
2019-09-04 21:01:54 +05:30
|
|
|
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)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
2019-09-04 21:01:54 +05:30
|
|
|
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
|
2020-01-01 13:55:28 +05:30
|
|
|
it 'returns 404 when trying to delete an issue' do
|
2019-09-04 21:01:54 +05:30
|
|
|
delete api("/projects/#{project.id}/issues/123", user)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns 404 when using the issue ID instead of IID' do
|
|
|
|
delete api("/projects/#{project.id}/issues/#{issue.id}", user)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'time tracking endpoints' do
|
|
|
|
let(:issuable) { issue }
|
|
|
|
|
|
|
|
include_examples 'time tracking endpoints', 'issue'
|
|
|
|
end
|
|
|
|
end
|