Commit fce2070c authored by Douwe Maan's avatar Douwe Maan

Merge branch '28251-mr-and-issue-iids-for-api-v4-ee' into 'master'

API routes referencing a specific issue should use the issue `iid`

See merge request !1325
parents f4bdac56 f1d7c416
......@@ -90,7 +90,7 @@ GET /projects/:id/merge_requests/:merge_request_id
Parameters:
- `id` (required) - The ID of a project
- `merge_request_id` (required) - The ID of MR
- `merge_request_iid` (required) - The internal ID of the merge request
```json
{
......
......@@ -5,6 +5,8 @@ module API
version %w(v3 v4), using: :path
version 'v3', using: :path do
helpers ::API::V3::Helpers
mount ::API::V3::AwardEmoji
mount ::API::V3::Boards
mount ::API::V3::Branches
......
......@@ -3,12 +3,16 @@ module API
include PaginationParams
before { authenticate! }
AWARDABLES = %w[issue merge_request snippet].freeze
AWARDABLES = [
{ type: 'issue', find_by: :iid },
{ type: 'merge_request', find_by: :iid },
{ type: 'snippet', find_by: :id }
].freeze
resource :projects do
AWARDABLES.each do |awardable_type|
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
AWARDABLES.each do |awardable_params|
awardable_string = awardable_params[:type].pluralize
awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
params do
requires :id, type: String, desc: 'The ID of a project'
......@@ -104,10 +108,10 @@ module API
note_id = params.delete(:note_id)
awardable.notes.find(note_id)
elsif params.include?(:issue_id)
user_project.issues.find(params[:issue_id])
elsif params.include?(:merge_request_id)
user_project.merge_requests.find(params[:merge_request_id])
elsif params.include?(:issue_iid)
user_project.issues.find_by!(iid: params[:issue_iid])
elsif params.include?(:merge_request_iid)
user_project.merge_requests.find_by!(iid: params[:merge_request_iid])
else
user_project.snippets.find(params[:snippet_id])
end
......
......@@ -82,16 +82,16 @@ module API
label || not_found!('Label')
end
def find_project_issue(id)
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
def find_project_issue(iid)
IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
def find_project_merge_request(id)
MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
def find_project_merge_request(iid)
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find(id)
def find_merge_request_with_access(iid, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find_by!(iid: iid)
authorize! access_level, merge_request
merge_request
end
......
......@@ -104,10 +104,10 @@ module API
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
get ":id/issues/:issue_id" do
issue = find_project_issue(params[:issue_id])
get ":id/issues/:issue_iid" do
issue = find_project_issue(params[:issue_iid])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
......@@ -154,7 +154,7 @@ module API
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
optional :title, type: String, desc: 'The title of an issue'
optional :updated_at, type: DateTime,
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
......@@ -164,8 +164,8 @@ module API
:labels, :created_at, :due_date, :confidential, :state_event,
:weight
end
put ':id/issues/:issue_id' do
issue = user_project.issues.find(params.delete(:issue_id))
put ':id/issues/:issue_iid' do
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
# Setting created_at time only allowed for admins and project owners
......@@ -192,11 +192,11 @@ module API
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
post ':id/issues/:issue_id/move' do
issue = user_project.issues.find_by(id: params[:issue_id])
post ':id/issues/:issue_iid/move' do
issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
new_project = Project.find_by(id: params[:to_project_id])
......@@ -212,10 +212,10 @@ module API
desc 'Delete a project issue'
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
delete ":id/issues/:issue_id" do
issue = user_project.issues.find_by(id: params[:issue_id])
delete ":id/issues/:issue_iid" do
issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
......
......@@ -13,11 +13,11 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
use :pagination
end
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ":id/merge_requests/:merge_request_iid/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
end
......@@ -29,12 +29,12 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end
......
......@@ -103,23 +103,23 @@ module API
desc 'Delete a merge request'
params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
merge_request = find_project_merge_request(params[:merge_request_id])
delete ":id/merge_requests/:merge_request_iid" do
merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
end
params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
desc 'Get a single merge request' do
success Entities::MergeRequest
end
get ':id/merge_requests/:merge_request_id' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
......@@ -127,8 +127,8 @@ module API
desc 'Get the commits of a merge request' do
success Entities::RepoCommit
end
get ':id/merge_requests/:merge_request_id/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
present paginate(commits), with: Entities::RepoCommit
......@@ -137,8 +137,8 @@ module API
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
get ':id/merge_requests/:merge_request_id/changes' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ':id/merge_requests/:merge_request_iid/changes' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
......@@ -156,8 +156,8 @@ module API
:milestone_id, :labels, :state_event,
:remove_source_branch, :squash
end
put ':id/merge_requests/:merge_request_id' do
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
put ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
......@@ -183,8 +183,8 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
put ':id/merge_requests/:merge_request_id/merge' do
merge_request = find_project_merge_request(params[:merge_request_id])
put ':id/merge_requests/:merge_request_iid/merge' do
merge_request = find_project_merge_request(params[:merge_request_iid])
# Merge request can not be merged
# because user dont have permissions to push into target branch
......@@ -223,8 +223,9 @@ module API
desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
success Entities::MergeRequest
end
post ':id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_id])
post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_iid])
unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
......@@ -239,8 +240,8 @@ module API
params do
use :pagination
end
get ':id/merge_requests/:merge_request_id/comments' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ':id/merge_requests/:merge_request_iid/comments' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
......@@ -250,8 +251,8 @@ module API
params do
requires :note, type: String, desc: 'The text of the comment'
end
post ':id/merge_requests/:merge_request_id/comments' do
merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
post ':id/merge_requests/:merge_request_iid/comments' do
merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note)
opts = {
note: params[:note],
......@@ -274,8 +275,8 @@ module API
params do
use :pagination
end
get ':id/merge_requests/:merge_request_id/closes_issues' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
get ':id/merge_requests/:merge_request_iid/closes_issues' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
......
......@@ -5,11 +5,11 @@ module API
included do
helpers do
def issuable_name
declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request'
end
def issuable_key
"#{issuable_name}_id".to_sym
"#{issuable_name}_iid".to_sym
end
def update_issuable_key
......@@ -50,7 +50,7 @@ module API
issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
issuable_collection_name = issuable_name.pluralize
issuable_key = "#{issuable_name}_id".to_sym
issuable_key = "#{issuable_name}_iid".to_sym
desc "Set a time estimate for a project #{issuable_name}"
params do
......
......@@ -5,8 +5,8 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) }
'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
'issues' => ->(iid) { find_project_issue(iid) }
}.freeze
params do
......@@ -14,13 +14,13 @@ module API
end
resource :projects do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_id".to_sym
type_id_str = "#{type.singularize}_iid".to_sym
desc 'Create a todo on an issuable' do
success Entities::Todo
end
params do
requires type_id_str, type: Integer, desc: 'The ID of an issuable'
requires type_id_str, type: Integer, desc: 'The IID of an issuable'
end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
......
......@@ -16,11 +16,64 @@ module API
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
[":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"].each do |endpoint|
[
":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
desc 'Get a list of project +awardable+ award emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
use :pagination
end
get endpoint do
if can_read_awardable?
awards = awardable.award_emoji
present paginate(awards), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
end
desc 'Get a specific award emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
requires :award_id, type: Integer, desc: 'The ID of the award'
end
get "#{endpoint}/:award_id" do
if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
end
desc 'Award a new Emoji' do
detail 'This feature was introduced in 8.9'
success Entities::AwardEmoji
end
params do
requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
end
post endpoint do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
if award.persisted?
present award, with: Entities::AwardEmoji
else
not_found!("Award Emoji #{award.errors.messages}")
end
end
desc 'Delete a +awardables+ award emoji' do
detail 'This feature was introduced in 8.9'
success ::API::Entities::AwardEmoji
success Entities::AwardEmoji
end
params do
requires :award_id, type: Integer, desc: 'The ID of an award emoji'
......@@ -30,13 +83,22 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
present award.destroy, with: ::API::Entities::AwardEmoji
award.destroy
present award, with: Entities::AwardEmoji
end
end
end
end
helpers do
def can_read_awardable?
can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
awardable.user_can_award?(current_user, params[:name])
end
def awardable
@awardable ||=
begin
......@@ -53,6 +115,15 @@ module API
end
end
end
def read_ability(awardable)
case awardable
when Note
read_ability(awardable.noteable)
else
:"read_#{awardable.class.to_s.underscore}"
end
end
end
end
end
......
module API
module V3
module Helpers
def find_project_issue(id)
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
def find_project_merge_request(id)
MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
end
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find(id)
authorize! access_level, merge_request
merge_request
end
end
end
end
module API
module V3
# MergeRequestDiff API
class MergeRequestDiffs < Grape::API
include PaginationParams
before { authenticate! }
resource :projects do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success ::API::Entities::MergeRequestDiff
end
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
use :pagination
end
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present paginate(merge_request.merge_request_diffs), with: ::API::Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
detail 'This feature was introduced in GitLab 8.12.'
success ::API::Entities::MergeRequestDiffFull
end
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull
end
end
end
end
end
module API
module V3
module TimeTrackingEndpoints
extend ActiveSupport::Concern
included do
helpers do
def issuable_name
declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
end
def issuable_key
"#{issuable_name}_id".to_sym
end
def update_issuable_key
"update_#{issuable_name}".to_sym
end
def read_issuable_key
"read_#{issuable_name}".to_sym
end
def load_issuable
@issuable ||= begin
case issuable_name
when 'issue'
find_project_issue(params.delete(issuable_key))
when 'merge_request'
find_project_merge_request(params.delete(issuable_key))
end
end
end
def update_issuable(attrs)
custom_params = declared_params(include_missing: false)
custom_params.merge!(attrs)
issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
if issuable.valid?
present issuable, with: ::API::Entities::IssuableTimeStats
else
render_validation_error!(issuable)
end
end
def update_service
issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
end
end
issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
issuable_collection_name = issuable_name.pluralize
issuable_key = "#{issuable_name}_id".to_sym
desc "Set a time estimate for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
requires :duration, type: String, desc: 'The duration to be parsed'
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
end
desc "Reset the time estimate for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(time_estimate: 0)
end
desc "Add spent time for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
requires :duration, type: String, desc: 'The duration to be parsed'
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
authorize! update_issuable_key, load_issuable
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
user: current_user
})
end
desc "Reset spent time for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
authorize! update_issuable_key, load_issuable
status :ok
update_issuable(spend_time: { duration: :reset, user: current_user })
end
desc "Show time stats for a project #{issuable_name}"
params do
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end
get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
authorize! read_issuable_key, load_issuable
present load_issuable, with: ::API::Entities::IssuableTimeStats
end
end
end
end
end
......@@ -15,7 +15,7 @@ describe API::AwardEmoji, api: true do
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
context 'on an issue' do
it "returns an array of award_emoji" do
get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
......@@ -31,7 +31,7 @@ describe API::AwardEmoji, api: true do
context 'on a merge request' do
it "returns an array of award_emoji" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
......@@ -57,7 +57,7 @@ describe API::AwardEmoji, api: true do
it 'returns a status code 404' do
user1 = create(:user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1)
expect(response).to have_http_status(404)
end
......@@ -68,7 +68,7 @@ describe API::AwardEmoji, api: true do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an array of award emoji' do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
......@@ -79,7 +79,7 @@ describe API::AwardEmoji, api: true do
describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
context 'on an issue' do
it "returns the award emoji" do
get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(award_emoji.name)
......@@ -88,7 +88,7 @@ describe API::AwardEmoji, api: true do
end
it "returns a 404 error if the award is not found" do
get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
expect(response).to have_http_status(404)
end
......@@ -96,7 +96,7 @@ describe API::AwardEmoji, api: true do
context 'on a merge request' do
it 'returns the award emoji' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(downvote.name)
......@@ -123,7 +123,7 @@ describe API::AwardEmoji, api: true do
it 'returns a status code 404' do
user1 = create(:user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1)
expect(response).to have_http_status(404)
end
......@@ -134,7 +134,7 @@ describe API::AwardEmoji, api: true do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an award emoji' do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
expect(response).to have_http_status(200)
expect(json_response).not_to be_an Array
......@@ -147,7 +147,7 @@ describe API::AwardEmoji, api: true do
context "on an issue" do
it "creates a new award emoji" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish'
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('blowfish')
......@@ -155,13 +155,13 @@ describe API::AwardEmoji, api: true do
end
it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
expect(response).to have_http_status(400)
end
it "returns a 401 unauthorized error if the user is not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup'
expect(response).to have_http_status(401)
end
......@@ -173,15 +173,15 @@ describe API::AwardEmoji, api: true do
end
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: '+1'
expect(issue.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
......@@ -207,7 +207,7 @@ describe API::AwardEmoji, api: true do
it 'creates a new award emoji' do
expect do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
end.to change { note.award_emoji.count }.from(0).to(1)
expect(response).to have_http_status(201)
......@@ -215,21 +215,21 @@ describe API::AwardEmoji, api: true do
end
it "it returns 404 error when user authored note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: '+1'
expect(note.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
......@@ -241,14 +241,14 @@ describe API::AwardEmoji, api: true do
context 'when the awardable is an Issue' do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
expect(response).to have_http_status(204)
end.to change { issue.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when the award emoji can not be found' do
delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
expect(response).to have_http_status(404)
end
......@@ -257,14 +257,14 @@ describe API::AwardEmoji, api: true do
context 'when the awardable is a Merge Request' do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
expect(response).to have_http_status(204)
end.to change { merge_request.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user)
expect(response).to have_http_status(404)
end
......@@ -289,7 +289,7 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
expect(response).to have_http_status(204)
end.to change { note.award_emoji.count }.from(1).to(0)
......
......@@ -757,9 +757,9 @@ describe API::Issues, api: true do
end
end
describe "GET /projects/:id/issues/:issue_id" do
describe "GET /projects/:id/issues/:issue_iid" do
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(issue.id)
......@@ -778,8 +778,8 @@ describe API::Issues, api: true do
expect(json_response['weight']).to be_nil
end
it "returns a project issue by id" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
it "returns a project issue by internal id" do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(issue.title)
......@@ -791,40 +791,52 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
it "returns 404 if the issue ID is used" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(404)
end
context 'confidential issues' do
it "returns 404 for non project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
expect(response).to have_http_status(404)
end
it "returns 404 for project members with guest role" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
expect(response).to have_http_status(404)
end
it "returns confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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)
......@@ -1006,23 +1018,29 @@ describe API::Issues, api: true do
end
end
describe "PUT /projects/:id/issues/:issue_id to update only title" do
describe "PUT /projects/:id/issues/:issue_iid to update only title" do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", 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
it "returns 404 error if issue iid not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
it 'allows special label names' do
it "returns 404 error if issue id is used instead of the iid" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
......@@ -1036,40 +1054,40 @@ describe API::Issues, api: true do
context 'confidential issues' do
it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "returns 403 for project members with guest role" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "updates a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", 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 api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
confidential: true
expect(response).to have_http_status(200)
......@@ -1077,7 +1095,7 @@ describe API::Issues, api: true do
end
it 'makes a confidential issue public' do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: false
expect(response).to have_http_status(200)
......@@ -1085,7 +1103,7 @@ describe API::Issues, api: true do
end
it 'does not update a confidential issue with wrong confidential flag' do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: 'foo'
expect(response).to have_http_status(400)
......@@ -1094,7 +1112,7 @@ describe API::Issues, api: true do
end
end
describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
let(:params) do
{
title: 'updated title',
......@@ -1107,7 +1125,7 @@ describe API::Issues, api: true do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
put api("/projects/#{project.id}/issues/#{issue.id}", user), params
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params
expect(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
......@@ -1121,12 +1139,12 @@ describe API::Issues, api: true do
end
end
describe 'PUT /projects/:id/issues/:issue_id to update labels' do
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
......@@ -1137,7 +1155,7 @@ describe API::Issues, api: true do
label.toggle_subscription(user2, project)
perform_enqueued_jobs do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title', labels: label.title
end
......@@ -1145,14 +1163,14 @@ describe API::Issues, api: true do
end
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
it 'updates labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'foo'
......@@ -1160,7 +1178,7 @@ describe API::Issues, api: true do
end
it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", 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'
......@@ -1174,7 +1192,7 @@ describe API::Issues, api: true do
end
it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'g' * 256
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
......@@ -1183,9 +1201,9 @@ describe API::Issues, api: true do
end
end
describe "PUT /projects/:id/issues/:issue_id to update state and label" do
describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label2', state_event: "close"
expect(response).to have_http_status(200)
......@@ -1194,7 +1212,7 @@ describe API::Issues, api: true do
end
it 'reopens a project isssue' do
put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen'
expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'reopened'
......@@ -1203,7 +1221,7 @@ describe API::Issues, api: true do
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
put api("/projects/#{project.id}/issues/#{issue.id}", user),
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
expect(response).to have_http_status(200)
......@@ -1213,11 +1231,11 @@ describe API::Issues, api: true do
end
end
describe 'PUT /projects/:id/issues/:issue_id to update due date' do
describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date
expect(response).to have_http_status(200)
expect(json_response['due_date']).to eq(due_date)
......@@ -1226,7 +1244,7 @@ describe API::Issues, api: true do
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), weight: 5
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to eq(5)
......@@ -1235,35 +1253,35 @@ describe API::Issues, api: true do
it 'removes a weight from an issue' do
weighted_issue = create(:issue, project: project, weight: 2)
put api("/projects/#{project.id}/issues/#{weighted_issue.id}", user), weight: nil
put api("/projects/#{project.id}/issues/#{weighted_issue.iid}", user), weight: nil
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
end
it 'returns 400 if weight is less than minimum weight' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), weight: -1
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: -1
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
it 'returns 400 if weight is more than maximum weight' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), weight: 10
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 10
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
end
describe "DELETE /projects/:id/issues/:issue_id" do
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.id}", non_member)
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
expect(response).to have_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.id}", author)
delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
expect(response).to have_http_status(403)
end
......@@ -1272,7 +1290,7 @@ describe API::Issues, api: true do
let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
expect(response).to have_http_status(204)
end
......@@ -1285,14 +1303,20 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
end
it 'returns 404 when using the issue ID instead of IID' do
delete api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(404)
end
end
describe '/projects/:id/issues/:issue_id/move' do
describe '/projects/:id/issues/:issue_iid/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 api("/projects/#{project.id}/issues/#{issue.id}/move", user),
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(201)
......@@ -1301,7 +1325,7 @@ describe API::Issues, api: true do
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: project.id
expect(response).to have_http_status(400)
......@@ -1311,7 +1335,7 @@ describe API::Issues, api: true do
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project2.id
expect(response).to have_http_status(400)
......@@ -1320,13 +1344,23 @@ describe API::Issues, api: true do
end
it 'moves the issue to another namespace if I am admin' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
post api("/projects/#{project.id}/issues/#{issue.iid}/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 using the issue ID instead of iid' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
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 issue does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/123/move", user),
......@@ -1339,7 +1373,7 @@ describe API::Issues, api: true do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/123/issues/#{issue.id}/move", user),
post api("/projects/123/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(404)
......@@ -1349,7 +1383,7 @@ describe API::Issues, api: true do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: 123
expect(response).to have_http_status(404)
......@@ -1357,16 +1391,16 @@ describe API::Issues, api: true do
end
end
describe 'POST :id/issues/:issue_id/subscribe' do
describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2)
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
......@@ -1377,8 +1411,14 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue ID is used instead of the iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member)
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
expect(response).to have_http_status(404)
end
......@@ -1386,14 +1426,14 @@ describe API::Issues, api: true do
describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2)
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
expect(response).to have_http_status(304)
end
......@@ -1404,8 +1444,14 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
it 'returns 404 if using the issue ID instead of iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member)
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
expect(response).to have_http_status(404)
end
......
......@@ -13,9 +13,9 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
project.team << [user, :master]
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
it 'returns 200 for a valid merge request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user)
merge_request_diff = merge_request.merge_request_diffs.first
expect(response.status).to eq 200
......@@ -26,16 +26,22 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
end
it 'returns a 404 when merge_request_id not found' do
it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/versions", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do
let(:merge_request_diff) { merge_request.merge_request_diffs.first }
it 'returns a 200 for a valid merge request' do
merge_request_diff = merge_request.merge_request_diffs.first
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}", user)
expect(response.status).to eq 200
expect(json_response['id']).to eq(merge_request_diff.id)
......@@ -43,8 +49,18 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
end
it 'returns a 404 when merge_request_id not found' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 when merge_request version_id is not found' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 when merge_request_iid is not found' do
get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user)
expect(response).to have_http_status(404)
end
end
......
......@@ -154,9 +154,9 @@ describe API::MergeRequests, api: true do
end
end
describe "GET /projects/:id/merge_requests/:merge_request_id" do
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
it 'exposes known attributes' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
......@@ -185,7 +185,7 @@ describe API::MergeRequests, api: true do
end
it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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)
......@@ -195,25 +195,31 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
it "returns a 404 error if merge_request_id not found" do
it "returns a 404 error if merge_request_iid not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
end
it "returns a 404 error if merge_request `id` is used instead of iid" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", 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
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
it 'returns a 200 when merge request is valid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
commit = merge_request.commits.first
expect(response.status).to eq 200
......@@ -224,24 +230,36 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(commit.title)
end
it 'returns a 404 when merge_request_id not found' do
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/commits", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 when merge_request id is used instead of iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do
it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/changes", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 when merge_request id is used instead of iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/merge_requests" do
......@@ -463,7 +481,7 @@ describe API::MergeRequests, api: true do
end
end
describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
describe "DELETE /projects/:id/merge_requests/:merge_request_iid" do
context "when the user is developer" do
let(:developer) { create(:user) }
......@@ -472,25 +490,37 @@ describe API::MergeRequests, api: true do
end
it "denies the deletion of the merge request" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_http_status(204)
end
it "returns 404 for an invalid merge request IID" do
delete api("/projects/#{project.id}/merge_requests/12345", user)
expect(response).to have_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(404)
end
end
end
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(200)
end
......@@ -499,7 +529,7 @@ describe API::MergeRequests, api: true do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
......@@ -507,14 +537,14 @@ describe API::MergeRequests, api: true do
it "returns 405 if merge_request is not open" do
merge_request.close
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
......@@ -522,7 +552,7 @@ describe API::MergeRequests, api: true do
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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
......@@ -531,27 +561,27 @@ describe API::MergeRequests, api: true do
it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha
expect(response).to have_http_status(200)
end
it "updates the MR's squash attribute" do
expect do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), squash: true
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), squash: true
end.to change { merge_request.reload.squash }
expect(response).to have_http_status(200)
......@@ -561,18 +591,30 @@ describe API::MergeRequests, api: true do
allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
allow(pipeline).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_pipeline_succeeds: true
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
expect(response).to have_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(404)
end
end
describe "PUT /projects/:id/merge_requests/:merge_request_id" do
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
context "to close a MR" do
it "returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close"
expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
......@@ -580,45 +622,45 @@ describe API::MergeRequests, api: true do
end
it "updates title and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id
expect(response).to have_http_status(200)
expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "updates squash and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), squash: true
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), squash: true
expect(response).to have_http_status(200)
expect(json_response['squash']).to be_truthy
end
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
......@@ -631,7 +673,7 @@ describe API::MergeRequests, api: true do
end
it 'does not update state when title is empty' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil
merge_request.reload
expect(response).to have_http_status(400)
......@@ -639,19 +681,31 @@ describe API::MergeRequests, api: true do
end
it 'does not update state when target_branch is empty' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", 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
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close"
expect(response).to have_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do
it "returns comment" do
original_count = merge_request.notes.size
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment"
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
......@@ -661,23 +715,29 @@ describe API::MergeRequests, api: true do
end
it "returns 400 if note is missing" do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
expect(response).to have_http_status(400)
end
it "returns 404 if note is attached to non existent merge request" do
it "returns 404 if merge request iid is invalid" do
post api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
expect(response).to have_http_status(404)
end
it "returns 404 if merge request id is used instead of iid" do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user),
note: 'My comment'
expect(response).to have_http_status(404)
end
end
describe "GET :id/merge_requests/:merge_request_id/comments" do
describe "GET :id/merge_requests/:merge_request_iid/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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
......@@ -688,20 +748,25 @@ describe API::MergeRequests, api: true do
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
it "returns a 404 error if merge_request_iid is invalid" do
get api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response).to have_http_status(404)
end
it "returns a 404 error if merge_request id is used instead of iid" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(404)
end
end
describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
describe 'GET :id/merge_requests/:merge_request_iid/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 api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
......@@ -711,7 +776,7 @@ describe API::MergeRequests, api: true do
end
it 'returns an empty array when there are no issues to be closed' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
......@@ -725,7 +790,7 @@ describe API::MergeRequests, api: true do
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 api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
......@@ -741,22 +806,34 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest)
expect(response).to have_http_status(403)
end
it "returns 404 for an invalid merge request IID" do
get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user)
expect(response).to have_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
expect(response).to have_http_status(404)
end
end
describe 'POST :id/merge_requests/:merge_request_id/subscribe' do
describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do
it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
......@@ -767,26 +844,32 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
it 'returns 404 if the merge request id is used instead of iid' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
expect(response).to have_http_status(403)
end
end
describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do
describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do
it 'unsubscribes from a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
expect(response).to have_http_status(304)
end
......@@ -797,11 +880,17 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
it 'returns 404 if the merge request id is used instead of iid' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", 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 api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest)
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest)
expect(response).to have_http_status(403)
end
......
......@@ -163,7 +163,7 @@ describe API::Todos, api: true do
shared_examples 'an issuable' do |issuable_type|
it 'creates a todo on an issuable' do
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
expect(response.status).to eq(201)
expect(json_response['project']).to be_a Hash
......@@ -180,7 +180,7 @@ describe API::Todos, api: true do
it 'returns 304 there already exist a todo on that issuable' do
create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
expect(response.status).to eq(304)
end
......@@ -195,7 +195,7 @@ describe API::Todos, api: true do
guest = create(:user)
project_1.team << [guest, :guest]
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
if issuable_type == 'merge_requests'
expect(response).to have_http_status(403)
......
......@@ -13,6 +13,231 @@ describe API::V3::AwardEmoji, api: true do
before { project.team << [user, :master] }
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
context 'on an issue' do
it "returns an array of award_emoji" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award_emoji.name)
end
it "returns a 404 error when issue id not found" do
get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user)
expect(response).to have_http_status(404)
end
end
context 'on a merge request' do
it "returns an array of award_emoji" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(downvote.name)
end
end
context 'on a snippet' do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:award) { create(:award_emoji, awardable: snippet) }
it 'returns the awarded emoji' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award.name)
end
end
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an array of award emoji' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(rocket.name)
end
end
describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
context 'on an issue' do
it "returns the award emoji" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(award_emoji.name)
expect(json_response['awardable_id']).to eq(issue.id)
expect(json_response['awardable_type']).to eq("Issue")
end
it "returns a 404 error if the award is not found" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
expect(response).to have_http_status(404)
end
end
context 'on a merge request' do
it 'returns the award emoji' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(downvote.name)
expect(json_response['awardable_id']).to eq(merge_request.id)
expect(json_response['awardable_type']).to eq("MergeRequest")
end
end
context 'on a snippet' do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:award) { create(:award_emoji, awardable: snippet) }
it 'returns the awarded emoji' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(award.name)
expect(json_response['awardable_id']).to eq(snippet.id)
expect(json_response['awardable_type']).to eq("Snippet")
end
end
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an award emoji' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
expect(response).to have_http_status(200)
expect(json_response).not_to be_an Array
expect(json_response['name']).to eq(rocket.name)
end
end
describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
let(:issue2) { create(:issue, project: project, author: user) }
context "on an issue" do
it "creates a new award emoji" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
it "returns a 400 bad request error if the name is not given" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
expect(response).to have_http_status(400)
end
it "returns a 401 unauthorized error if the user is not authenticated" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
expect(response).to have_http_status(401)
end
it "returns a 404 error if the user authored issue" do
post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
expect(issue.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
end
context 'on a snippet' do
it 'creates a new award emoji' do
snippet = create(:project_snippet, :public, project: project)
post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
let(:note2) { create(:note, project: project, noteable: issue, author: user) }
it 'creates a new award emoji' do
expect do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
end.to change { note.award_emoji.count }.from(0).to(1)
expect(response).to have_http_status(201)
expect(json_response['user']['username']).to eq(user.username)
end
it "it returns 404 error when user authored note" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
expect(note.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
context 'when the awardable is an Issue' do
it 'deletes the award' do
......
......@@ -1322,6 +1322,6 @@ describe API::V3::Issues, api: true do
describe 'time tracking endpoints' do
let(:issuable) { issue }
include_examples 'time tracking endpoints', 'issue'
include_examples 'V3 time tracking endpoints', 'issue'
end
end
require "spec_helper"
describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
include ApiHelpers
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
before do
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
project.team << [user, :master]
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
it 'returns 200 for a valid merge request' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
merge_request_diff = merge_request.merge_request_diffs.first
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
expect(json_response.first['id']).to eq(merge_request_diff.id)
expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
end
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/versions", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
it 'returns a 200 for a valid merge request' do
merge_request_diff = merge_request.merge_request_diffs.first
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
expect(response.status).to eq 200
expect(json_response['id']).to eq(merge_request_diff.id)
expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
end
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
expect(response).to have_http_status(404)
end
end
end
......@@ -868,7 +868,7 @@ describe API::MergeRequests, api: true do
describe 'Time tracking' do
let(:issuable) { merge_request }
include_examples 'time tracking endpoints', 'merge_request'
include_examples 'V3 time tracking endpoints', 'merge_request'
end
def mr_with_later_created_and_updated_at_time
......
......@@ -7,13 +7,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
context 'with an unauthorized user' do
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), duration: '1w') }
it_behaves_like 'an unauthorized API user'
end
it "sets the time estimate for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
expect(response).to have_http_status(200)
expect(json_response['human_time_estimate']).to eq('1w')
......@@ -21,12 +21,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe 'updating the current estimate' do
before do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
end
context 'when duration has a bad format' do
it 'does not modify the original estimate' do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: 'foo'
expect(response).to have_http_status(400)
expect(issuable.reload.human_time_estimate).to eq('1w')
......@@ -35,7 +35,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'with a valid duration' do
it 'updates the estimate' do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '3w1h'
expect(response).to have_http_status(200)
expect(issuable.reload.human_time_estimate).to eq('3w 1h')
......@@ -46,13 +46,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
context 'with an unauthorized user' do
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets the time estimate for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user)
expect(response).to have_http_status(200)
expect(json_response['time_estimate']).to eq(0)
......@@ -62,7 +62,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
context 'with an unauthorized user' do
subject do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", non_member),
duration: '2h'
end
......@@ -70,7 +70,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "add spent time for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '2h'
expect(response).to have_http_status(201)
......@@ -81,7 +81,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'subtracts time of the total spent time' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
expect(response).to have_http_status(201)
......@@ -93,7 +93,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'does not modify the total time spent' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1w'
expect(response).to have_http_status(400)
......@@ -104,13 +104,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
context 'with an unauthorized user' do
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets spent time for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
......@@ -122,7 +122,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
issuable.update_attributes!(spend_time: { duration: 1800, user: user },
time_estimate: 3600)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(1800)
......
shared_examples 'V3 time tracking endpoints' do |issuable_name|
issuable_collection_name = issuable_name.pluralize
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
context 'with an unauthorized user' do
subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
it_behaves_like 'an unauthorized API user'
end
it "sets the time estimate for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
expect(response).to have_http_status(200)
expect(json_response['human_time_estimate']).to eq('1w')
end
describe 'updating the current estimate' do
before do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
end
context 'when duration has a bad format' do
it 'does not modify the original estimate' do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
expect(response).to have_http_status(400)
expect(issuable.reload.human_time_estimate).to eq('1w')
end
end
context 'with a valid duration' do
it 'updates the estimate' do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
expect(response).to have_http_status(200)
expect(issuable.reload.human_time_estimate).to eq('3w 1h')
end
end
end
end
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
context 'with an unauthorized user' do
subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets the time estimate for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
expect(response).to have_http_status(200)
expect(json_response['time_estimate']).to eq(0)
end
end
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
context 'with an unauthorized user' do
subject do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
duration: '2h'
end
it_behaves_like 'an unauthorized API user'
end
it "add spent time for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '2h'
expect(response).to have_http_status(201)
expect(json_response['human_total_time_spent']).to eq('2h')
end
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1h'
expect(response).to have_http_status(201)
expect(json_response['total_time_spent']).to eq(3600)
end
end
context 'when time to subtract is greater than the total spent time' do
it 'does not modify the total time spent' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1w'
expect(response).to have_http_status(400)
expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
end
end
end
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
context 'with an unauthorized user' do
subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets spent time for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
end
end
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
issuable.update_attributes!(spend_time: { duration: 1800, user: user },
time_estimate: 3600)
get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(1800)
expect(json_response['time_estimate']).to eq(3600)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment