summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorOswaldo Ferreira <oswaldo@gitlab.com>2017-01-25 19:35:27 -0200
committerOswaldo Ferreira <oswaldo@gitlab.com>2017-02-03 12:29:16 -0200
commit082f1af5df04ebfe7ef700fd94b713206ad41b5b (patch)
tree333a1a51bf4622b3d9443317b4e1980c94af9fca /spec
parenta89aab9c630593524b8a210746d2eb680ac5d102 (diff)
downloadgitlab-ce-27180-v4-api-namespace-introduction.tar.gz
Remove deprecated MR and Issue endpoints and preserve V3 namespace27180-v4-api-namespace-introduction
Diffstat (limited to 'spec')
-rw-r--r--spec/requests/api/issues_spec.rb17
-rw-r--r--spec/requests/api/merge_requests_spec.rb28
-rw-r--r--spec/requests/api/v3/issues_spec.rb1259
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb726
-rw-r--r--spec/support/api_helpers.rb9
5 files changed, 2002 insertions, 37 deletions
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 62f1b8d7ca2..863da19f294 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -612,23 +612,6 @@ describe API::Issues, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
- it 'returns a project issue by iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
-
- 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['id']).to eq issue.id
- expect(json_response.first['iid']).to eq issue.iid
- 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
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 71a7994e544..8f0f18dde3c 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -75,6 +75,16 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
+ it 'returns merge_request by "iids" array' do
+ get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
+
+ 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['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -161,24 +171,6 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'returns merge_request by iid' do
- url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
- get api(url, user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq merge_request.title
- expect(json_response.first['id']).to eq merge_request.id
- end
-
- it 'returns merge_request by iid array' do
- get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
-
- 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['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
- end
-
it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
new file mode 100644
index 00000000000..33a127de98a
--- /dev/null
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -0,0 +1,1259 @@
+require 'spec_helper'
+
+describe API::V3::Issues, api: true do
+ include ApiHelpers
+ include EmailHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 3.hours.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee,
+ created_at: generate(:issue_created_at),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: project,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 1.hour.ago
+ end
+ let!(: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) }
+ let!(: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) { URI.escape(Milestone::None.title) }
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /issues" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/issues")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of issues" do
+ get v3_api("/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ 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 v3_api('/issues?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 opened issues' do
+ get v3_api('/issues?state=opened', 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(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get v3_api('/issues?state=all', 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 labeled issues' do
+ get v3_api("/issues?labels=#{label.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['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues when at least one label matches' do
+ get v3_api("/issues?labels=#{label.title},foo,bar", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get v3_api('/issues?labels=foo,bar', 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 labeled issues matching given state' do
+ get v3_api("/issues?labels=#{label.title}&state=opened", 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['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 v3_api("/issues?labels=#{label.title}&state=closed", 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 no issue matches milestone' do
+ get v3_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 v3_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 v3_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 v3_api("/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ 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 v3_api("/issues?milestone=#{no_milestone_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
+ get v3_api('/issues', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api('/issues?sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api('/issues?order_by=updated_at', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api('/issues?order_by=updated_at&sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+
+ describe "GET /groups/:id/issues" do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignee: assignee,
+ updated_at: 2.hours.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.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) }
+
+ before do
+ group_project.team << [user, :reporter]
+ end
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ it 'returns group issues without confidential issues for non project members' do
+ get v3_api(base_url, non_member)
+
+ 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['title']).to eq(group_issue.title)
+ end
+
+ it 'returns group confidential issues for author' do
+ get v3_api(base_url, author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get v3_api(base_url, assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get v3_api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for admin' do
+ get v3_api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns an array of labeled group issues' do
+ get v3_api("#{base_url}?labels=#{group_label.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['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", 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 no group issue matches labels' do
+ get v3_api("#{base_url}?labels=foo,bar", 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 no issue matches milestone' do
+ get v3_api("#{base_url}?milestone=#{group_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 v3_api("#{base_url}?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 v3_api("#{base_url}?milestone=#{group_milestone.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_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
+ '&state=closed'
+
+ 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_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}?milestone=#{no_milestone_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
+ get v3_api(base_url, user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ it "returns 404 on private projects for other users" do
+ private_project = create(:empty_project, :private)
+ create(:issue, project: private_project)
+
+ get v3_api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ create(:issue, project: restricted_project)
+
+ get v3_api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect(json_response).to eq([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get v3_api("#{base_url}/issues", non_member)
+
+ 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['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get v3_api("#{base_url}/issues", guest)
+
+ 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['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for author' do
+ get v3_api("#{base_url}/issues", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get v3_api("#{base_url}/issues", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get v3_api("#{base_url}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for admin' do
+ get v3_api("#{base_url}/issues", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns an array of labeled project issues' do
+ get v3_api("#{base_url}/issues?labels=#{label.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['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled project issues where all labels match' do
+ get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", 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['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get v3_api("#{base_url}/issues?labels=foo,bar", 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 no issue matches milestone' do
+ get v3_api("#{base_url}/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 v3_api("#{base_url}/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 v3_api("#{base_url}/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 v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ 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 v3_api("#{base_url}/issues?milestone=#{no_milestone_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
+ get v3_api("#{base_url}/issues", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}/issues?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues/:issue_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_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['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['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it "returns a project issue by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns a project issue by iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+
+ 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['id']).to eq issue.id
+ expect(json_response.first['iid']).to eq issue.iid
+ end
+
+ it 'returns an empty array for an unknown project issue iid' do
+ get v3_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
+ get v3_api("/projects/#{project.id}/issues/54321", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it "returns 404 for non project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 for project members with guest role" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns confidential issue for project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+
+ expect(response).to have_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
+ it 'creates a new project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a new confidential project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: true
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'y'
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: false
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it "returns a 400 bad request if title not given" do
+ post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ 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 v3_api("/projects/#{project.id}/issues", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving issues in a merge request' do
+ let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:merge_request) { discussion.noteable }
+ let(:project) { merge_request.source_project }
+ before do
+ project.team << [user, :master]
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_for_resolving_discussions: merge_request.iid
+ end
+
+ it 'creates a new project issue' do
+ expect(response).to have_http_status(:created)
+ end
+
+ it 'resolves the discussions in a merge request' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'assigns a description to the issue mentioning the merge request' do
+ expect(json_response['description']).to include(merge_request.to_reference)
+ 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 v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ expect(response).to have_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 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: '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(is_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 v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
+
+ expect(response).to have_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_id to update only title" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "returns 404 error if issue id not found" do
+ put v3_api("/projects/#{project.id}/issues/44444", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ 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 v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "updates a confidential issue for project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for author" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for admin" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'sets an issue to confidential' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ confidential: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ it "sends notifications for subscribers of newly added labels when issue is updated" do
+ label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it 'removes all labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'updates labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'foo,bar'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'g' * 256
+
+ expect(response).to have_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_id to update state and label" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label2', state_event: "close"
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'reopened'
+ 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 v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label3', state_event: 'close', updated_at: update_time
+
+ expect(response).to have_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_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ describe "DELETE /projects/:id/issues/:issue_id" do
+ it "rejects a non member from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a developer from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the issue if an admin requests it" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete v3_api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_id/move' do
+ let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
+
+ it 'moves an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: project.id
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project2.id
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project2.id)
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/123/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_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 v3_api("/projects/123/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_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 v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: 123
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/subscription' do
+ it 'subscribes to an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE :id/issues/:issue_id/subscription' do
+ it 'unsubscribes from an issue' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
+end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
new file mode 100644
index 00000000000..b94e1ef4ced
--- /dev/null
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -0,0 +1,726 @@
+require "spec_helper"
+
+describe API::MergeRequests, api: true do
+ include ApiHelpers
+ let(:base_time) { Time.now }
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:non_member) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+
+ before do
+ project.team << [user, :reporter]
+ end
+
+ describe "GET /projects/:id/merge_requests" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/projects/#{project.id}/merge_requests")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of open merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=opened", 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.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of closed merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?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['title']).to eq(merge_request_closed.title)
+ end
+
+ it "returns an array of merged merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=merged", 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['title']).to eq(merge_request_merged.title)
+ end
+
+ context "with ordering" do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it "returns an array of merge_requests in ascending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it "returns an array of merge_requests in descending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by updated_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+ end
+
+ describe "GET /projects/:id/merge_requests/:merge_request_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(merge_request.id)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['project_id']).to eq(merge_request.project.id)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['description']).to eq(merge_request.description)
+ expect(json_response['state']).to eq(merge_request.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(merge_request.label_names)
+ expect(json_response['milestone']).to be_nil
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_branch']).to eq(merge_request.target_branch)
+ expect(json_response['source_branch']).to eq(merge_request.source_branch)
+ expect(json_response['upvotes']).to eq(0)
+ expect(json_response['downvotes']).to eq(0)
+ expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
+ expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
+ expect(json_response['work_in_progress']).to be_falsy
+ expect(json_response['merge_when_build_succeeds']).to be_falsy
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['work_in_progress']).to eq(false)
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it 'returns merge_request by iid' do
+ url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
+ get v3_api(url, user)
+ expect(response.status).to eq 200
+ expect(json_response.first['title']).to eq merge_request.title
+ expect(json_response.first['id']).to eq merge_request.id
+ end
+
+ it 'returns merge_request by iid array' do
+ get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
+
+ 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['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'Work in Progress' do
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['work_in_progress']).to eq(true)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
+ it 'returns a 200 when merge request is valid' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+ commit = merge_request.commits.first
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(merge_request.commits.size)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['title']).to eq(commit.title)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+ it 'returns the change information of the merge_request' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+ expect(response.status).to eq 200
+ expect(json_response['changes'].size).to eq(merge_request.diffs.size)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests" do
+ context 'between branches projects' do
+ it "returns merge_request" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label2',
+ milestone_id: milestone.id,
+ remove_source_branch: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it "returns 422 when source_branch equals target_branch" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
+ expect(response).to have_http_status(422)
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", target_branch: "master", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "markdown", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ target_branch: 'master', source_branch: 'markdown'
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'markdown',
+ target_branch: 'master',
+ author: user,
+ 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
+
+ context 'with existing MR' do
+ before do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ @mr = MergeRequest.all.last
+ end
+
+ it 'returns 409 when MR already exists for source/target' do
+ expect do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'New test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ end.to change { MergeRequest.count }.by(0)
+ expect(response).to have_http_status(409)
+ end
+ end
+ end
+
+ context 'forked projects' do
+ let!(:user2) { create(:user) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
+
+ before :each do |each|
+ fork_project.team << [user2, :reporter]
+ end
+
+ it "returns merge_request" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
+ author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['description']).to eq('Test description for Test merge_request')
+ end
+
+ it "does not return 422 when source_branch equals target_branch" do
+ expect(project.id).not_to eq(fork_project.id)
+ expect(fork_project.forked?).to be_truthy
+ expect(fork_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when target_branch is specified' do
+ it 'returns 422 if not a forked project' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user,
+ target_project_id: fork_project.id
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns 422 if targeting a different fork' do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: unrelated_project.id
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ it "returns 201 when target_branch is specified and for the same project" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ context "when the user is developer" do
+ let(:developer) { create(:user) }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ it "denies the deletion of the merge request" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when the user is project owner" do
+ it "destroys the merge request owners can destroy" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
+
+ it "returns merge_request in case of success" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 406 if branch can't be merged" do
+ allow_any_instance_of(MergeRequest).
+ to receive(:can_be_merged?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(406)
+ expect(json_response['message']).to eq('Branch cannot be merged')
+ end
+
+ it "returns 405 if merge_request is not open" do
+ merge_request.close
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 405 if merge_request is a work in progress" do
+ merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 401 if user has no permissions to merge" do
+ user2 = create(:user)
+ project.team << [user2, :reporter]
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
+ expect(response).to have_http_status(401)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ end
+
+ it "returns 409 if the SHA parameter doesn't match" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
+ end
+
+ it "succeeds if the SHA parameter matches" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "enables merge when pipeline succeeds if the pipeline is active" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_build_succeeds']).to eq(true)
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ context "to close a MR" do
+ it "returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ end
+ end
+
+ it "updates title and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('New title')
+ end
+
+ it "updates description and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
+ expect(response).to have_http_status(200)
+ expect(json_response['description']).to eq('New description')
+ end
+
+ it "updates milestone_id and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+ expect(response).to have_http_status(200)
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ end
+
+ it "returns merge_request with renamed target_branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
+ expect(response).to have_http_status(200)
+ expect(json_response['target_branch']).to eq('wiki')
+ end
+
+ it "returns merge_request that removes the source branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+ title: 'new issue',
+ 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 'does not update state when title is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+ it "returns comment" do
+ original_count = merge_request.notes.size
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['author']['name']).to eq(user.name)
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(merge_request.reload.notes.size).to eq(original_count + 1)
+ end
+
+ it "returns 400 if note is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 404 if note is attached to non existent merge request" do
+ post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
+ note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET :id/merge_requests/:merge_request_id/comments" do
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+
+ it "returns merge_request comments ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", 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['note']).to eq("a comment on a MR")
+ expect(json_response.first['author']['id']).to eq(user.id)
+ expect(json_response.last['note']).to eq("another comment on a MR")
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+ it 'returns the issue that will be closed on merge' do
+ issue = create(:issue, project: project)
+ mr = merge_request.tap do |mr|
+ mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
+ end
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", 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(issue.id)
+ end
+
+ it 'returns an empty array when there are no issues to be closed' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", 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['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ it 'subscribes to a merge request' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ it 'unsubscribes from a merge request' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'time tracking endpoints', 'merge_request'
+ end
+
+ def mr_with_later_created_and_updated_at_time
+ merge_request
+ merge_request.created_at += 1.hour
+ merge_request.updated_at += 30.minutes
+ merge_request.save
+ merge_request
+ end
+
+ def mr_with_earlier_created_and_updated_at_time
+ merge_request_closed
+ merge_request_closed.created_at -= 1.hour
+ merge_request_closed.updated_at -= 30.minutes
+ merge_request_closed.save
+ merge_request_closed
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 68b196d9033..ae6e708cf87 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -17,8 +17,8 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
- def api(path, user = nil)
- "/api/#{API::API.version}#{path}" +
+ def api(path, user = nil, version: API::API.version)
+ "/api/#{version}#{path}" +
# Normalize query string
(path.index('?') ? '' : '?') +
@@ -31,6 +31,11 @@ module ApiHelpers
end
end
+ # Temporary helper method for simplifying V3 exclusive API specs
+ def v3_api(path, user = nil)
+ api(path, user, version: 'v3')
+ end
+
def ci_api(path, user = nil)
"/ci/api/v1/#{path}" +