Commit 17adce45 authored by Sean McGivern's avatar Sean McGivern

Merge branch '1979-2-0' into 'master'

Add ApprovalState

See merge request gitlab-org/gitlab-ee!8974
parents 1899e9a2 f2eed8e8
# frozen_string_literal: true
require 'forwardable'
# A state object to centralize logic related to various approval related states.
# This reduce interface footprint on MR and allows easier cache invalidation.
class ApprovalState
extend Forwardable
include ::Gitlab::Utils::StrongMemoize
attr_reader :merge_request, :project
def_delegators :@merge_request, :merge_status, :approved_by_users, :approvals
alias_method :approved_approvers, :approved_by_users
def initialize(merge_request)
@merge_request = merge_request
@project = merge_request.target_project
end
# Excludes the author if 'self-approval' isn't explicitly enabled on project settings.
def self.filter_author(users, merge_request)
return users if merge_request.target_project.merge_requests_author_approval?
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
users.where.not(id: merge_request.authors)
else
users - merge_request.authors
end
end
def wrapped_approval_rules
strong_memoize(:wrapped_approval_rules) do
regular_rules + code_owner_rules
end
end
def approval_rules_overwritten?
merge_request.approval_rules.regular.exists?
end
def approval_needed?
return false unless project.feature_available?(:merge_request_approvers)
overall_approvals_required > 0 || wrapped_approval_rules.any? { |rule| rule.approvals_required > 0 }
end
def overall_approvals_required
@overall_approvals_required ||= project.approvals_before_merge
end
def approved?
strong_memoize(:approved) do
(overall_approvals_required == 0 || approvals.size >= overall_approvals_required) && wrapped_approval_rules.all?(&:approved?)
end
end
def any_approver_allowed?
approved? || overall_approvals_required > approvers.size
end
# Number of approvals remaining (excluding existing approvals) before the MR is
# considered approved.
def approvals_left
strong_memoize(:approvals_left) do
wrapped_approval_rules.sum(&:approvals_left)
end
end
def approval_rules_left
wrapped_approval_rules.reject(&:approved?)
end
def approvers
strong_memoize(:approvers) { filtered_approvers(target: :approvers) }
end
# @param regular [Boolean]
# @param code_owner [Boolean]
# @param target [:approvers, :users]
# @param unactioned [Boolean]
def filtered_approvers(regular: true, code_owner: true, target: :approvers, unactioned: false)
rules = []
rules.concat(regular_rules) if regular
rules.concat(code_owner_rules) if code_owner
users = rules.flat_map(&target)
users.uniq!
users -= approved_approvers if unactioned
self.class.filter_author(users, merge_request)
end
# approvers_left
def unactioned_approvers
strong_memoize(:unactioned_approvers) { approvers - approved_approvers }
end
def can_approve?(user)
return false unless user
# The check below considers authors being able to approve the MR.
# That is, they're included/excluded from that list accordingly.
return true if unactioned_approvers.include?(user)
# We can safely unauthorize authors if it reaches this guard clause.
return false if merge_request.authors.include?(user)
return false unless user.can?(:update_merge_request, merge_request)
any_approver_allowed? && merge_request.approvals.where(user: user).empty?
end
def has_approved?(user)
return false unless user
approved_approvers.include?(user)
end
def authors_can_approve?
project.merge_requests_author_approval?
end
# TODO: remove after #1979 is closed
# This is a temporary method for backward compatibility
# before introduction of approval rules.
# This avoids re-queries.
def first_regular_rule
strong_memoize(:first_regular_rule) do
regular_rules.first
end
end
private
def regular_rules
strong_memoize(:regular_rules) do
rule_source = approval_rules_overwritten? ? merge_request : project
rules = rule_source.approval_rules.regular
unless project.feature_available?(:multiple_approval_rules)
rules = rules.order(id: :asc).limit(1)
end
wrap_rules(rules)
end
end
def code_owner_rules
strong_memoize(:code_owner_rules) do
wrap_rules(merge_request.approval_rules.code_owner)
end
end
def wrap_rules(rules)
rules.map { |rule| ApprovalWrappedRule.new(merge_request, rule) }
end
end
# frozen_string_literal: true
# A common state computation interface to wrap around ApprovalRuleLike models
class ApprovalWrappedRule
extend Forwardable
include Gitlab::Utils::StrongMemoize
attr_reader :merge_request
attr_reader :approval_rule
def_delegators :@approval_rule, :id, :name, :users, :groups, :approvals_required, :code_owner
def initialize(merge_request, approval_rule)
@merge_request = merge_request
@approval_rule = approval_rule
end
def project
@merge_request.target_project
end
def approvers
ApprovalState.filter_author(@approval_rule.approvers, merge_request)
end
# @return [Array<User>] all approvers related to this rule
#
# This is dynamically calculated when MR is open, but is persisted in DB after MR is merged.
#
# After merge, the approval state should no longer change.
# Persisting instead of recomputing dynamically guarantees this even
# if project level rule ever adds/removes approvers after the merge.
#
# For open MRs, it is still dynamically calculated instead of persisted because
# - Additional complexity to add update hooks
# - DB updating many MRs for one project rule change is inefficient
def approved_approvers
return approval_rule.approved_approvers if merge_request.merged?
strong_memoize(:approved_approvers) do
overall_approver_ids = merge_request.approvals.map(&:user_id)
approvers.select do |approver|
overall_approver_ids.include?(approver.id)
end
end
end
def approved?
strong_memoize(:approved) do
approvals_left <= 0 || unactioned_approvers.size <= 0
end
end
# Number of approvals remaining (excluding existing approvals)
# before the rule is considered approved.
#
# If there are fewer potential approvers than approvals left,
# users should either reduce `approvals_required`
# and/or allow MR authors to approve their own merge
# requests (in case only one approval is needed).
def approvals_left
strong_memoize(:approvals_left) do
approvals_left_count = approvals_required - approved_approvers.size
[approvals_left_count, 0].max
end
end
def unactioned_approvers
approvers - approved_approvers
end
end
......@@ -7,6 +7,10 @@ module Approvable
# such as approver_groups and target_project in presenters
include ::VisibleApprovable
def approval_state
@approval_state ||= ApprovalState.new(self)
end
def approval_needed?
approvals_required&.nonzero?
end
......
......@@ -80,5 +80,6 @@ module VisibleApprovable
clear_memoization(:approvers_left)
clear_memoization(:all_approvers_including_groups)
clear_memoization(:approval_state)
end
end
......@@ -55,6 +55,7 @@ class License < ActiveRecord::Base
ldap_group_sync_filter
multiple_clusters
multiple_group_issue_boards
multiple_approval_rules
merge_request_performance_metrics
object_storage
group_saml
......
# frozen_string_literal: true
require 'spec_helper'
describe ApprovalState do
def create_rule(additional_params = {})
create(
:approval_merge_request_rule,
additional_params.merge(merge_request: merge_request)
)
end
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
let(:approver1) { create(:user) }
let(:approver2) { create(:user) }
let(:approver3) { create(:user) }
let(:group_approver1) { create(:user) }
let(:group1) do
group = create(:group)
group.add_developer(group_approver1)
group
end
subject { merge_request.approval_state }
shared_examples 'filtering author' do
before do
allow(merge_request).to receive(:authors).and_return([merge_request.author, create(:user, username: 'commiter')])
project.update(merge_requests_author_approval: merge_requests_author_approval)
create_rule(users: merge_request.authors)
end
context 'when self approval is disabled' do
let(:merge_requests_author_approval) { false }
it 'excludes authors' do
expect(results).not_to include(*merge_request.authors)
end
end
context 'when self approval is enabled' do
let(:merge_requests_author_approval) { true }
it 'includes author' do
expect(results).to include(*merge_request.authors)
end
end
end
context 'when multiple rules are allowed' do
before do
stub_licensed_features(multiple_approval_rules: true)
end
describe '#wrapped_approval_rules' do
before do
2.times { create_rule }
end
it 'returns all rules in wrapper' do
expect(subject.wrapped_approval_rules).to all(be_an(ApprovalWrappedRule))
expect(subject.wrapped_approval_rules.size).to eq(2)
end
end
describe '#approval_rules_overwritten?' do
context 'when approval rule on the merge request does not exist' do
it 'returns false' do
expect(subject.approval_rules_overwritten?).to eq(false)
end
end
context 'when approval rule on the merge request exists' do
before do
create(:approval_merge_request_rule, merge_request: merge_request)
end
it 'returns true' do
expect(subject.approval_rules_overwritten?).to eq(true)
end
end
end
describe '#approval_needed?' do
context 'when feature not available' do
it 'returns false' do
allow(subject.project).to receive(:feature_available?).with(:merge_request_approvers).and_return(false)
expect(subject.approval_needed?).to eq(false)
end
end
context 'when overall approvals required is not zero' do
before do
project.update(approvals_before_merge: 1)
end
it 'returns true' do
expect(subject.approval_needed?).to eq(true)
end
end
context "when any rule's approvals required is not zero" do
it 'returns false' do
create_rule(approvals_required: 1)
expect(subject.approval_needed?).to eq(true)
end
end
context "when overall approvals required and all rule's approvals_required are zero" do
it 'returns false' do
create_rule(approvals_required: 0)
expect(subject.approval_needed?).to eq(false)
end
end
context "when overall approvals required is zero, and there is no rule" do
it 'returns false' do
expect(subject.approval_needed?).to eq(false)
end
end
end
describe '#approved?' do
context 'when no rules' do
before do
project.update(approvals_before_merge: 1)
end
context 'when overall_approvals_required is not met' do
it 'returns false' do
expect(subject.wrapped_approval_rules.size).to eq(0)
expect(subject.approved?).to eq(false)
end
end
context 'when overall_approvals_required is met' do
it 'returns true' do
create(:approval, merge_request: merge_request)
expect(subject.wrapped_approval_rules.size).to eq(0)
expect(subject.approved?).to eq(true)
end
end
end
context 'when rules are present' do
before do
2.times { create_rule(users: [create(:user)]) }
end
context 'when all rules are approved' do
before do
subject.wrapped_approval_rules.each do |rule|
create(:approval, merge_request: merge_request, user: rule.users.first)
end
end
it 'returns true' do
expect(subject.approved?).to eq(true)
end
context 'when overall_approvals_required is not met' do
before do
project.update(approvals_before_merge: 3)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
end
end
context 'when some rules are not approved' do
before do
allow(subject.wrapped_approval_rules.first).to receive(:approved?).and_return(false)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
end
end
end
describe '#any_approver_allowed?' do
context 'when approved' do
before do
allow(subject).to receive(:approved?).and_return(true)
end
it 'returns true' do
expect(subject.any_approver_allowed?).to eq(true)
end
end
context 'when not approved' do
before do
allow(subject).to receive(:approved?).and_return(false)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
context 'when overall_approvals_required cannot be met' do
before do
project.update(approvals_before_merge: 1)
end
it 'returns false' do
expect(subject.any_approver_allowed?).to eq(true)
end
end
end
end
describe '#approvals_left' do
before do
create_rule(approvals_required: 5)
create_rule(approvals_required: 7)
end
it 'sums approvals_left from rules' do
expect(subject.approvals_left).to eq(12)
end
end
describe '#approval_rules_left' do
def create_unapproved_rule
create_rule(approvals_required: 1, users: [create(:user)])
end
it 'counts approval_rules left' do
create_unapproved_rule
create_unapproved_rule
expect(subject.approval_rules_left.size).to eq(2)
end
end
describe '#approvers' do
it 'includes all approvers, including code owner and group members' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
expect(subject.approvers).to contain_exactly(approver1, group_approver1)
end
it_behaves_like 'filtering author' do
let(:results) { subject.approvers }
end
end
describe '#filtered_approvers' do
describe 'only direct users, without code owners' do
it 'includes only rule user members' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
expect(subject.filtered_approvers(code_owner: false, target: :users)).to contain_exactly(approver1)
end
end
describe 'only code owners' do
it 'includes only code owners' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
expect(subject.filtered_approvers(regular: false)).to contain_exactly(approver2)
end
end
describe 'only unactioned' do
it 'excludes approved approvers' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
create(:approval, merge_request: merge_request, user: approver1)
expect(subject.filtered_approvers(unactioned: true)).to contain_exactly(approver2, group_approver1)
end
end
it_behaves_like 'filtering author' do
let(:results) { subject.filtered_approvers }
end
end
describe '#unactioned_approvers' do
it 'sums approvals_left from rules' do
create_rule(users: [approver1, approver2])
create_rule(users: [approver1])
merge_request.approvals.create(user: approver2)
expect(subject.unactioned_approvers).to contain_exactly(approver1)
end
it_behaves_like 'filtering author' do
let(:results) { subject.unactioned_approvers }
end
end
describe '#can_approve?' do
shared_examples_for 'authors self-approval authorization' do
context 'when authors are authorized to approve their own MRs' do
before do
project.update!(merge_requests_author_approval: true)
end
it 'allows the author to approve the MR if within the approvers list' do
expect(subject.can_approve?(author)).to be_truthy
end
it 'does not allow the author to approve the MR if not within the approvers list' do
allow(subject).to receive(:approvers).and_return([])
expect(subject.can_approve?(author)).to be_falsey
end
end
context 'when authors are not authorized to approve their own MRs' do
it 'does not allow the author to approve the MR' do
expect(subject.can_approve?(author)).to be_falsey
end
end
end
def create_project_member(role)
user = create(:user)
project.add_user(user, role)
user
end
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) }
let(:author) { create_project_member(:developer) }
let(:approver) { create_project_member(:developer) }
let(:approver2) { create_project_member(:developer) }
let(:developer) { create_project_member(:developer) }
let(:other_developer) { create_project_member(:developer) }
let(:reporter) { create_project_member(:reporter) }
let(:stranger) { create(:user) }
context 'when there is one approver required' do
let!(:rule) { create_rule(approvals_required: 1) }
context 'when that approver is the MR author' do
before do
rule.users << author
end
it_behaves_like 'authors self-approval authorization'
it 'requires one approval' do
expect(subject.approvals_left).to eq(1)
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
end
it 'does not allow a logged-out user to approve the MR' do
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when that approver is not the MR author' do
before do
rule.users << approver
end
it 'requires one approval' do
expect(subject.approvals_left).to eq(1)
end
it 'only allows the approver to approve the MR' do
expect(subject.can_approve?(approver)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(developer)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
end
context 'when there are multiple approvers required' do
let!(:rule) { create_rule(approvals_required: 3) }
context 'when one of those approvers is the MR author' do
before do
rule.users = [author, approver, approver2]
end
it_behaves_like 'authors self-approval authorization'
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(3)
end
it 'allows any other other approver to approve the MR' do
expect(subject.can_approve?(approver)).to be_truthy
end
it 'does not allow a logged-out user to approve the MR' do
expect(subject.can_approve?(nil)).to be_falsey
end
context 'when self-approval is disabled and all of the valid approvers have approved the MR' do
before do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(1)
end
it 'does not allow the author to approve the MR' do
expect(subject.can_approve?(author)).to be_falsey
end
it 'does not allow the approvers to approve the MR again' do
expect(subject.can_approve?(approver)).to be_falsey
expect(subject.can_approve?(approver2)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when self-approval is enabled and all of the valid approvers have approved the MR' do
before do
project.update!(merge_requests_author_approval: true)
create(:approval, user: author, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(1)
end
it 'does not allow the approvers to approve the MR again' do
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(approver2)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when all approvers have approved the MR' do
before do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
create(:approval, user: developer, merge_request: merge_request)
end
it 'is approved' do
expect(subject).to be_approved
end
it "returns sum of each rule's approvals_left" do
expect(subject.approvals_left).to eq(1)
end
end
end
context 'when the approvers do not contain the MR author' do
before do
rule.users = [developer, approver, approver2]
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(3)
end
it 'only allows the approvers to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(approver)).to be_truthy
expect(subject.can_approve?(approver2)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
context 'when only 1 approval approved' do
it 'only allows the approvers to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(approver)).to be_falsey
expect(subject.can_approve?(approver2)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(other_developer)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when all approvals received' do
it 'allows anyone with write access except for author to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
create(:approval, user: developer, merge_request: merge_request)
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(other_developer)).to be_truthy
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
end
end
end
describe '#has_approved?' do
it 'returns false if user is nil' do
expect(subject.has_approved?(nil)).to eq(false)
end
it 'returns true if user has approved' do
create(:approval, merge_request: merge_request, user: approver1)
expect(subject.has_approved?(approver1)).to eq(true)
expect(subject.has_approved?(approver2)).to eq(false)
end
end
describe '#authors_can_approve?' do
context 'when project allows author approval' do
before do
project.update(merge_requests_author_approval: true)
end
it 'returns true' do
expect(subject.authors_can_approve?).to eq(true)
end
end
context 'when project disallows author approval' do
before do
project.update(merge_requests_author_approval: false)
end
it 'returns true' do
expect(subject.authors_can_approve?).to eq(false)
end
end
end
end
context 'when only a single rule is allowed' do
def create_unapproved_rule(additional_params = {})
create_rule(
additional_params.reverse_merge(approvals_required: 1, users: [create(:user)])
)
end
def create_rules
rule1
rule2
code_owner_rule
end
let(:rule1) { create_unapproved_rule }
let(:rule2) { create_unapproved_rule }
let(:code_owner_rule) { create_unapproved_rule(code_owner: true, approvals_required: 0) }
before do
stub_licensed_features multiple_approval_rules: false
end
describe '#wrapped_approval_rules' do
it 'returns one regular rule in wrapper' do
create_rules
subject.wrapped_approval_rules.each do |rule|
expect(rule.is_a?(ApprovalWrappedRule)).to eq(true)
end
expect(subject.wrapped_approval_rules.size).to eq(2)
end
end
describe '#approval_rules_overwritten?' do
context 'when approval rule does not exist' do
it 'returns false' do
expect(subject.approval_rules_overwritten?).to eq(false)
end
end
context 'when approval rule exists' do
before do
create(:approval_merge_request_rule, merge_request: merge_request)
end
it 'returns true' do
expect(subject.approval_rules_overwritten?).to eq(true)
end
end
end
describe '#approval_needed?' do
context 'when feature not available' do
it 'returns false' do
allow(subject.project).to receive(:feature_available?).with(:merge_request_approvers).and_return(false)
expect(subject.approval_needed?).to eq(false)
end
end
context 'when overall approvals required is not zero' do
before do
project.update(approvals_before_merge: 1)
end
it 'returns true' do
expect(subject.approval_needed?).to eq(true)
end
end
context "when any rule's approvals required is not zero" do
it 'returns false' do
create_rule(approvals_required: 1)
expect(subject.approval_needed?).to eq(true)
end
end
context "when overall approvals required and all rule's approvals_required are zero" do
it 'returns false' do
create_rule(approvals_required: 0)
expect(subject.approval_needed?).to eq(false)
end
end
context "when overall approvals required is zero, and there is no rule" do
it 'returns false' do
expect(subject.approval_needed?).to eq(false)
end
end
end
describe '#approved?' do
context 'when no rules' do
before do
project.update(approvals_before_merge: 1)
end
context 'when overall_approvals_required is not met' do
it 'returns false' do
expect(subject.wrapped_approval_rules.size).to eq(0)
expect(subject.approved?).to eq(false)
end
end
context 'when overall_approvals_required is met' do
it 'returns true' do
create(:approval, merge_request: merge_request)
expect(subject.wrapped_approval_rules.size).to eq(0)
expect(subject.approved?).to eq(true)
end
end
end
context 'when rules are present' do
before do
2.times { create_rule(users: [create(:user)]) }
end
context 'when all rules are approved' do
before do
subject.wrapped_approval_rules.each do |rule|
create(:approval, merge_request: merge_request, user: rule.users.first)
end
end
it 'returns true' do
expect(subject.approved?).to eq(true)
end
context 'when overall_approvals_required is not met' do
before do
project.update(approvals_before_merge: 3)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
end
end
context 'when some rules are not approved' do
before do
allow(subject.wrapped_approval_rules.first).to receive(:approved?).and_return(false)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
end
end
end
describe '#any_approver_allowed?' do
context 'when approved' do
before do
allow(subject).to receive(:approved?).and_return(true)
end
it 'returns true' do
expect(subject.any_approver_allowed?).to eq(true)
end
end
context 'when not approved' do
before do
allow(subject).to receive(:approved?).and_return(false)
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
context 'when overall_approvals_required cannot be met' do
before do
project.update(approvals_before_merge: 1)
end
it 'returns false' do
expect(subject.any_approver_allowed?).to eq(true)
end
end
end
end
describe '#approvals_left' do
let(:rule1) { create_unapproved_rule(approvals_required: 5) }
let(:rule2) { create_unapproved_rule(approvals_required: 7) }
it 'sums approvals_left from rules' do
create_rules
expect(subject.approvals_left).to eq(5)
end
end
describe '#approval_rules_left' do
it 'counts approval_rules left' do
create_rules
expect(subject.approval_rules_left.size).to eq(1)
end
end
describe '#approvers' do
let(:code_owner_rule) { create_rule(code_owner: true, groups: [group1]) }
it 'includes approvers from first rule and code owner rule' do
create_rules
approvers = rule1.users + [group_approver1]
expect(subject.approvers).to contain_exactly(*approvers)
end
it_behaves_like 'filtering author' do
let(:results) { subject.approvers }
end
end
describe '#filtered_approvers' do
describe 'only direct users, without code owners' do
it 'includes only rule user members' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
expect(subject.filtered_approvers(code_owner: false, target: :users)).to contain_exactly(approver1)
end
end
describe 'excludes regular rule' do
it 'includes only code owners' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
expect(subject.filtered_approvers(regular: false)).to contain_exactly(approver2)
end
end
describe 'only unactioned' do
it 'excludes approved approvers' do
create_rule(users: [approver1])
create_rule(users: [approver1], groups: [group1])
create_rule(users: [approver2], code_owner: true)
create(:approval, merge_request: merge_request, user: approver1)
expect(subject.filtered_approvers(unactioned: true)).to contain_exactly(approver2)
end
end
it_behaves_like 'filtering author' do
let(:results) { subject.filtered_approvers }
end
end
describe '#unactioned_approvers' do
it 'sums approvals_left from rules' do
create_rule(users: [approver1, approver2])
create_rule(users: [approver1])
merge_request.approvals.create(user: approver2)
expect(subject.unactioned_approvers).to contain_exactly(approver1)
end
it_behaves_like 'filtering author' do
let(:results) { subject.unactioned_approvers }
end
end
describe '#can_approve?' do
shared_examples_for 'authors self-approval authorization' do
context 'when authors are authorized to approve their own MRs' do
before do
project.update!(merge_requests_author_approval: true)
end
it 'allows the author to approve the MR if within the approvers list' do
expect(subject.can_approve?(author)).to be_truthy
end
it 'does not allow the author to approve the MR if not within the approvers list' do
allow(subject).to receive(:approvers).and_return([])
expect(subject.can_approve?(author)).to be_falsey
end
end
context 'when authors are not authorized to approve their own MRs' do
it 'does not allow the author to approve the MR' do
expect(subject.can_approve?(author)).to be_falsey
end
end
end
def create_project_member(role)
user = create(:user)
project.add_user(user, role)
user
end
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) }
let(:author) { create_project_member(:developer) }
let(:approver) { create_project_member(:developer) }
let(:approver2) { create_project_member(:developer) }
let(:developer) { create_project_member(:developer) }
let(:other_developer) { create_project_member(:developer) }
let(:reporter) { create_project_member(:reporter) }
let(:stranger) { create(:user) }
context 'when there is one approver required' do
let!(:rule) { create_rule(approvals_required: 1) }
context 'when that approver is the MR author' do
before do
rule.users << author
end
it_behaves_like 'authors self-approval authorization'
it 'requires one approval' do
expect(subject.approvals_left).to eq(1)
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
end
it 'does not allow a logged-out user to approve the MR' do
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when that approver is not the MR author' do
before do
rule.users << approver
end
it 'requires one approval' do
expect(subject.approvals_left).to eq(1)
end
it 'only allows the approver to approve the MR' do
expect(subject.can_approve?(approver)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(developer)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
end
context 'when there are multiple approvers required' do
let!(:rule) { create_rule(approvals_required: 3) }
context 'when one of those approvers is the MR author' do
before do
rule.users = [author, approver, approver2]
end
it_behaves_like 'authors self-approval authorization'
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(3)
end
it 'allows any other other approver to approve the MR' do
expect(subject.can_approve?(approver)).to be_truthy
end
it 'does not allow a logged-out user to approve the MR' do
expect(subject.can_approve?(nil)).to be_falsey
end
context 'when self-approval is disabled and all of the valid approvers have approved the MR' do
before do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(1)
end
it 'does not allow the author to approve the MR' do
expect(subject.can_approve?(author)).to be_falsey
end
it 'does not allow the approvers to approve the MR again' do
expect(subject.can_approve?(approver)).to be_falsey
expect(subject.can_approve?(approver2)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when self-approval is enabled and all of the valid approvers have approved the MR' do
before do
project.update!(merge_requests_author_approval: true)
create(:approval, user: author, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(1)
end
it 'does not allow the approvers to approve the MR again' do
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(approver2)).to be_falsey
end
it 'allows any other project member with write access to approve the MR' do
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when all approvers have approved the MR' do
before do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
create(:approval, user: developer, merge_request: merge_request)
end
it 'is approved' do
expect(subject).to be_approved
end
it "returns sum of each rule's approvals_left" do
expect(subject.approvals_left).to eq(1)
end
end
end
context 'when the approvers do not contain the MR author' do
before do
rule.users = [developer, approver, approver2]
end
it 'requires the original number of approvals' do
expect(subject.approvals_left).to eq(3)
end
it 'only allows the approvers to approve the MR' do
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(approver)).to be_truthy
expect(subject.can_approve?(approver2)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
context 'when only 1 approval approved' do
it 'only allows the approvers to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
expect(subject.can_approve?(developer)).to be_truthy
expect(subject.can_approve?(approver)).to be_falsey
expect(subject.can_approve?(approver2)).to be_truthy
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(other_developer)).to be_falsey
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
context 'when all approvals received' do
it 'allows anyone with write access except for author to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver2, merge_request: merge_request)
create(:approval, user: developer, merge_request: merge_request)
expect(subject.can_approve?(author)).to be_falsey
expect(subject.can_approve?(reporter)).to be_falsey
expect(subject.can_approve?(other_developer)).to be_truthy
expect(subject.can_approve?(stranger)).to be_falsey
expect(subject.can_approve?(nil)).to be_falsey
end
end
end
end
end
describe '#has_approved?' do
it 'returns false if user is nil' do
expect(subject.has_approved?(nil)).to eq(false)
end
it 'returns true if user has approved' do
create(:approval, merge_request: merge_request, user: approver1)
expect(subject.has_approved?(approver1)).to eq(true)
expect(subject.has_approved?(approver2)).to eq(false)
end
end
describe '#authors_can_approve?' do
context 'when project allows author approval' do
before do
project.update(merge_requests_author_approval: true)
end
it 'returns true' do
expect(subject.authors_can_approve?).to eq(true)
end
end
context 'when project disallows author approval' do
before do
project.update(merge_requests_author_approval: false)
end
it 'returns true' do
expect(subject.authors_can_approve?).to eq(false)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ApprovalWrappedRule do
let(:merge_request) { create(:merge_request) }
let(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approvals_required: approvals_required) }
let(:approvals_required) { 0 }
let(:approver1) { create(:user) }
let(:approver2) { create(:user) }
let(:approver3) { create(:user) }
subject { described_class.new(merge_request, rule) }
describe '#project' do
it 'returns merge request project' do
expect(subject.project).to eq(merge_request.target_project)
end
end
describe '#approvals_left' do
before do
create(:approval, merge_request: merge_request, user: approver1)
create(:approval, merge_request: merge_request, user: approver2)
rule.users << approver1
rule.users << approver2
end
context 'when approvals_required is greater than approved approver count' do
let(:approvals_required) { 8 }
it 'returns approvals still needed' do
expect(subject.approvals_left).to eq(6)
end
end
context 'when approvals_required is less than approved approver count' do
let(:approvals_required) { 1 }
it 'returns zero' do
expect(subject.approvals_left).to eq(0)
end
end
end
describe '#approved?' do
before do
create(:approval, merge_request: merge_request, user: approver1)
rule.users << approver1
end
context 'when approvals left is zero' do
let(:approvals_required) { 1 }
it 'returns true' do
expect(subject.approved?).to eq(true)
end
end
context 'when approvals left is not zero, but there is still unactioned approvers' do
let(:approvals_required) { 99 }
before do
rule.users << approver2
end
it 'returns false' do
expect(subject.approved?).to eq(false)
end
end
context 'when approvals left is not zero, but there is no unactioned approvers' do
let(:approvals_required) { 99 }
it 'returns true' do
expect(subject.approved?).to eq(true)
end
end
end
describe '#approved_approvers' do
context 'when some approvers has made the approvals' do
before do
create(:approval, merge_request: merge_request, user: approver1)
create(:approval, merge_request: merge_request, user: approver2)
rule.users = [approver1, approver3]
end
it 'returns approved approvers' do
expect(subject.approved_approvers).to contain_exactly(approver1)
end
end
context 'when merged' do
let(:merge_request) { create(:merged_merge_request) }
before do
rule.approved_approvers << approver3
end
it 'returns approved approvers from database' do
expect(subject.approved_approvers).to contain_exactly(approver3)
end
end
end
describe '#unactioned_approvers' do
context 'when some approvers has not approved yet' do
before do
create(:approval, merge_request: merge_request, user: approver1)
rule.users = [approver1, approver2]
end
it 'returns unactioned approvers' do
expect(subject.unactioned_approvers).to contain_exactly(approver2)
end
end
context 'when merged' do
let(:merge_request) { create(:merged_merge_request) }
before do
rule.approved_approvers << approver3
rule.users = [approver1, approver3]
end
it 'returns approved approvers from database' do
expect(subject.unactioned_approvers).to contain_exactly(approver1)
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