Commit 13cc2966 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'prevent-author-from-being-an-approver' into 'master'

Prevent author from being an approver

@dbalexandre can you review this please? Background: https://gitlab.com/gitlab-org/gitlab-ee/issues/388#note_12743940

I'd like to apply commit https://gitlab.com/gitlab-org/gitlab-ee/commit/3758f751906ad76a47a40c653c1b07c767772260 to CE, as it doesn't need to be EE-specific. I'd also really like to not have to do https://gitlab.com/gitlab-org/gitlab-ee/commit/ee751a5963937c25ba456311d26bf4103946ac5f but that comment requires it 😕 

See merge request !560
parents 93273e15 0ab86d46
...@@ -4,6 +4,7 @@ v 8.10.0 (unreleased) ...@@ -4,6 +4,7 @@ v 8.10.0 (unreleased)
- Rename Git Hooks to Push Rules - Rename Git Hooks to Push Rules
- Fix EE keys fingerprint add index migration if came from CE - Fix EE keys fingerprint add index migration if came from CE
- Add todos for MR approvers !547 - Add todos for MR approvers !547
- Prevent the author of an MR from being on the approvers list
- Isolate EE LDAP library code in EE module (Part 1) !511 - Isolate EE LDAP library code in EE module (Part 1) !511
- Make Elasticsearch indexer run as an async task - Make Elasticsearch indexer run as an async task
......
...@@ -220,6 +220,7 @@ class @UsersSelect ...@@ -220,6 +220,7 @@ class @UsersSelect
@showCurrentUser = $(select).data('current-user') @showCurrentUser = $(select).data('current-user')
@pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches') @pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches')
@authorId = $(select).data('author-id') @authorId = $(select).data('author-id')
@skipUsers = $(select).data('skip-users')
showNullUser = $(select).data('null-user') showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user') showAnyUser = $(select).data('any-user')
showEmailUser = $(select).data('email-user') showEmailUser = $(select).data('email-user')
...@@ -328,6 +329,7 @@ class @UsersSelect ...@@ -328,6 +329,7 @@ class @UsersSelect
current_user: @showCurrentUser current_user: @showCurrentUser
push_code_to_protected_branches: @pushCodeToProtectedBranches push_code_to_protected_branches: @pushCodeToProtectedBranches
author_id: @authorId author_id: @authorId
skip_users: @skipUsers
dataType: "json" dataType: "json"
).done (users) -> ).done (users) ->
callback(users) callback(users)
......
...@@ -5,6 +5,7 @@ class AutocompleteController < ApplicationController ...@@ -5,6 +5,7 @@ class AutocompleteController < ApplicationController
def users def users
@users = @users.non_ldap if params[:skip_ldap] == 'true' @users = @users.non_ldap if params[:skip_ldap] == 'true'
@users = @users.search(params[:search]) if params[:search].present? @users = @users.search(params[:search]) if params[:search].present?
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active @users = @users.active
@users = @users.reorder(:name) @users = @users.reorder(:name)
......
...@@ -462,8 +462,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -462,8 +462,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def set_suggested_approvers def set_suggested_approvers
if @merge_request.requires_approve? if @merge_request.requires_approve?
@suggested_approvers = Gitlab::AuthorityAnalyzer.new( @suggested_approvers = Gitlab::AuthorityAnalyzer.new(
@merge_request, @merge_request
current_user
).calculate(@merge_request.approvals_required) ).calculate(@merge_request.approvals_required)
end end
end end
......
...@@ -5,21 +5,9 @@ module SelectsHelper ...@@ -5,21 +5,9 @@ module SelectsHelper
css_class << "skip_ldap " if opts[:skip_ldap] css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '') css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
first_user = opts[:first_user] && current_user ? current_user.username : false
html = { html = {
class: css_class, class: css_class,
data: { data: users_select_data_attributes(opts)
placeholder: opts[:placeholder] || 'Search for a user',
null_user: opts[:null_user] || false,
any_user: opts[:any_user] || false,
email_user: opts[:email_user] || false,
first_user: first_user,
current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || ''
}
} }
unless opts[:scope] == :all unless opts[:scope] == :all
...@@ -85,4 +73,20 @@ module SelectsHelper ...@@ -85,4 +73,20 @@ module SelectsHelper
hidden_field_tag(id, value, class: css_class) hidden_field_tag(id, value, class: css_class)
end end
private
def users_select_data_attributes(opts)
{
placeholder: opts[:placeholder] || 'Search for a user',
null_user: opts[:null_user] || false,
any_user: opts[:any_user] || false,
email_user: opts[:email_user] || false,
first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || '',
skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
}
end
end end
...@@ -560,52 +560,95 @@ class MergeRequest < ActiveRecord::Base ...@@ -560,52 +560,95 @@ class MergeRequest < ActiveRecord::Base
locked_at.nil? || locked_at < (Time.now - 1.day) locked_at.nil? || locked_at < (Time.now - 1.day)
end end
def approvals_left def requires_approve?
approvals_required - approvals.count approvals_required.nonzero?
end end
def approvers_left def approved?
user_ids = overall_approvers.map(&:user_id) - approvals.map(&:user_id) approvals_left < 1
User.where id: user_ids end
# Number of approvals remaining (excluding existing approvals) before the MR is
# considered approved. If there are fewer potential approvers than approvals left,
# choose the lower so the MR doesn't get 'stuck' in a state where it can't be approved.
#
def approvals_left
[approvals_required - approvals.count, number_of_potential_approvers].min
end end
def approvals_required def approvals_required
approvals_before_merge || target_project.approvals_before_merge approvals_before_merge || target_project.approvals_before_merge
end end
def requires_approve? # An MR can potentially be approved by:
approvals_required.nonzero? # - anyone in the approvers list
end # - any other project member with developer access or higher (if there are no approvers
# left)
#
# It cannot be approved by:
# - a user who has already approved the MR
# - the MR author
#
def number_of_potential_approvers
has_access = ['access_level > ?', Member::REPORTER]
wheres = [
"id IN (#{overall_approvers.select(:user_id).to_sql})",
"id IN (#{project.members.where(has_access).select(:user_id).to_sql})"
]
def overall_approvers if project.group
if approvers.any? wheres << "id IN (#{project.group.members.where(has_access).select(:user_id).to_sql})"
approvers
else
target_project.approvers
end
end end
def approved? User.where("(#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql}) AND id != #{author.id}").count
approvals_left < 1
end end
def approved_by?(user) # Users in the list of approvers who have not already approved this MR.
approved_by_users.include?(user) #
def approvers_left
User.where(id: overall_approvers.select(:user_id)).where.not(id: approvals.select(:user_id))
end end
def approved_by_users # The list of approvers from either this MR (if they've been set on the MR) or the
approvals.map(&:user) # target project. Excludes the author by default.
#
# Before a merge request has been created, author will be nil, so pass the current user
# on the MR create page.
#
def overall_approvers(exclude_user: nil)
exclude_user ||= author
approvers_relation = approvers.any? ? approvers : target_project.approvers
exclude_user ? approvers_relation.where.not(user_id: exclude_user.id) : approvers_relation
end end
def can_approve?(user) def can_approve?(user)
approvers_left.include?(user) || return true if approvers_left.include?(user)
(any_approver_allowed? && !approved_by?(user)) return false if user == author
return false unless user.can?(:update_merge_request, self)
any_approver_allowed? && approvals.where(user: user).empty?
end end
# Once there are fewer approvers left in the list than approvals required, allow other
# project members to approve the MR.
#
def any_approver_allowed? def any_approver_allowed?
approvals_left > approvers_left.count approvals_left > approvers_left.count
end end
def approved_by_users
approvals.map(&:user)
end
def approver_ids=(value)
value.split(",").map(&:strip).each do |user_id|
next if author && user_id == author.id
approvers.find_or_initialize_by(user_id: user_id, target_id: id)
end
end
def has_ci? def has_ci?
source_project.ci_service && commits.any? source_project.ci_service && commits.any?
end end
...@@ -644,12 +687,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -644,12 +687,6 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def approver_ids=(value)
value.split(",").map(&:strip).each do |user_id|
approvers.find_or_initialize_by(user_id: user_id, target_id: id)
end
end
def state_icon_name def state_icon_name
if merged? if merged?
"check" "check"
......
...@@ -114,7 +114,9 @@ ...@@ -114,7 +114,9 @@
= f.label :approver_ids, class: 'control-label' do = f.label :approver_ids, class: 'control-label' do
Approvers Approvers
.col-sm-10 .col-sm-10
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', scope: :all, email_user: true) - author = @merge_request.author || current_user
- skip_users = @merge_request.overall_approvers.map(&:user) + [author]
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', scope: :all, email_user: true, skip_users: skip_users)
.help-block .help-block
This merge request must be approved by these users. This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers. You can override the project settings by setting your own list of approvers.
...@@ -123,25 +125,21 @@ ...@@ -123,25 +125,21 @@
.panel-heading .panel-heading
Approvers Approvers
%ul.well-list.approver-list %ul.well-list.approver-list
- if @merge_request.new_record? - using_project_approvers = @merge_request.approvers.empty?
- @merge_request.target_project.approvers.each do |approver| - item_class = 'project-approvers' if using_project_approvers
%li.project-approvers{id: dom_id(approver.user)} - @merge_request.overall_approvers(exclude_user: author).each do |approver|
%li{id: dom_id(approver.user), class: item_class}
= link_to approver.user.name, approver.user = link_to approver.user.name, approver.user
.pull-right .pull-right
- if using_project_approvers
= link_to "#", data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, class: "btn-xs btn btn-remove", title: 'Remove approver' do = link_to "#", data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, class: "btn-xs btn btn-remove", title: 'Remove approver' do
= icon("sign-out") = icon("sign-out")
Remove Remove
- if @merge_request.target_project.approvers.empty?
%li.no-approvers There are no approvers
- else - else
- @merge_request.approvers.each do |approver|
%li{id: dom_id(approver.user)}
= link_to approver.user.name, approver.user
.pull-right
= link_to namespace_project_merge_request_approver_path(@project.namespace, @project, @merge_request, approver), data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove approver' do = link_to namespace_project_merge_request_approver_path(@project.namespace, @project, @merge_request, approver), data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove approver' do
= icon("sign-out") = icon("sign-out")
Remove Remove
- if @merge_request.approvers.empty? - if @merge_request.overall_approvers.empty?
%li.no-approvers There are no approvers %li.no-approvers There are no approvers
.help-block.suggested-approvers .help-block.suggested-approvers
- if @suggested_approvers.any? - if @suggested_approvers.any?
......
...@@ -33,7 +33,8 @@ independent of changes to the merge request. ...@@ -33,7 +33,8 @@ independent of changes to the merge request.
### Approvers ### Approvers
At approvers you can define the default set of users that need to approve a At approvers you can define the default set of users that need to approve a
merge request. merge request. The author of a merge request cannot be set as an approver for
that merge request.
If there are more approvers than required approvals, any subset of these users If there are more approvers than required approvals, any subset of these users
can approve the merge request. can approve the merge request.
......
...@@ -320,28 +320,13 @@ Feature: Project Merge Requests ...@@ -320,28 +320,13 @@ Feature: Project Merge Requests
And I click link "Close" And I click link "Close"
Then I should see closed merge request "Bug NS-04" Then I should see closed merge request "Bug NS-04"
Scenario: I approve merge request Scenario: Developer can approve merge request
Given merge request 'Bug NS-04' must be approved Given I am a "Shop" developer
And I click link "Bug NS-04"
And I should not see merge button
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
Scenario: Reporter can approve merge request
Given I am a "Shop" reporter
And I visit project "Shop" merge requests page And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04" And I click link "Bug NS-04"
And I should not see merge button And I should not see merge button
When I click link "Approve" When I click link "Approve"
Then I should see message that merge request can be merged
Scenario: I approve merge request if I am an approver
Given merge request 'Bug NS-04' must be approved by current user
And I click link "Bug NS-04"
And I should not see merge button
And I should see message that MR require an approval from me
When I click link "Approve"
Then I should see approved merge request "Bug NS-04" Then I should see approved merge request "Bug NS-04"
Scenario: I can not approve merge request if I am not an approver Scenario: I can not approve merge request if I am not an approver
...@@ -353,15 +338,19 @@ Feature: Project Merge Requests ...@@ -353,15 +338,19 @@ Feature: Project Merge Requests
@javascript @javascript
Scenario: I see suggested approvers on new merge request form Scenario: I see suggested approvers on new merge request form
Given project settings contain list of approvers Given I am a "Shop" developer
And project settings contain list of approvers
And I visit project "Shop" merge requests page
When I click link "New Merge Request" When I click link "New Merge Request"
And I select "feature_conflict" as source And I select "feature_conflict" as source
Then I see suggested approver Then I see suggested approver
@javascript @javascript
Scenario: I see auto-suggested approvers on new merge request form Scenario: I see auto-suggested approvers on new merge request form
Given project settings contain list of approvers Given I am a "Shop" developer
And project settings contain list of approvers
And there is one auto-suggested approver And there is one auto-suggested approver
And I visit project "Shop" merge requests page
When I click link "New Merge Request" When I click link "New Merge Request"
And I select "feature_conflict" as source And I select "feature_conflict" as source
Then I see auto-suggested approver Then I see auto-suggested approver
......
...@@ -641,10 +641,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -641,10 +641,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content('diff --git') expect(page).to have_content('diff --git')
end end
step 'I am a "Shop" reporter' do step 'I am a "Shop" developer' do
user = create(:user, name: "Mike") user = create(:user, name: "Mike")
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
project.team << [user, :reporter] project.team << [user, :developer]
logout logout
login_with user login_with user
......
...@@ -2,9 +2,8 @@ module Gitlab ...@@ -2,9 +2,8 @@ module Gitlab
class AuthorityAnalyzer class AuthorityAnalyzer
COMMITS_TO_CONSIDER = 5 COMMITS_TO_CONSIDER = 5
def initialize(merge_request, current_user) def initialize(merge_request)
@merge_request = merge_request @merge_request = merge_request
@current_user = current_user
@users = Hash.new(0) @users = Hash.new(0)
end end
...@@ -12,7 +11,7 @@ module Gitlab ...@@ -12,7 +11,7 @@ module Gitlab
involved_users involved_users
# Picks most active users from hash like: {user1: 2, user2: 6} # Picks most active users from hash like: {user1: 2, user2: 6}
@users.sort_by { |user, count| count }.map(&:first).take(number_of_approvers) @users.sort_by { |user, count| -count }.map(&:first).take(number_of_approvers)
end end
private private
...@@ -22,7 +21,9 @@ module Gitlab ...@@ -22,7 +21,9 @@ module Gitlab
list_of_involved_files.each do |path| list_of_involved_files.each do |path|
@repo.commits(@merge_request.target_branch, path: path, limit: COMMITS_TO_CONSIDER).each do |commit| @repo.commits(@merge_request.target_branch, path: path, limit: COMMITS_TO_CONSIDER).each do |commit|
@users[commit.author] += 1 if commit.author if commit.author && commit.author != @merge_request.author
@users[commit.author] += 1
end
end end
end end
end end
......
...@@ -176,4 +176,18 @@ describe AutocompleteController do ...@@ -176,4 +176,18 @@ describe AutocompleteController do
expect(body.collect { |u| u['id'] }).not_to include(99999) expect(body.collect { |u| u['id'] }).not_to include(99999)
end end
end end
context 'skip_users parameter included' do
before { sign_in(user) }
let(:response_user_ids) { JSON.parse(response.body).map { |user| user['id'] } }
it 'skips the user IDs passed' do
get(:users, skip_users: [user, user2].map(&:id))
expect(response_user_ids).not_to include(user.id)
expect(response_user_ids).not_to include(user2.id)
expect(response_user_ids).not_to be_empty
end
end
end end
require 'rails_helper'
feature 'Merge request approvals', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, approvals_before_merge: 1) }
context 'when editing an MR with a different author' do
let(:author) { create(:user) }
let(:merge_request) { create(:merge_request, author: author, source_project: project) }
before do
project.team << [user, :developer]
project.team << [author, :developer]
login_as(user)
visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
find('#s2id_merge_request_approver_ids .select2-input').click
end
it 'does not allow setting the author as an approver' do
expect(find('.select2-results')).not_to have_content(author.name)
end
it 'allows setting the current user as an approver' do
expect(find('.select2-results')).to have_content(user.name)
end
end
context 'when creating an MR' do
let(:other_user) { create(:user) }
before do
project.team << [user, :developer]
project.team << [other_user, :developer]
login_as(user)
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_branch: 'feature' })
find('#s2id_merge_request_approver_ids .select2-input').click
end
it 'allows setting other users as approvers' do
expect(find('.select2-results')).to have_content(other_user.name)
end
it 'does not allow setting the current user as an approver' do
expect(find('.select2-results')).not_to have_content(user.name)
end
end
end
...@@ -56,45 +56,47 @@ describe MergeRequestsHelper do ...@@ -56,45 +56,47 @@ describe MergeRequestsHelper do
end end
describe 'render_require_section' do describe 'render_require_section' do
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:approver) { create(:user) }
before do
5.times { project.team << [create(:user), :developer] }
project.team << [approver, :developer]
end
it "returns correct value in case - one approval" do it "returns correct value in case - one approval" do
project.update(approvals_before_merge: 1) project.update(approvals_before_merge: 1)
merge_request = create(:merge_request, target_project: project, source_project: project)
expect(render_require_section(merge_request)).to eq("Requires one more approval") expect(render_require_section(merge_request)).to eq("Requires one more approval")
end end
it "returns correct value in case - two approval" do it "returns correct value in case - two approval" do
project.update(approvals_before_merge: 2) project.update(approvals_before_merge: 2)
merge_request = create(:merge_request, target_project: project, source_project: project)
expect(render_require_section(merge_request)).to eq("Requires 2 more approvals") expect(render_require_section(merge_request)).to eq("Requires 2 more approvals")
end end
it "returns correct value in case - one approver" do it "returns correct value in case - one approver" do
project.update(approvals_before_merge: 1) project.update(approvals_before_merge: 1)
merge_request = create(:merge_request, target_project: project, source_project: project) create(:approver, user: approver, target: merge_request)
user = create :user
merge_request.approvers.create(user_id: user.id)
expect(render_require_section(merge_request)).to eq("Requires one more approval (from #{user.name})") expect(render_require_section(merge_request)).to eq("Requires one more approval (from #{approver.name})")
end end
it "returns correct value in case - one approver and one more" do it "returns correct value in case - one approver and one more" do
project.update(approvals_before_merge: 2) project.update(approvals_before_merge: 2)
merge_request = create(:merge_request, target_project: project, source_project: project) create(:approver, user: approver, target: merge_request)
user = create :user
merge_request.approvers.create(user_id: user.id)
expect(render_require_section(merge_request)).to eq("Requires 2 more approvals (from #{user.name} and 1 more)") expect(render_require_section(merge_request)).to eq("Requires 2 more approvals (from #{approver.name} and 1 more)")
end end
it "returns correct value in case - two approver and one more" do it "returns correct value in case - two approver and one more" do
project.update(approvals_before_merge: 3) project.update(approvals_before_merge: 3)
merge_request = create(:merge_request, target_project: project, source_project: project) approver2 = create(:user)
user = create :user create(:approver, user: approver, target: merge_request)
user1 = create :user create(:approver, user: approver2, target: merge_request)
merge_request.approvers.create(user_id: user.id)
merge_request.approvers.create(user_id: user1.id)
expect(render_require_section(merge_request)).to eq("Requires 3 more approvals (from #{user1.name}, #{user.name} and 1 more)") expect(render_require_section(merge_request)).to eq("Requires 3 more approvals (from #{approver2.name}, #{approver.name} and 1 more)")
end end
end end
end end
require 'spec_helper'
describe Gitlab::AuthorityAnalyzer, lib: true do
describe '#calculate' do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: author) }
let(:files) { [double(:file, deleted_file: true, old_path: 'foo')] }
let(:commits) do
[
double(:commit, author: author),
double(:commit, author: user_a),
double(:commit, author: user_a),
double(:commit, author: user_b),
double(:commit, author: author)
]
end
let(:approvers) { Gitlab::AuthorityAnalyzer.new(merge_request).calculate(number_of_approvers) }
before do
merge_request.compare = double(:compare, diffs: files)
allow(merge_request.target_project.repository).to receive(:commits).and_return(commits)
end
context 'when there are fewer contributors than requested' do
let(:number_of_approvers) { 5 }
it 'returns the full number of users' do
expect(approvers.length).to eq(2)
end
end
context 'when there are more contributors than requested' do
let(:number_of_approvers) { 1 }
it 'returns only the top n contributors' do
expect(approvers).to contain_exactly(user_a)
end
end
end
end
...@@ -250,7 +250,7 @@ describe MergeRequest, models: true do ...@@ -250,7 +250,7 @@ describe MergeRequest, models: true do
end end
end end
describe "approvers_left" do describe "#approvers_left" do
let(:merge_request) {create :merge_request} let(:merge_request) {create :merge_request}
it "returns correct value" do it "returns correct value" do
...@@ -264,6 +264,56 @@ describe MergeRequest, models: true do ...@@ -264,6 +264,56 @@ describe MergeRequest, models: true do
end end
end end
describe "#number_of_potential_approvers" do
let(:project) { create(:empty_project) }
let(:author) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) }
it "includes approvers set on the MR" do
expect do
create(:approver, user: create(:user), target: merge_request)
end.to change { merge_request.number_of_potential_approvers }.by(1)
end
it "includes project members with developer access and up" do
expect do
project.team << [create(:user), :guest]
project.team << [create(:user), :reporter]
project.team << [create(:user), :developer]
project.team << [create(:user), :master]
end.to change { merge_request.number_of_potential_approvers }.by(2)
end
it "excludes users who have already approved the MR" do
expect do
approver = create(:user)
create(:approver, user: approver, target: merge_request)
create(:approval, user: approver, merge_request: merge_request)
end.not_to change { merge_request.number_of_potential_approvers }
end
it "excludes the MR author" do
expect do
create(:approver, user: create(:user), target: merge_request)
create(:approver, user: author, target: merge_request)
end.to change { merge_request.number_of_potential_approvers }.by(1)
end
context "when the project is part of a group" do
let(:group) { create(:group) }
before { project.update_attributes(group: group) }
it "includes group members with developer access and up" do
expect do
group.add_guest(create(:user))
group.add_reporter(create(:user))
group.add_developer(create(:user))
group.add_master(create(:user))
end.to change { merge_request.number_of_potential_approvers }.by(2)
end
end
end
describe "#approvals_required" do describe "#approvals_required" do
let(:merge_request) { build(:merge_request) } let(:merge_request) { build(:merge_request) }
before { merge_request.target_project.update_attributes(approvals_before_merge: 3) } before { merge_request.target_project.update_attributes(approvals_before_merge: 3) }
...@@ -707,4 +757,175 @@ describe MergeRequest, models: true do ...@@ -707,4 +757,175 @@ describe MergeRequest, models: true do
subject.reload_diff subject.reload_diff
end end
end end
describe 'approvals' do
let(:project) { create(:empty_project) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) }
let(:author) { create(:user) }
let(:approver) { create(:user) }
context 'on a project with only one member' do
context 'when there is one approver' do
before { project.update_attributes(approvals_before_merge: 1) }
context 'when that approver is the MR author' do
before do
project.team << [author, :developer]
create(:approver, user: author, target: merge_request)
end
it 'does not require approval for the merge request' do
expect(merge_request.approvals_left).to eq(0)
end
it 'does not allow the approver to approve the MR' do
expect(merge_request.can_approve?(author)).to be_falsey
end
end
context 'when that approver is not the MR author' do
before do
project.team << [approver, :developer]
create(:approver, user: approver, target: merge_request)
end
it 'requires one approval' do
expect(merge_request.approvals_left).to eq(1)
end
it 'allows the approver to approve the MR' do
expect(merge_request.can_approve?(approver)).to be_truthy
end
end
end
end
context 'on a project with several members' do
let(:approver_2) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:stranger) { create(:user) }
before do
project.team << [author, :developer]
project.team << [approver, :developer]
project.team << [approver_2, :developer]
project.team << [developer, :developer]
project.team << [reporter, :reporter]
end
context 'when there is one approver required' do
before { project.update_attributes(approvals_before_merge: 1) }
context 'when that approver is the MR author' do
before { create(:approver, user: author, target: merge_request) }
it 'requires one approval' do
expect(merge_request.approvals_left).to eq(1)
end
it 'does not allow the author to approve the MR' do
expect(merge_request.can_approve?(author)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(merge_request.can_approve?(developer)).to be_truthy
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(stranger)).to be_falsey
end
end
context 'when that approver is not the MR author' do
before { create(:approver, user: approver, target: merge_request) }
it 'requires one approval' do
expect(merge_request.approvals_left).to eq(1)
end
it 'only allows the approver to approve the MR' do
expect(merge_request.can_approve?(approver)).to be_truthy
expect(merge_request.can_approve?(author)).to be_falsey
expect(merge_request.can_approve?(developer)).to be_falsey
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(stranger)).to be_falsey
end
end
end
context 'when there are multiple approvers required' do
before { project.update_attributes(approvals_before_merge: 3) }
context 'when one of those approvers is the MR author' do
before do
create(:approver, user: author, target: merge_request)
create(:approver, user: approver, target: merge_request)
create(:approver, user: approver_2, target: merge_request)
end
it 'requires the original number of approvals' do
expect(merge_request.approvals_left).to eq(3)
end
it 'does not allow the author to approve the MR' do
expect(merge_request.can_approve?(author)).to be_falsey
end
it 'allows any other other approver to approve the MR' do
expect(merge_request.can_approve?(approver)).to be_truthy
end
context 'when all of the valid approvers have approved the MR' do
before do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver_2, merge_request: merge_request)
end
it 'requires the original number of approvals' do
expect(merge_request.approvals_left).to eq(1)
end
it 'does not allow the author to approve the MR' do
expect(merge_request.can_approve?(author)).to be_falsey
end
it 'does not allow the approvers to approve the MR again' do
expect(merge_request.can_approve?(approver)).to be_falsey
expect(merge_request.can_approve?(approver_2)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(merge_request.can_approve?(developer)).to be_truthy
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(stranger)).to be_falsey
end
end
end
context 'when the approvers do not contain the MR author' do
before do
create(:approver, user: developer, target: merge_request)
create(:approver, user: approver, target: merge_request)
create(:approver, user: approver_2, target: merge_request)
end
it 'requires the original number of approvals' do
expect(merge_request.approvals_left).to eq(3)
end
it 'only allows the approvers to approve the MR' do
expect(merge_request.can_approve?(developer)).to be_truthy
expect(merge_request.can_approve?(approver)).to be_truthy
expect(merge_request.can_approve?(approver_2)).to be_truthy
expect(merge_request.can_approve?(author)).to be_falsey
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(stranger)).to be_falsey
end
end
end
end
end
end end
...@@ -704,14 +704,31 @@ describe API::API, api: true do ...@@ -704,14 +704,31 @@ describe API::API, api: true do
end end
describe 'POST :id/merge_requests/:merge_request_id/approve' do describe 'POST :id/merge_requests/:merge_request_id/approve' do
it 'approves the merge request' do before { project.update_attribute(:approvals_before_merge, 2) }
project.update_attribute(:approvals_before_merge, 2)
context 'as the author of the merge request' do
before { post 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
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/approve", user) context 'as a valid approver' do
let(:approver) { create(:user) }
before do
project.team << [approver, :developer]
project.team << [create(:user), :developer]
post 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(response.status).to eq(201)
expect(json_response['approvals_left']).to eq(1) expect(json_response['approvals_left']).to eq(1)
expect(json_response['approved_by'][0]['user']['username']).to eq(user.username) expect(json_response['approved_by'][0]['user']['username']).to eq(approver.username)
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment