Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
17adce45
Commit
17adce45
authored
Jan 15, 2019
by
Sean McGivern
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '1979-2-0' into 'master'
Add ApprovalState See merge request gitlab-org/gitlab-ee!8974
parents
1899e9a2
f2eed8e8
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1488 additions
and
0 deletions
+1488
-0
ee/app/models/approval_state.rb
ee/app/models/approval_state.rb
+156
-0
ee/app/models/approval_wrapped_rule.rb
ee/app/models/approval_wrapped_rule.rb
+73
-0
ee/app/models/concerns/approvable.rb
ee/app/models/concerns/approvable.rb
+4
-0
ee/app/models/concerns/visible_approvable.rb
ee/app/models/concerns/visible_approvable.rb
+1
-0
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/spec/models/approval_state_spec.rb
ee/spec/models/approval_state_spec.rb
+1120
-0
ee/spec/models/approval_wrapped_rule_spec.rb
ee/spec/models/approval_wrapped_rule_spec.rb
+133
-0
No files found.
ee/app/models/approval_state.rb
0 → 100644
View file @
17adce45
# 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
ee/app/models/approval_wrapped_rule.rb
0 → 100644
View file @
17adce45
# 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
ee/app/models/concerns/approvable.rb
View file @
17adce45
...
...
@@ -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
...
...
ee/app/models/concerns/visible_approvable.rb
View file @
17adce45
...
...
@@ -80,5 +80,6 @@ module VisibleApprovable
clear_memoization
(
:approvers_left
)
clear_memoization
(
:all_approvers_including_groups
)
clear_memoization
(
:approval_state
)
end
end
ee/app/models/license.rb
View file @
17adce45
...
...
@@ -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
...
...
ee/spec/models/approval_state_spec.rb
0 → 100644
View file @
17adce45
# 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
ee/spec/models/approval_wrapped_rule_spec.rb
0 → 100644
View file @
17adce45
# 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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment