Commit 49f7a449 authored by Sean McGivern's avatar Sean McGivern

Merge branch '9-0-api-changes-ee' into 'master'

Port "V4 API: Remove deprecated MR and Issue endpoints and preserve V3 namespace"

See merge request !1166
parents f21f30ae 57b59e1e
---
title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
merge_request: 8967
author:
...@@ -51,6 +51,7 @@ following locations: ...@@ -51,6 +51,7 @@ following locations:
- [Todos](todos.md) - [Todos](todos.md)
- [Users](users.md) - [Users](users.md)
- [Validate CI configuration](ci/lint.md) - [Validate CI configuration](ci/lint.md)
- [V3 to V4](v3_to_v4.md)
- [Version](version.md) - [Version](version.md)
### Internal CI API ### Internal CI API
......
...@@ -183,7 +183,6 @@ GET /projects/:id/issues?labels=foo,bar ...@@ -183,7 +183,6 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened GET /projects/:id/issues?milestone=1.0.0&state=opened
GET /projects/:id/issues?iid=42
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
......
...@@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list ...@@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all GET /projects/:id/merge_requests?state=all
GET /projects/:id/merge_requests?iid=42 GET /projects/:id/merge_requests?iids[]=42&iids[]=43
GET /projects/:id/merge_requests?iid[]=42&iid[]=43
``` ```
Parameters: Parameters:
......
# V3 to V4 version
Our V4 API version is currently available as *Beta*! It means that V3
will still be supported and remain unchanged for now, but be aware that the following
changes are in V4:
### Changes
- `iid` filter has been removed from `projects/:id/issues`
- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
module API module API
class API < Grape::API class API < Grape::API
include APIGuard include APIGuard
version 'v3', using: :path
version %w(v3 v4), using: :path
version 'v3', using: :path do
mount ::API::V3::Issues
mount ::API::V3::MergeRequests
end
before { allow_access_with_scope :api } before { allow_access_with_scope :api }
......
...@@ -15,8 +15,6 @@ module API ...@@ -15,8 +15,6 @@ module API
labels = args.delete(:labels) labels = args.delete(:labels)
args[:label_name] = labels if match_all_labels args[:label_name] = labels if match_all_labels
args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
# TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
...@@ -99,7 +97,6 @@ module API ...@@ -99,7 +97,6 @@ module API
params do params do
optional :state, type: String, values: %w[opened closed all], default: 'all', optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues' desc: 'Return opened, closed, or all issues'
optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params use :issues_params
end end
get ":id/issues" do get ":id/issues" do
......
...@@ -2,8 +2,6 @@ module API ...@@ -2,8 +2,6 @@ module API
class MergeRequests < Grape::API class MergeRequests < Grape::API
include PaginationParams include PaginationParams
DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
before { authenticate! } before { authenticate! }
params do params do
...@@ -48,14 +46,14 @@ module API ...@@ -48,14 +46,14 @@ module API
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc', optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.' desc: 'Return merge requests sorted in `asc` or `desc` order.'
optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
use :pagination use :pagination
end end
get ":id/merge_requests" do get ":id/merge_requests" do
authorize! :read_merge_request, user_project authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations merge_requests = user_project.merge_requests.inc_notes_with_associations
merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
merge_requests = merge_requests =
case params[:state] case params[:state]
...@@ -106,228 +104,218 @@ module API ...@@ -106,228 +104,218 @@ module API
merge_request.destroy merge_request.destroy
end end
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
params do params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end end
{ ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status| desc 'Get a single merge request' do
desc 'Get a single merge request' do success Entities::MergeRequest
if status == :deprecated end
detail DEPRECATION_MESSAGE get ':id/merge_requests/:merge_request_id' do
end merge_request = find_merge_request_with_access(params[:merge_request_id])
success Entities::MergeRequest
end
get path do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end end
desc 'Get the commits of a merge request' do desc 'Get the commits of a merge request' do
success Entities::RepoCommit success Entities::RepoCommit
end end
get "#{path}/commits" do get ':id/merge_requests/:merge_request_id/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request.commits, with: Entities::RepoCommit present merge_request.commits, with: Entities::RepoCommit
end end
desc 'Show the merge request changes' do desc 'Show the merge request changes' do
success Entities::MergeRequestChanges success Entities::MergeRequestChanges
end end
get "#{path}/changes" do get ':id/merge_requests/:merge_request_id/changes' do
merge_request = find_merge_request_with_access(params[:merge_request_id]) merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end end
desc 'Update a merge request' do desc 'Update a merge request' do
success Entities::MergeRequest success Entities::MergeRequest
end end
params do params do
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen merge], optional :state_event, type: String, values: %w[close reopen merge],
desc: 'Status of the merge request' desc: 'Status of the merge request'
use :optional_params use :optional_params
at_least_one_of :title, :target_branch, :description, :assignee_id, at_least_one_of :title, :target_branch, :description, :assignee_id,
:milestone_id, :labels, :state_event, :approvals_before_merge, :milestone_id, :labels, :state_event,
:remove_source_branch, :squash :remove_source_branch, :squash
end end
put path do put ':id/merge_requests/:merge_request_id' do
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
mr_params = declared_params(include_missing: false) 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? mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid? if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
end
end end
end
desc 'Merge a merge request' do desc 'Merge a merge request' do
success Entities::MergeRequest success Entities::MergeRequest
end end
params do params do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message' optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean, optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible' desc: 'When true, the source branch will be deleted if possible'
optional :merge_when_build_succeeds, type: Boolean, optional :merge_when_build_succeeds, type: Boolean,
desc: 'When true, this merge request will be merged when the pipeline succeeds' desc: 'When true, this merge request will be merged when the pipeline succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' 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' optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end end
put "#{path}/merge" do put ':id/merge_requests/:merge_request_id/merge' do
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged # Merge request can not be merged
# because user dont have permissions to push into target branch # because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user) unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! unless merge_request.mergeable_state? not_allowed! unless merge_request.mergeable_state?
render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
if params[:sha] && merge_request.diff_head_sha != params[:sha] if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end end
if params[:squash] if params[:squash]
merge_request.update(squash: params[:squash]) merge_request.update(squash: params[:squash])
end end
merge_params = { merge_params = {
commit_message: params[:merge_commit_message], commit_message: params[:merge_commit_message],
should_remove_source_branch: params[:should_remove_source_branch] should_remove_source_branch: params[:should_remove_source_branch]
} }
if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
end
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
end end
desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
success Entities::MergeRequest end
end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = find_project_merge_request(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) 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_build_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_id])
::MergeRequest::MergeWhenPipelineSucceedsService unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
desc 'Get the comments of a merge request' do ::MergeRequest::MergeWhenPipelineSucceedsService
detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0' .new(merge_request.target_project, current_user)
success Entities::MRNote .cancel(merge_request)
end end
params do
use :pagination
end
get "#{path}/comments" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
desc 'Post a comment to a merge request' do desc 'Get the comments of a merge request' do
detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0' success Entities::MRNote
success Entities::MRNote end
end params do
params do use :pagination
requires :note, type: String, desc: 'The text of the comment' end
end get ':id/merge_requests/:merge_request_id/comments' do
post "#{path}/comments" do merge_request = find_merge_request_with_access(params[:merge_request_id])
merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
opts = { desc 'Post a comment to a merge request' do
note: params[:note], success Entities::MRNote
noteable_type: 'MergeRequest', end
noteable_id: merge_request.id 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)
note = ::Notes::CreateService.new(user_project, current_user, opts).execute opts = {
note: params[:note],
noteable_type: 'MergeRequest',
noteable_id: merge_request.id
}
if note.save note = ::Notes::CreateService.new(user_project, current_user, opts).execute
present note, with: Entities::MRNote
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
desc 'List issues that will be closed on merge' do if note.save
success Entities::MRNote present note, with: Entities::MRNote
end else
params do render_api_error!("Failed to save note #{note.errors.messages}", 400)
use :pagination
end
get "#{path}/closes_issues" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end end
end
# Get the status of the merge request's approvals desc 'List issues that will be closed on merge' do
# success Entities::MRNote
# Parameters: end
# id (required) - The ID of a project params do
# merge_request_id (required) - ID of MR use :pagination
# Examples: end
# GET /projects/:id/merge_requests/:merge_request_id/approvals get ':id/merge_requests/:merge_request_id/closes_issues' do
# merge_request = find_merge_request_with_access(params[:merge_request_id])
get "#{path}/approvals" do issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
merge_request = user_project.merge_requests.find(params[:merge_request_id]) present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end
# Approve a merge request # Get the status of the merge request's approvals
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# merge_request_id (required) - ID of MR # merge_request_id (required) - ID of MR
# Examples: # Examples:
# POST /projects/:id/merge_requests/:merge_request_id/approvals # GET /projects/:id/merge_requests/:merge_request_id/approvals
# #
post "#{path}/approve" do get ':id/merge_requests/:merge_request_id/approvals' do
merge_request = user_project.merge_requests.find(params[:merge_request_id]) merge_request = user_project.merge_requests.find(params[:merge_request_id])
unauthorized! unless merge_request.can_approve?(current_user)
::MergeRequests::ApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user authorize! :read_merge_request, merge_request
end present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end
delete "#{path}/unapprove" do # Approve a merge request
merge_request = user_project.merge_requests.find(params[:merge_request_id]) #
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - ID of MR
# Examples:
# POST /projects/:id/merge_requests/:merge_request_id/approvals
#
post ':id/merge_requests/:merge_request_id/approve' do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
not_found! unless merge_request.has_approved?(current_user) unauthorized! unless merge_request.can_approve?(current_user)
::MergeRequests::RemoveApprovalService ::MergeRequests::ApprovalService
.new(user_project, current_user) .new(user_project, current_user)
.execute(merge_request) .execute(merge_request)
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end end
delete ':id/merge_requests/:merge_request_id/unapprove' do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
not_found! unless merge_request.has_approved?(current_user)
::MergeRequests::RemoveApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end end
end end
end end
......
module API
module V3
class Issues < Grape::API
include PaginationParams
before { authenticate! }
helpers do
def find_issues(args = {})
args = params.merge(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
match_all_labels = args.delete(:match_all_labels)
labels = args.delete(:labels)
args[:label_name] = labels if match_all_labels
args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
if !match_all_labels && labels.present?
issues = issues.includes(:labels).where('labels.title' => labels.split(','))
end
issues.reorder(args[:order_by] => args[:sort])
end
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
use :pagination
end
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
end
end
resource :issues do
desc "Get currently authenticated user's issues" do
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get do
issues = find_issues(scope: 'authored')
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups do
desc 'Get a list of group issues' do
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'opened',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
group = find_group!(params[:id])
issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
detail 'iid filter is deprecated have been removed on V4'
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
project = find_project(params[:id])
issues = find_issues(project_id: project.id)
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
end
get ":id/issues/:issue_id" do
issue = find_project_issue(params[:issue_id])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
desc 'Create a new project issue' do
success Entities::Issue
end
params do
requires :title, type: String, desc: 'The title of an issue'
optional :created_at, type: DateTime,
desc: 'Date time when the issue was created. Available only for admins and project owners.'
optional :merge_request_for_resolving_discussions, type: Integer,
desc: 'The IID of a merge request for which to resolve discussions'
use :issue_params
end
post ':id/issues' do
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
end
issue_params = declared_params(include_missing: false)
if merge_request_iid = params[:merge_request_for_resolving_discussions]
issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
execute.
find_by(iid: merge_request_iid)
end
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Update an existing issue' do
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The 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.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
at_least_one_of :title, :description, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_id' do
issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:updated_at)
end
issue = ::Issues::UpdateService.new(user_project,
current_user,
declared_params(include_missing: false)).execute(issue)
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Move an existing issue' do
success Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The 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])
not_found!('Issue') unless issue
new_project = Project.find_by(id: params[:to_project_id])
not_found!('Project') unless new_project
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
present issue, with: Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
end
desc 'Delete a project issue'
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
end
delete ":id/issues/:issue_id" do
issue = user_project.issues.find_by(id: params[:issue_id])
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
issue.destroy
end
end
end
end
end
module API
module V3
class MergeRequests < Grape::API
include PaginationParams
DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
include TimeTrackingEndpoints
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
error!(errors[:project_access], 422)
elsif errors[:branch_conflict].any?
error!(errors[:branch_conflict], 422)
elsif errors[:validate_fork].any?
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
end
render_api_error!(errors, 400)
end
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :approvals_before_merge, type: Integer, desc: 'Number of approvals required before this can be merged'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :squash, type: Boolean, desc: 'Squash commits when merging'
end
end
desc 'List merge requests' do
success Entities::MergeRequest
end
params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
desc: 'Return opened, closed, merged, or all merge requests'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
merge_requests =
case params[:state]
when 'opened' then merge_requests.opened
when 'closed' then merge_requests.closed
when 'merged' then merge_requests.merged
else merge_requests
end
merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Create a merge request' do
success Entities::MergeRequest
end
params do
requires :title, type: String, desc: 'The title of the merge request'
requires :source_branch, type: String, desc: 'The source branch'
requires :target_branch, type: String, desc: 'The target branch'
optional :target_project_id, type: Integer,
desc: 'The target project of the merge request defaults to the :id of the project'
use :optional_params
end
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
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?
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
desc 'Delete a merge request'
params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
end
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
{ ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
desc 'Get a single merge request' do
if status == :deprecated
detail DEPRECATION_MESSAGE
end
success Entities::MergeRequest
end
get path do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Get the commits of a merge request' do
success Entities::RepoCommit
end
get "#{path}/commits" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request.commits, with: Entities::RepoCommit
end
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
desc 'Update a merge request' do
success Entities::MergeRequest
end
params do
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen merge],
desc: 'Status of the merge request'
use :optional_params
at_least_one_of :title, :target_branch, :description, :assignee_id,
:milestone_id, :labels, :state_event, :approvals_before_merge,
:remove_source_branch, :squash
end
put path do
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :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?
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
desc 'Merge a merge request' do
success Entities::MergeRequest
end
params do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
optional :merge_when_build_succeeds, type: Boolean,
desc: 'When true, this merge request will be merged when the pipeline succeeds'
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 "#{path}/merge" do
merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! unless merge_request.mergeable_state?
render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
if params[:squash]
merge_request.update(squash: params[:squash])
end
merge_params = {
commit_message: params[:merge_commit_message],
should_remove_source_branch: params[:should_remove_source_branch]
}
if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
end
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
success Entities::MergeRequest
end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = find_project_merge_request(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
::MergeRequest::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
desc 'Get the comments of a merge request' do
detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
success Entities::MRNote
end
params do
use :pagination
end
get "#{path}/comments" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
desc 'Post a comment to a merge request' do
detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
success Entities::MRNote
end
params do
requires :note, type: String, desc: 'The text of the comment'
end
post "#{path}/comments" do
merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
opts = {
note: params[:note],
noteable_type: 'MergeRequest',
noteable_id: merge_request.id
}
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.save
present note, with: Entities::MRNote
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
params do
use :pagination
end
get "#{path}/closes_issues" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
# Get the status of the merge request's approvals
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - ID of MR
# Examples:
# GET /projects/:id/merge_requests/:merge_request_id/approvals
#
get "#{path}/approvals" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end
# Approve a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - ID of MR
# Examples:
# POST /projects/:id/merge_requests/:merge_request_id/approvals
#
post "#{path}/approve" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
unauthorized! unless merge_request.can_approve?(current_user)
::MergeRequests::ApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end
delete "#{path}/unapprove" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
not_found! unless merge_request.has_approved?(current_user)
::MergeRequests::RemoveApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end
end
end
end
end
end
...@@ -9,6 +9,7 @@ describe GeoNode, type: :model do ...@@ -9,6 +9,7 @@ describe GeoNode, type: :model do
let(:dummy_url) { 'https://localhost:3000/gitlab' } let(:dummy_url) { 'https://localhost:3000/gitlab' }
let(:url_helpers) { Gitlab::Application.routes.url_helpers } let(:url_helpers) { Gitlab::Application.routes.url_helpers }
let(:api_version) { API::API.version }
context 'associations' do context 'associations' do
it { is_expected.to belong_to(:geo_node_key).dependent(:destroy) } it { is_expected.to belong_to(:geo_node_key).dependent(:destroy) }
...@@ -158,7 +159,7 @@ describe GeoNode, type: :model do ...@@ -158,7 +159,7 @@ describe GeoNode, type: :model do
end end
describe '#notify_projects_url' do describe '#notify_projects_url' do
let(:refresh_url) { 'https://localhost:3000/gitlab/api/v3/geo/refresh_projects' } let(:refresh_url) { "https://localhost:3000/gitlab/api/#{api_version}/geo/refresh_projects" }
it 'returns api url based on node uri' do it 'returns api url based on node uri' do
expect(new_node.notify_projects_url).to eq(refresh_url) expect(new_node.notify_projects_url).to eq(refresh_url)
...@@ -166,7 +167,7 @@ describe GeoNode, type: :model do ...@@ -166,7 +167,7 @@ describe GeoNode, type: :model do
end end
describe '#notify_wikis_url' do describe '#notify_wikis_url' do
let(:refresh_url) { 'https://localhost:3000/gitlab/api/v3/geo/refresh_wikis' } let(:refresh_url) { "https://localhost:3000/gitlab/api/#{api_version}/geo/refresh_wikis" }
it 'returns api url based on node uri' do it 'returns api url based on node uri' do
expect(new_node.notify_wikis_url).to eq(refresh_url) expect(new_node.notify_wikis_url).to eq(refresh_url)
...@@ -174,7 +175,7 @@ describe GeoNode, type: :model do ...@@ -174,7 +175,7 @@ describe GeoNode, type: :model do
end end
describe '#geo_events_url' do describe '#geo_events_url' do
let(:events_url) { 'https://localhost:3000/gitlab/api/v3/geo/receive_events' } let(:events_url) { "https://localhost:3000/gitlab/api/#{api_version}/geo/receive_events" }
it 'returns api url based on node uri' do it 'returns api url based on node uri' do
expect(new_node.geo_events_url).to eq(events_url) expect(new_node.geo_events_url).to eq(events_url)
......
...@@ -613,23 +613,6 @@ describe API::Issues, api: true do ...@@ -613,23 +613,6 @@ describe API::Issues, api: true do
expect(json_response['iid']).to eq(issue.iid) expect(json_response['iid']).to eq(issue.iid)
end end
it 'returns a project issue by iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
expect(response.status).to eq 200
expect(json_response.length).to eq 1
expect(json_response.first['title']).to eq issue.title
expect(json_response.first['id']).to eq issue.id
expect(json_response.first['iid']).to eq issue.iid
end
it 'returns an empty array for an unknown project issue iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
expect(response.status).to eq 200
expect(json_response.length).to eq 0
end
it "returns 404 if issue id not found" do it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user) get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
...@@ -74,6 +74,16 @@ describe API::MergeRequests, api: true do ...@@ -74,6 +74,16 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(merge_request_merged.title) expect(json_response.first['title']).to eq(merge_request_merged.title)
end end
it 'returns merge_request by "iids" array' do
get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq merge_request_closed.title
expect(json_response.first['id']).to eq merge_request_closed.id
end
context "with ordering" do context "with ordering" do
before do before do
@mr_later = mr_with_later_created_and_updated_at_time @mr_later = mr_with_later_created_and_updated_at_time
...@@ -160,24 +170,6 @@ describe API::MergeRequests, api: true do ...@@ -160,24 +170,6 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy expect(json_response['force_close_merge_request']).to be_falsy
end end
it 'returns merge_request by iid' do
url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
get api(url, user)
expect(response.status).to eq 200
expect(json_response.first['title']).to eq merge_request.title
expect(json_response.first['id']).to eq merge_request.id
end
it 'returns merge_request by iid array' do
get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq merge_request_closed.title
expect(json_response.first['id']).to eq merge_request_closed.id
end
it "returns a 404 error if merge_request_id not found" do it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user) get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
require 'spec_helper'
describe API::V3::Issues, api: true do
include ApiHelpers
include EmailHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
assignee: user,
project: project,
state: :closed,
milestone: milestone,
created_at: generate(:issue_created_at),
updated_at: 3.hours.ago
end
let!(:confidential_issue) do
create :issue,
:confidential,
project: project,
author: author,
assignee: assignee,
created_at: generate(:issue_created_at),
updated_at: 2.hours.ago
end
let!(:issue) do
create :issue,
author: user,
assignee: user,
project: project,
milestone: milestone,
created_at: generate(:issue_created_at),
updated_at: 1.hour.ago
end
let!(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
let!(:label_link) { create(:label_link, label: label, target: issue) }
let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let!(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { URI.escape(Milestone::None.title) }
before do
project.team << [user, :reporter]
project.team << [guest, :guest]
end
describe "GET /issues" do
context "when unauthenticated" do
it "returns authentication error" do
get v3_api("/issues")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "returns an array of issues" do
get v3_api("/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
end
it 'returns an array of closed issues' do
get v3_api('/issues?state=closed', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of opened issues' do
get v3_api('/issues?state=opened', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns an array of all issues' do
get v3_api('/issues?state=all', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
it 'returns an array of labeled issues' do
get v3_api("/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled issues when at least one label matches' do
get v3_api("/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an empty array if no issue matches labels' do
get v3_api('/issues?labels=foo,bar', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an array of labeled issues matching given state' do
get v3_api("/issues?labels=#{label.title}&state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
it 'returns an empty array if no issue matches labels and state filters' do
get v3_api("/issues?labels=#{label.title}&state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no issue matches milestone' do
get v3_api("/issues?milestone=#{empty_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if milestone does not exist' do
get v3_api("/issues?milestone=foo", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an array of issues in given milestone' do
get v3_api("/issues?milestone=#{milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
it 'returns an array of issues matching state in milestone' do
get v3_api("/issues?milestone=#{milestone.title}", user),
'&state=closed'
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get v3_api("/issues?milestone=#{no_milestone_title}", author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
end
it 'sorts by created_at descending by default' do
get v3_api('/issues', user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get v3_api('/issues?sort=asc', user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get v3_api('/issues?order_by=updated_at', user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get v3_api('/issues?order_by=updated_at&sort=asc', user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
end
end
describe "GET /groups/:id/issues" do
let!(:group) { create(:group) }
let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
let!(:group_closed_issue) do
create :closed_issue,
author: user,
assignee: user,
project: group_project,
state: :closed,
milestone: group_milestone,
updated_at: 3.hours.ago
end
let!(:group_confidential_issue) do
create :issue,
:confidential,
project: group_project,
author: author,
assignee: assignee,
updated_at: 2.hours.ago
end
let!(:group_issue) do
create :issue,
author: user,
assignee: user,
project: group_project,
milestone: group_milestone,
updated_at: 1.hour.ago
end
let!(:group_label) do
create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
end
let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
let!(:group_empty_milestone) do
create(:milestone, title: '4.0.0', project: group_project)
end
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
before do
group_project.team << [user, :reporter]
end
let(:base_url) { "/groups/#{group.id}/issues" }
it 'returns group issues without confidential issues for non project members' do
get v3_api(base_url, non_member)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(group_issue.title)
end
it 'returns group confidential issues for author' do
get v3_api(base_url, author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'returns group confidential issues for assignee' do
get v3_api(base_url, assignee)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'returns group issues with confidential issues for project members' do
get v3_api(base_url, user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'returns group confidential issues for admin' do
get v3_api(base_url, admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'returns an array of labeled group issues' do
get v3_api("#{base_url}?labels=#{group_label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues where all labels match' do
get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no group issue matches labels' do
get v3_api("#{base_url}?labels=foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no issue matches milestone' do
get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if milestone does not exist' do
get v3_api("#{base_url}?milestone=foo", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an array of issues in given milestone' do
get v3_api("#{base_url}?milestone=#{group_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of issues matching state in milestone' do
get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
'&state=closed'
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get v3_api("#{base_url}?milestone=#{no_milestone_title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_confidential_issue.id)
end
it 'sorts by created_at descending by default' do
get v3_api(base_url, user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get v3_api("#{base_url}?sort=asc", user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get v3_api("#{base_url}?order_by=updated_at", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
get v3_api("/projects/#{private_project.id}/issues", non_member)
expect(response).to have_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
create(:issue, project: restricted_project)
get v3_api("/projects/#{restricted_project.id}/issues", non_member)
expect(json_response).to eq([])
end
it 'returns project issues without confidential issues for non project members' do
get v3_api("#{base_url}/issues", non_member)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project issues without confidential issues for project members with guest role' do
get v3_api("#{base_url}/issues", guest)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for author' do
get v3_api("#{base_url}/issues", author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for assignee' do
get v3_api("#{base_url}/issues", assignee)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project issues with confidential issues for project members' do
get v3_api("#{base_url}/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for admin' do
get v3_api("#{base_url}/issues", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns an array of labeled project issues' do
get v3_api("#{base_url}/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an array of labeled project issues where all labels match' do
get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
end
it 'returns an empty array if no project issue matches labels' do
get v3_api("#{base_url}/issues?labels=foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no issue matches milestone' do
get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an empty array if milestone does not exist' do
get v3_api("#{base_url}/issues?milestone=foo", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'returns an array of issues in given milestone' do
get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
it 'returns an array of issues matching state in milestone' do
get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
'&state=closed'
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
end
it 'sorts by created_at descending by default' do
get v3_api("#{base_url}/issues", user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get v3_api("#{base_url}/issues?sort=asc", user)
response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
it 'sorts by updated_at descending when requested' do
get v3_api("#{base_url}/issues?order_by=updated_at", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts by updated_at ascending when requested' do
get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
end
describe "GET /projects/:id/issues/:issue_id" do
it 'exposes known attributes' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
expect(json_response['title']).to eq(issue.title)
expect(json_response['description']).to eq(issue.description)
expect(json_response['state']).to eq(issue.state)
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
end
it "returns a project issue by id" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
it 'returns a project issue by iid' do
get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
expect(response.status).to eq 200
expect(json_response.length).to eq 1
expect(json_response.first['title']).to eq issue.title
expect(json_response.first['id']).to eq issue.id
expect(json_response.first['iid']).to eq issue.iid
end
it 'returns an empty array for an unknown project issue iid' do
get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
expect(response.status).to eq 200
expect(json_response.length).to eq 0
end
it "returns 404 if issue id not found" do
get v3_api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
end
context 'confidential issues' do
it "returns 404 for non project members" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
expect(response).to have_http_status(404)
end
it "returns 404 for project members with guest role" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
expect(response).to have_http_status(404)
end
it "returns confidential issue for project members" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for author" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for assignee" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for admin" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
end
end
describe "POST /projects/:id/issues" do
it 'creates a new project issue' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(['label', 'label2'])
expect(json_response['confidential']).to be_falsy
end
it 'creates a new confidential project issue' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: true
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a new confidential project issue with a different param' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'y'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
it 'creates a public issue when confidential param is false' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: false
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
it 'creates a public issue when confidential param is invalid' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
it "sends notifications for subscribers of newly added labels" do
label = project.labels.first
label.toggle_subscription(user2, project)
perform_enqueued_jobs do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: label.title
end
should_email(user2)
end
it "returns a 400 bad request if title not given" do
post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
expect(response).to have_http_status(400)
end
it 'allows special label names' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'g' * 256
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
context 'resolving issues in a merge request' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
project.team << [user, :master]
post v3_api("/projects/#{project.id}/issues", user),
title: 'New Issue',
merge_request_for_resolving_discussions: merge_request.iid
end
it 'creates a new project issue' do
expect(response).to have_http_status(:created)
end
it 'resolves the discussions in a merge request' do
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'assigns a description to the issue mentioning the merge request' do
expect(json_response['description']).to include(merge_request.to_reference)
end
end
context 'with due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', due_date: due_date
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['due_date']).to eq(due_date)
end
end
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: creation_time
expect(response).to have_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
context 'the user can only read the issue' do
it 'cannot create new labels' do
expect do
post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2'
end.not_to change { project.labels.count }
end
end
end
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
end
let(:params) do
{
title: 'new issue',
description: 'content here',
labels: 'label, label2'
}
end
it "does not create a new project issue" do
expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
expect(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('new issue')
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
end
end
describe "PUT /projects/:id/issues/:issue_id to update only title" do
it "updates a project issue" do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "returns 404 error if issue id not found" do
put v3_api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
it 'allows special label names' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'confidential issues' do
it "returns 403 for non project members" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "returns 403 for project members with guest role" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "updates a confidential issue for project members" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for author" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for admin" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'sets an issue to confidential' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
confidential: true
expect(response).to have_http_status(200)
expect(json_response['confidential']).to be_truthy
end
it 'makes a confidential issue public' do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: false
expect(response).to have_http_status(200)
expect(json_response['confidential']).to be_falsy
end
it 'does not update a confidential issue with wrong confidential flag' do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: 'foo'
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
end
end
describe 'PUT /projects/:id/issues/:issue_id to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'does not update labels if not present' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
it "sends notifications for subscribers of newly added labels when issue is updated" do
label = create(:label, title: 'foo', color: '#FFAABB', project: project)
label.toggle_subscription(user2, project)
perform_enqueued_jobs do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title', labels: label.title
end
should_email(user2)
end
it 'removes all labels' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
it 'updates labels' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'foo,bar'
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
end
it 'allows special label names' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'returns 400 if title is too long' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'g' * 256
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
end
end
describe "PUT /projects/:id/issues/:issue_id to update state and label" do
it "updates a project issue" do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label2', state_event: "close"
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
end
it 'reopens a project isssue' do
put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'reopened'
end
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
end
describe 'PUT /projects/:id/issues/:issue_id to update due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
expect(response).to have_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
describe "DELETE /projects/:id/issues/:issue_id" do
it "rejects a non member from deleting an issue" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
expect(response).to have_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
expect(response).to have_http_status(403)
end
context "when the user is project owner" do
let(:owner) { create(:user) }
let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'opened'
end
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
delete v3_api("/projects/#{project.id}/issues/123", user)
expect(response).to have_http_status(404)
end
end
end
describe '/projects/:id/issues/:issue_id/move' do
let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: project.id
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project2.id
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
it 'moves the issue to another namespace if I am admin' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
to_project_id: target_project2.id
expect(response).to have_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/123/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/123/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: 123
expect(response).to have_http_status(404)
end
end
end
describe 'POST :id/issues/:issue_id/subscription' do
it 'subscribes to an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
post v3_api("/projects/#{project.id}/issues/123/subscription", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
expect(response).to have_http_status(404)
end
end
describe 'DELETE :id/issues/:issue_id/subscription' do
it 'unsubscribes from an issue' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
expect(response).to have_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
expect(response).to have_http_status(404)
end
end
describe 'time tracking endpoints' do
let(:issuable) { issue }
include_examples 'time tracking endpoints', 'issue'
end
end
require "spec_helper"
describe API::MergeRequests, v3_api: true do
include ApiHelpers
let(:base_time) { Time.now }
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:non_member) { create(:user) }
let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
project.team << [user, :reporter]
end
describe "GET /projects/:id/merge_requests" do
context "when unauthenticated" do
it "returns authentication error" do
get v3_api("/projects/#{project.id}/merge_requests")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
it "returns an array of all merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
expect(json_response.last['merge_commit_sha']).to be_nil
expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
expect(json_response.first['title']).to eq(merge_request_merged.title)
expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
expect(json_response.first['merge_commit_sha']).not_to be_nil
expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
expect(json_response.first['squash']).to eq(merge_request_merged.squash)
end
it "returns an array of all merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of open merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of closed merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
it "returns an array of merged merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=merged", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@mr_earlier = mr_with_earlier_created_and_updated_at_time
end
it "returns an array of merge_requests in ascending order" do
get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
it "returns an array of merge_requests in descending order" do
get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
it "returns an array of merge_requests ordered by updated_at" do
get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
it "returns an array of merge_requests ordered by created_at" do
get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
end
end
end
describe "GET /projects/:id/merge_requests/:merge_request_id" do
it 'exposes known attributes' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['project_id']).to eq(merge_request.project.id)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['description']).to eq(merge_request.description)
expect(json_response['state']).to eq(merge_request.state)
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(merge_request.label_names)
expect(json_response['milestone']).to be_nil
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['target_branch']).to eq(merge_request.target_branch)
expect(json_response['source_branch']).to eq(merge_request.source_branch)
expect(json_response['upvotes']).to eq(0)
expect(json_response['downvotes']).to eq(0)
expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
end
it "returns merge_request" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
end
it 'returns merge_request by iid' do
url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
get v3_api(url, user)
expect(response.status).to eq 200
expect(json_response.first['title']).to eq merge_request.title
expect(json_response.first['id']).to eq merge_request.id
end
it 'returns merge_request by iid array' do
get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq merge_request_closed.title
expect(json_response.first['id']).to eq merge_request_closed.id
end
it "returns a 404 error if merge_request_id not found" do
get v3_api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
end
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
it "returns merge_request" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
expect(response).to have_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
it 'returns a 200 when merge request is valid' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
commit = merge_request.commits.first
expect(response.status).to eq 200
expect(json_response.size).to eq(merge_request.commits.size)
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['title']).to eq(commit.title)
end
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
it 'returns the change information of the merge_request' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
end
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/merge_requests" do
context 'between branches projects' do
it "returns merge_request" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user,
labels: 'label, label2',
milestone_id: milestone.id,
remove_source_branch: true,
squash: true
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(['label', 'label2'])
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['force_remove_source_branch']).to be_truthy
expect(json_response['squash']).to be_truthy
end
it "returns 422 when source_branch equals target_branch" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
expect(response).to have_http_status(422)
end
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
expect(response).to have_http_status(400)
end
it "returns 400 when target_branch is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
expect(response).to have_http_status(400)
end
it "returns 400 when title is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
expect(response).to have_http_status(400)
end
it 'allows special label names' do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'markdown',
target_branch: 'master',
author: user,
labels: 'label, label?, label&foo, ?, &'
expect(response.status).to eq(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
context 'with existing MR' do
before do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user
@mr = MergeRequest.all.last
end
it 'returns 409 when MR already exists for source/target' do
expect do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'New test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user
end.to change { MergeRequest.count }.by(0)
expect(response).to have_http_status(409)
end
end
end
context 'forked projects' do
let!(:user2) { create(:user) }
let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
fork_project.team << [user2, :reporter]
end
it "returns merge_request" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['description']).to eq('Test description for Test merge_request')
end
it "does not return 422 when source_branch equals target_branch" do
expect(project.id).not_to eq(fork_project.id)
expect(fork_project.forked?).to be_truthy
expect(fork_project.forked_from_project).to eq(project)
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
end
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
it "returns 400 when target_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
it "returns 400 when title is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
context 'when target_branch is specified' do
it 'returns 422 if not a forked project' do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
author: user,
target_project_id: fork_project.id
expect(response).to have_http_status(422)
end
it 'returns 422 if targeting a different fork' do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
source_branch: 'markdown',
author: user2,
target_project_id: unrelated_project.id
expect(response).to have_http_status(422)
end
end
it "returns 201 when target_branch is specified and for the same project" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
expect(response).to have_http_status(201)
end
end
context 'the approvals_before_merge param' do
def create_merge_request(approvals_before_merge)
post v3_api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author: user,
labels: 'label, label2',
milestone_id: milestone.id,
approvals_before_merge: approvals_before_merge
end
context 'when the target project has approvals_before_merge set to zero' do
before do
project.update_attributes(approvals_before_merge: 0)
create_merge_request(1)
end
it 'returns a 400' do
expect(response).to have_http_status(400)
end
it 'includes the error in the response' do
expect(json_response['message']['validate_approvals_before_merge']).not_to be_empty
end
end
context 'when the target project has a non-zero approvals_before_merge' do
context 'when the approvals_before_merge param is less than or equal to the value in the target project' do
before do
project.update_attributes(approvals_before_merge: 1)
create_merge_request(1)
end
it 'returns a 400' do
expect(response).to have_http_status(400)
end
it 'includes the error in the response' do
expect(json_response['message']['validate_approvals_before_merge']).not_to be_empty
end
end
context 'when the approvals_before_merge param is greater than the value in the target project' do
before do
project.update_attributes(approvals_before_merge: 1)
create_merge_request(2)
end
it 'returns a created status' do
expect(response).to have_http_status(201)
end
it 'sets approvals_before_merge of the newly-created MR' do
expect(json_response['approvals_before_merge']).to eq(2)
end
end
end
end
end
describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
context "when the user is developer" do
let(:developer) { create(:user) }
before do
project.team << [developer, :developer]
end
it "denies the deletion of the merge request" do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
expect(response).to have_http_status(403)
end
end
context "when the user is project owner" do
it "destroys the merge request owners can destroy" do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(200)
end
end
end
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
it "returns merge_request in case of success" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(200)
end
it "returns 406 if branch can't be merged" do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
end
it "returns 405 if merge_request is not open" do
merge_request.close
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it 'returns 405 if the build failed for a merge request that requires success' do
allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
it "returns 409 if the SHA parameter doesn't match" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
expect(response).to have_http_status(409)
expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
end
it "succeeds if the SHA parameter matches" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
expect(response).to have_http_status(200)
end
it "updates the MR's squash attribute" do
expect do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), squash: true
end.to change { merge_request.reload.squash }
expect(response).to have_http_status(200)
end
it "enables merge when pipeline succeeds if the pipeline is active" do
allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
allow(pipeline).to receive(:active?).and_return(true)
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_build_succeeds']).to eq(true)
end
end
describe "PUT /projects/:id/merge_requests/:merge_request_id" do
context "to close a MR" do
it "returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
end
end
it "updates title and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('New title')
end
it "updates description and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
expect(response).to have_http_status(200)
expect(json_response['description']).to eq('New description')
end
it "updates milestone_id and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
expect(response).to have_http_status(200)
expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "updates squash and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", 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 v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
it "returns merge_request that removes the source branch" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
expect(response).to have_http_status(200)
expect(json_response['force_remove_source_branch']).to be_truthy
end
it 'allows special label names' do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
it 'does not update state when title is empty' do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
merge_request.reload
expect(response).to have_http_status(400)
expect(merge_request.state).to eq('opened')
end
it 'does not update state when target_branch is empty' do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
merge_request.reload
expect(response).to have_http_status(400)
expect(merge_request.state).to eq('opened')
end
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
it "returns comment" do
original_count = merge_request.notes.size
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['author']['name']).to eq(user.name)
expect(json_response['author']['username']).to eq(user.username)
expect(merge_request.reload.notes.size).to eq(original_count + 1)
end
it "returns 400 if note is missing" do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(400)
end
it "returns 404 if note is attached to non existent merge request" do
post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
expect(response).to have_http_status(404)
end
end
describe "GET :id/merge_requests/:merge_request_id/comments" do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
it "returns merge_request comments ordered by created_at" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq("a comment on a MR")
expect(json_response.first['author']['id']).to eq(user.id)
expect(json_response.last['note']).to eq("another comment on a MR")
end
it "returns a 404 error if merge_request_id not found" do
get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response).to have_http_status(404)
end
end
describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
it 'returns the issue that will be closed on merge' do
issue = create(:issue, project: project)
mr = merge_request.tap do |mr|
mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
end
get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns an empty array when there are no issues to be closed' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
it 'handles external issues' do
jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns 403 if the user has no access to the merge request' do
project = create(:empty_project, :private)
merge_request = create(:merge_request, :simple, source_project: project)
guest = create(:user)
project.team << [guest, :guest]
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
expect(response).to have_http_status(403)
end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
it 'subscribes to a merge request' do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response).to have_http_status(404)
end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
it 'unsubscribes from a merge request' do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response).to have_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response).to have_http_status(404)
end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end
describe 'GET :id/merge_requests/:merge_request_id/approvals' do
it 'retrieves the approval status' do
approver = create :user
project.update_attribute(:approvals_before_merge, 2)
project.team << [approver, :developer]
project.team << [create(:user), :developer]
merge_request.approvals.create(user: approver)
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approvals", user)
expect(response.status).to eq(200)
expect(json_response['approvals_required']).to eq 2
expect(json_response['approvals_left']).to eq 1
expect(json_response['approved_by'][0]['user']['username']).to eq(approver.username)
expect(json_response['user_can_approve']).to be false
expect(json_response['user_has_approved']).to be false
end
end
describe 'POST :id/merge_requests/:merge_request_id/approve' do
before { project.update_attribute(:approvals_before_merge, 2) }
context 'as the author of the merge request' do
before { post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", user) }
it 'returns a 401' do
expect(response).to have_http_status(401)
end
end
context 'as a valid approver' do
let(:approver) { create(:user) }
before do
project.team << [approver, :developer]
project.team << [create(:user), :developer]
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", approver)
end
it 'approves the merge request' do
expect(response.status).to eq(201)
expect(json_response['approvals_left']).to eq(1)
expect(json_response['approved_by'][0]['user']['username']).to eq(approver.username)
expect(json_response['user_has_approved']).to be true
end
end
end
describe 'DELETE :id/merge_requests/:merge_request_id/unapprove' do
before { project.update_attribute(:approvals_before_merge, 2) }
context 'as a user who has approved the merge request' do
let(:approver) { create(:user) }
let(:unapprover) { create(:user) }
before do
project.team << [approver, :developer]
project.team << [unapprover, :developer]
project.team << [create(:user), :developer]
merge_request.approvals.create(user: approver)
merge_request.approvals.create(user: unapprover)
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unapprove", unapprover)
end
it 'unapproves the merge request' do
expect(response.status).to eq(200)
expect(json_response['approvals_left']).to eq(1)
usernames = json_response['approved_by'].map { |u| u['user']['username'] }
expect(usernames).not_to include(unapprover.username)
expect(usernames.size).to be 1
expect(json_response['user_has_approved']).to be false
expect(json_response['user_can_approve']).to be true
end
end
end
describe 'Time tracking' do
let(:issuable) { merge_request }
include_examples 'time tracking endpoints', 'merge_request'
end
def mr_with_later_created_and_updated_at_time
merge_request
merge_request.created_at += 1.hour
merge_request.updated_at += 30.minutes
merge_request.save
merge_request
end
def mr_with_earlier_created_and_updated_at_time
merge_request_closed
merge_request_closed.created_at -= 1.hour
merge_request_closed.updated_at -= 30.minutes
merge_request_closed.save
merge_request_closed
end
end
...@@ -17,8 +17,8 @@ module ApiHelpers ...@@ -17,8 +17,8 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..." # => "/api/v2/issues?foo=bar&private_token=..."
# #
# Returns the relative path to the requested API resource # Returns the relative path to the requested API resource
def api(path, user = nil) def api(path, user = nil, version: API::API.version)
"/api/#{API::API.version}#{path}" + "/api/#{version}#{path}" +
# Normalize query string # Normalize query string
(path.index('?') ? '' : '?') + (path.index('?') ? '' : '?') +
...@@ -31,6 +31,11 @@ module ApiHelpers ...@@ -31,6 +31,11 @@ module ApiHelpers
end end
end end
# Temporary helper method for simplifying V3 exclusive API specs
def v3_api(path, user = nil)
api(path, user, version: 'v3')
end
def ci_api(path, user = nil) def ci_api(path, user = nil)
"/ci/api/v1/#{path}" + "/ci/api/v1/#{path}" +
......
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