Commit 83ffd7d7 authored by Robert Speicher's avatar Robert Speicher

Merge branch '12055-public-mr-approval-rules-api' into 'master'

Implement public MR-level approval rules API

See merge request gitlab-org/gitlab-ee!15441
parents cfb236d6 05689920
......@@ -152,8 +152,8 @@ POST /projects/:id/approval_rules
| `id` | integer | yes | The ID of a project |
| `name` | string | yes | The name of the approval rule |
| `approvals_required` | integer | yes | The number of required approvals for this rule |
| `users` | integer | no | The ids of users as approvers |
| `groups` | integer | no | The ids of groups as approvers |
| `user_ids` | Array | no | The ids of users as approvers |
| `group_ids` | Array | no | The ids of groups as approvers |
```json
{
......@@ -231,8 +231,8 @@ PUT /projects/:id/approval_rules/:approval_rule_id
| `approval_rule_id` | integer | yes | The ID of a approval rule |
| `name` | string | yes | The name of the approval rule |
| `approvals_required` | integer | yes | The number of required approvals for this rule |
| `users` | integer | no | The ids of users as approvers |
| `groups` | integer | no | The ids of groups as approvers |
| `user_ids` | Array | no | The ids of users as approvers |
| `group_ids` | Array | no | The ids of groups as approvers |
```json
{
......@@ -525,6 +525,270 @@ PUT /projects/:id/merge_requests/:merge_request_iid/approvers
}
```
### Get merge request level rules
>**Note:** This API endpoint is only available on 12.3 Starter and above.
You can request information about a merge request's approval rules using the following endpoint:
```
GET /projects/:id/merge_requests/:merge_request_iid/approval_rules
```
**Parameters:**
| Attribute | Type | Required | Description |
|---------------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `merge_request_iid` | integer | yes | The IID of MR |
```json
[
{
"id": 1,
"name": "security",
"rule_type": "regular",
"eligible_approvers": [
{
"id": 5,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
},
{
"id": 50,
"name": "Group Member 1",
"username": "group_member_1",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/group_member_1"
}
],
"approvals_required": 3,
"source_rule": null,
"users": [
{
"id": 5,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"groups": [
{
"id": 5,
"name": "group1",
"path": "group1",
"description": "",
"visibility": "public",
"lfs_enabled": false,
"avatar_url": null,
"web_url": "http://localhost/groups/group1",
"request_access_enabled": false,
"full_name": "group1",
"full_path": "group1",
"parent_id": null,
"ldap_cn": null,
"ldap_access": null
}
],
"contains_hidden_groups": false
}
]
```
### Create merge request level rule
>**Note:** This API endpoint is only available on 12.3 Starter and above.
You can create merge request approval rules using the following endpoint:
```
POST /projects/:id/merge_requests/:merge_request_iid/approval_rules
```
**Parameters:**
| Attribute | Type | Required | Description |
|----------------------------|---------|----------|------------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `merge_request_iid` | integer | yes | The IID of MR |
| `name` | string | yes | The name of the approval rule |
| `approvals_required` | integer | yes | The number of required approvals for this rule |
| `approval_project_rule_id` | integer | no | The ID of a project-level approval rule |
| `user_ids` | Array | no | The ids of users as approvers |
| `group_ids` | Array | no | The ids of groups as approvers |
**Important:** When `approval_project_rule_id` is set, the `name`, `users` and
`groups` of project-level rule will be copied. The `approvals_required` specified
will be used.
```json
{
"id": 1,
"name": "security",
"rule_type": "regular",
"eligible_approvers": [
{
"id": 2,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
},
{
"id": 50,
"name": "Group Member 1",
"username": "group_member_1",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/group_member_1"
}
],
"approvals_required": 1,
"source_rule": null,
"users": [
{
"id": 2,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"groups": [
{
"id": 5,
"name": "group1",
"path": "group1",
"description": "",
"visibility": "public",
"lfs_enabled": false,
"avatar_url": null,
"web_url": "http://localhost/groups/group1",
"request_access_enabled": false,
"full_name": "group1",
"full_path": "group1",
"parent_id": null,
"ldap_cn": null,
"ldap_access": null
}
],
"contains_hidden_groups": false
}
```
### Update merge request level rule
>**Note:** This API endpoint is only available on 12.3 Starter and above.
You can update merge request approval rules using the following endpoint:
```
PUT /projects/:id/merge_request/:merge_request_iid/approval_rules/:approval_rule_id
```
**Important:** Approvers and groups not in the `users`/`groups` param will be **removed**
**Important:** Updating a `report_approver` or `code_owner` rule is not allowed.
These are system generated rules.
**Parameters:**
| Attribute | Type | Required | Description |
|----------------------|---------|----------|------------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `merge_request_iid` | integer | yes | The ID of MR |
| `approval_rule_id` | integer | yes | The ID of a approval rule |
| `name` | string | yes | The name of the approval rule |
| `approvals_required` | integer | yes | The number of required approvals for this rule |
| `user_ids` | Array | no | The ids of users as approvers |
| `group_ids` | Array | no | The ids of groups as approvers |
```json
{
"id": 1,
"name": "security",
"rule_type": "regular",
"eligible_approvers": [
{
"id": 2,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
},
{
"id": 50,
"name": "Group Member 1",
"username": "group_member_1",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/group_member_1"
}
],
"approvals_required": 1,
"source_rule": null,
"users": [
{
"id": 2,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"groups": [
{
"id": 5,
"name": "group1",
"path": "group1",
"description": "",
"visibility": "public",
"lfs_enabled": false,
"avatar_url": null,
"web_url": "http://localhost/groups/group1",
"request_access_enabled": false,
"full_name": "group1",
"full_path": "group1",
"parent_id": null,
"ldap_cn": null,
"ldap_access": null
}
],
"contains_hidden_groups": false
}
```
### Delete merge request level rule
>**Note:** This API endpoint is only available on 12.3 Starter and above.
You can delete merge request approval rules using the following endpoint:
```
DELETE /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id
```
**Important:** Deleting a `report_approver` or `code_owner` rule is not allowed.
These are system generated rules.
**Parameters:**
| Attribute | Type | Required | Description |
|---------------------|---------|----------|---------------------------|
| `id` | integer | yes | The ID of a project |
| `merge_request_iid` | integer | yes | The ID of MR |
| `approval_rule_id` | integer | yes | The ID of a approval rule |
## Approve Merge Request
>**Note:** This API endpoint is only available on 8.9 Starter and above.
......
......@@ -4,7 +4,7 @@ class ApprovalMergeRequestRulePolicy < BasePolicy
delegate { @subject.merge_request }
condition(:editable) do
can?(:update_merge_request, @subject.merge_request)
can?(:update_merge_request, @subject.merge_request) && @subject.regular?
end
rule { editable }.enable :edit_approval_rule
......
......@@ -3,21 +3,17 @@
module ApprovalRules
class BaseService < ::BaseService
def execute
return error(['Prohibited']) unless can_edit?
return error(['Prohibited'], 403) unless can_edit?
filter_eligible_users!
filter_eligible_groups!
if rule.update(params)
rule.reset
success
else
error(rule.errors.messages)
end
action
end
private
def action
raise 'Not implemented'
end
attr_reader :rule
def can_edit?
......@@ -27,17 +23,5 @@ module ApprovalRules
def success(*args, &blk)
super.tap { |hsh| hsh[:rule] = rule }
end
def filter_eligible_users!
return unless params.key?(:user_ids)
params[:users] = project.members_among(User.id_in(params.delete(:user_ids)))
end
def filter_eligible_groups!
return unless params.key?(:group_ids)
params[:groups] = Group.id_in(params.delete(:group_ids)).public_or_visible_to_user(current_user)
end
end
end
......@@ -2,6 +2,8 @@
module ApprovalRules
class CreateService < ::ApprovalRules::BaseService
include ::ApprovalRules::Updater
# @param target [Project, MergeRequest]
def initialize(target, user, params)
@rule = target.approval_rules.build
......@@ -12,7 +14,27 @@ module ApprovalRules
params.reverse_merge!(rule_type: :report_approver)
end
copy_approval_project_rule_properties(params) if target.is_a?(MergeRequest)
super(@rule.project, user, params)
end
private
def copy_approval_project_rule_properties(params)
return if params[:approval_project_rule_id].blank?
approval_project_rule = @rule.project.approval_rules.find_by_id(params[:approval_project_rule_id])
return if approval_project_rule.blank?
# Remove the following from params so when set they'll be ignored
params.delete(:user_ids)
params.delete(:group_ids)
params[:name] = approval_project_rule.name
params[:users] = approval_project_rule.users
params[:groups] = approval_project_rule.groups
end
end
end
# frozen_string_literal: true
module ApprovalRules
class MergeRequestRuleDestroyService < ::ApprovalRules::BaseService
def initialize(approval_rule, user)
@rule = approval_rule
super(@rule.project, user, params)
end
def action
@rule.destroy
if @rule.destroyed?
success
else
error(rule.errors.messages)
end
end
end
end
......@@ -2,6 +2,8 @@
module ApprovalRules
class UpdateService < ::ApprovalRules::BaseService
include ::ApprovalRules::Updater
attr_reader :rule, :keep_existing_hidden_groups
def initialize(approval_rule, user, params)
......
# frozen_string_literal: true
module ApprovalRules
module Updater
def action
filter_eligible_users!
filter_eligible_groups!
if rule.update(params)
rule.reset
success
else
error(rule.errors.messages)
end
end
private
def filter_eligible_users!
return unless params.key?(:user_ids)
params[:users] = project.members_among(User.id_in(params.delete(:user_ids)))
end
def filter_eligible_groups!
return unless params.key?(:group_ids)
params[:groups] = Group.id_in(params.delete(:group_ids)).public_or_visible_to_user(current_user)
end
end
end
---
title: Implement public MR-level approval rules API
merge_request: 15441
author:
type: added
# frozen_string_literal: true
module API
module Helpers
module ApprovalHelpers
def present_approval(merge_request)
present merge_request.approval_state, with: ::EE::API::Entities::ApprovalState, current_user: current_user
end
end
end
end
......@@ -40,7 +40,7 @@ module API
if result[:status] == :success
present result[:rule], with: present_with, current_user: current_user
else
render_api_error!(result[:message], 400)
render_api_error!(result[:message], result[:http_status] || 400)
end
end
......@@ -54,7 +54,7 @@ module API
if result[:status] == :success
present result[:rule], with: present_with, current_user: current_user
else
render_api_error!(result[:message], 400)
render_api_error!(result[:message], result[:http_status] || 400)
end
end
......
# frozen_string_literal: true
module API
class MergeRequestApprovalRules < ::Grape::API
before { authenticate_non_get! }
ARRAY_COERCION_LAMBDA = ->(val) { val.empty? ? [] : Array.wrap(val) }
helpers do
def find_merge_request_approval_rule(merge_request, id)
merge_request.approval_rules.find_by_id!(id)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/merge_requests/:merge_request_iid/approval_rules' do
desc 'Get all merge request approval rules' do
success EE::API::Entities::MergeRequestApprovalRule
end
get do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.approval_rules, with: EE::API::Entities::MergeRequestApprovalRule, current_user: current_user
end
desc 'Create new merge request approval rules' do
success EE::API::Entities::MergeRequestApprovalRule
end
params do
requires :name, type: String, desc: 'The name of the approval rule'
requires :approvals_required, type: Integer, desc: 'The number of required approvals for this rule'
optional :approval_project_rule_id, type: Integer, desc: 'The ID of a project-level approval rule'
optional :user_ids, type: Array, coerce_with: ARRAY_COERCION_LAMBDA, desc: 'The user ids for this rule'
optional :group_ids, type: Array, coerce_with: ARRAY_COERCION_LAMBDA, desc: 'The group ids for this rule'
end
post do
merge_request = find_merge_request_with_access(params[:merge_request_iid], :update_approvers)
result = ::ApprovalRules::CreateService.new(merge_request, current_user, declared_params(include_missing: false)).execute
if result[:status] == :success
present result[:rule], with: EE::API::Entities::MergeRequestApprovalRule, current_user: current_user
else
render_api_error!(result[:message], result[:http_status] || 400)
end
end
segment ':approval_rule_id' do
desc 'Update merge request approval rule' do
success EE::API::Entities::MergeRequestApprovalRule
end
params do
requires :approval_rule_id, type: Integer, desc: 'The ID of an approval rule'
optional :name, type: String, desc: 'The name of the approval rule'
optional :approvals_required, type: Integer, desc: 'The number of required approvals for this rule'
optional :user_ids, type: Array, coerce_with: ARRAY_COERCION_LAMBDA, desc: 'The user ids for this rule'
optional :group_ids, type: Array, coerce_with: ARRAY_COERCION_LAMBDA, desc: 'The group ids for this rule'
optional :remove_hidden_groups, type: Boolean, desc: 'Whether hidden groups should be removed'
end
put do
merge_request = find_merge_request_with_access(params[:merge_request_iid], :update_approvers)
params = declared_params(include_missing: false)
approval_rule = find_merge_request_approval_rule(merge_request, params.delete(:approval_rule_id))
result = ::ApprovalRules::UpdateService.new(approval_rule, current_user, params).execute
if result[:status] == :success
present result[:rule], with: EE::API::Entities::MergeRequestApprovalRule, current_user: current_user
else
render_api_error!(result[:message], result[:http_status] || 400)
end
end
desc 'Destroy merge request approval rule'
params do
requires :approval_rule_id, type: Integer, desc: 'The ID of an approval_rule'
end
delete do
merge_request = find_merge_request_with_access(params[:merge_request_iid], :update_approvers)
approval_rule = find_merge_request_approval_rule(merge_request, params[:approval_rule_id])
destroy_conditionally!(approval_rule) do |rule|
result = ::ApprovalRules::MergeRequestRuleDestroyService.new(rule, current_user).execute
if result[:status] == :error
render_api_error!(result[:message], result[:http_status] || 400)
end
end
end
end
end
end
end
end
......@@ -6,8 +6,11 @@ module API
ARRAY_COERCION_LAMBDA = ->(val) { val.empty? ? [] : Array.wrap(val) }
helpers ::API::Helpers::ApprovalHelpers
helpers do
def present_approval(merge_request)
present merge_request.approval_state, with: ::EE::API::Entities::ApprovalState, current_user: current_user
end
def handle_merge_request_errors!(errors)
if errors.has_key? :project_access
error!(errors[:project_access], 422)
......@@ -50,13 +53,13 @@ module API
end
desc 'List approval rules for merge request', {
success: ::EE::API::Entities::MergeRequestApprovalRules,
success: ::EE::API::Entities::MergeRequestApprovalSettings,
hidden: true
}
get 'approval_settings' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.approval_state, with: ::EE::API::Entities::MergeRequestApprovalRules, current_user: current_user
present merge_request.approval_state, with: ::EE::API::Entities::MergeRequestApprovalSettings, current_user: current_user
end
desc 'Change approval-related configuration' do
......
......@@ -36,6 +36,7 @@ module EE
mount ::API::ProjectApprovals
mount ::API::Vulnerabilities
mount ::API::MergeRequestApprovals
mount ::API::MergeRequestApprovalRules
mount ::API::ProjectAliases
mount ::API::Dependencies
......
......@@ -317,41 +317,55 @@ module EE
expose :id, :name, :rule_type
end
class ApprovalSettingRule < ApprovalRuleShort
class ApprovalRule < ApprovalRuleShort
def initialize(object, options = {})
presenter = ::ApprovalRulePresenter.new(object, current_user: options[:current_user])
super(presenter, options)
end
expose :approvers, using: ::API::Entities::UserBasic
expose :approvers, as: :eligible_approvers, using: ::API::Entities::UserBasic
expose :approvals_required
expose :users, using: ::API::Entities::UserBasic
expose :groups, using: ::API::Entities::Group
expose :contains_hidden_groups?, as: :contains_hidden_groups
end
class ApprovalRule < ApprovalSettingRule
expose :approvers, as: :eligible_approvers, using: ::API::Entities::UserBasic, override: true
# Being used in private project-level approvals API.
# This overrides the `eligible_approvers` to be exposed as `approvers`.
#
# To be removed in https://gitlab.com/gitlab-org/gitlab-ee/issues/13574.
class ApprovalSettingRule < ApprovalRule
expose :approvers, using: ::API::Entities::UserBasic, override: true
end
class MergeRequestApprovalRule < ApprovalSettingRule
class MergeRequestApprovalRule < ApprovalRule
class SourceRule < Grape::Entity
expose :approvals_required
end
expose :approved_approvers, as: :approved_by, using: ::API::Entities::UserBasic
expose :code_owner
expose :source_rule, using: SourceRule
end
# Being used in private MR-level approvals API.
# This overrides the `eligible_approvers` to be exposed as `approvers` and
# include additional properties.
#
# To be made public in https://gitlab.com/gitlab-org/gitlab-ee/issues/13712
# and the `approvers` override can be removed.
class MergeRequestApprovalSettingRule < MergeRequestApprovalRule
expose :approvers, using: ::API::Entities::UserBasic, override: true
expose :code_owner
expose :approved_approvers, as: :approved_by, using: ::API::Entities::UserBasic
expose :approved?, as: :approved
end
# Decorates ApprovalState
class MergeRequestApprovalRules < Grape::Entity
class MergeRequestApprovalSettings < Grape::Entity
expose :approval_rules_overwritten do |approval_state|
approval_state.approval_rules_overwritten?
end
expose :wrapped_approval_rules, as: :rules, using: MergeRequestApprovalRule
expose :wrapped_approval_rules, as: :rules, using: MergeRequestApprovalSettingRule
end
# Decorates Project
......
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"approvals_required": { "type": "integer" },
"contains_hidden_groups": { "type": "boolean" },
"rule_type": { "type": "string" },
"source_rule": {
"type":["null", "object"],
"properties": {}
},
"eligible_approvers": {
"type": "array",
"items": {
"type": "object",
"properties": {}
}
},
"groups": {
"type": "array",
"items": {
"type": "object",
"properties": {}
}
},
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {}
}
}
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "./merge_request_approval_rule.json" }
}
......@@ -4,7 +4,7 @@ require 'spec_helper'
describe ApprovalMergeRequestRulePolicy do
let(:merge_request) { create(:merge_request) }
let!(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
let(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
def permissions(user, approval_rule)
described_class.new(user, approval_rule)
......@@ -14,10 +14,18 @@ describe ApprovalMergeRequestRulePolicy do
it 'allows updating approval rule' do
expect(permissions(merge_request.author, approval_rule)).to be_allowed(:edit_approval_rule)
end
context 'when rule is not regular type' do
let(:approval_rule) { create(:code_owner_rule, merge_request: merge_request) }
it 'disallows updating approval rule' do
expect(permissions(merge_request.author, approval_rule)).to be_disallowed(:edit_approval_rule)
end
end
end
context 'when user cannot update merge request' do
it 'disallow updating approval rule' do
it 'disallows updating approval rule' do
expect(permissions(create(:user), approval_rule)).to be_disallowed(:edit_approval_rule)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe API::MergeRequestApprovalRules do
set(:user) { create(:user) }
set(:other_user) { create(:user) }
set(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
let(:merge_request) { create(:merge_request, author: user, source_project: project, target_project: project) }
shared_examples_for 'a protected API endpoint for merge request approval rule action' do
context 'disable_overriding_approvers_per_merge_request is set to true' do
before do
project.update!(disable_overriding_approvers_per_merge_request: true)
action
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
end
context 'disable_overriding_approvers_per_merge_request is set to false' do
before do
project.update!(disable_overriding_approvers_per_merge_request: false)
action
end
context 'user cannot update merge request' do
let(:current_user) { other_user }
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
end
end
end
shared_examples_for 'a protected API endpoint that only allows action on regular merge request approval rule' do
context 'approval rule is not a regular type' do
let(:approval_rule) { create(:code_owner_rule, merge_request: merge_request) }
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/approval_rules' do
let(:current_user) { other_user }
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules" }
context 'user cannot read merge request' do
before do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
get api(url, other_user)
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
end
context 'use can read merge request' do
let(:approver) { create(:user) }
let(:group) { create(:group) }
let(:source_rule) { nil }
let(:users) { [approver] }
let(:groups) { [group] }
let!(:mr_approval_rule) do
create(
:approval_merge_request_rule,
merge_request: merge_request,
approval_project_rule: source_rule,
users: users,
groups: groups
)
end
before do
group.add_developer(approver)
merge_request.approvals.create(user: approver)
get api(url, current_user)
end
it 'matches the response schema' do
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/merge_request_approval_rules', dir: 'ee')
rules = json_response
expect(rules.size).to eq(1)
expect(rules.first['name']).to eq(mr_approval_rule.name)
expect(rules.first['approvals_required']).to eq(mr_approval_rule.approvals_required)
expect(rules.first['rule_type']).to eq(mr_approval_rule.rule_type)
expect(rules.first['contains_hidden_groups']).to eq(false)
expect(rules.first['source_rule']).to be_nil
expect(rules.first['eligible_approvers']).to match([hash_including('id' => approver.id)])
expect(rules.first['users']).to match([hash_including('id' => approver.id)])
expect(rules.first['groups']).to match([hash_including('id' => group.id)])
end
context 'groups contain private groups' do
let(:group) { create(:group, :private) }
context 'current_user cannot see private group' do
it 'hides private group' do
rules = json_response
expect(rules.first['contains_hidden_groups']).to eq(true)
expect(rules.first['groups']).to be_empty
end
end
context 'current_user can see private group' do
let(:current_user) { approver }
it 'shows private group' do
rules = json_response
expect(rules.first['contains_hidden_groups']).to eq(false)
expect(rules.first['groups']).to match([hash_including('id' => group.id)])
end
end
end
context 'has existing merge request rule that overrides a project-level rule' do
let(:source_rule) { create(:approval_project_rule, project: project) }
it 'includes source_rule' do
expect(json_response.first['source_rule']['approvals_required']).to eq(source_rule.approvals_required)
end
end
end
end
describe 'POST /projects/:id/merge_requests/:merge_request_iid/approval_rules' do
let(:current_user) { user }
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules" }
let(:approver) { create(:user) }
let(:group) { create(:group) }
let(:approval_project_rule_id) { nil }
let(:user_ids) { [] }
let(:group_ids) { [] }
let(:params) do
{
name: 'Test',
approvals_required: 1,
approval_project_rule_id: approval_project_rule_id,
user_ids: user_ids,
group_ids: group_ids
}
end
let(:action) { post api(url, current_user), params: params }
it_behaves_like 'a protected API endpoint for merge request approval rule action'
context 'when user can update merge request and approval rules can be overridden' do
before do
project.update!(disable_overriding_approvers_per_merge_request: false)
project.add_developer(approver)
group.add_developer(approver)
action
end
it 'matches the response schema' do
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/merge_request_approval_rule', dir: 'ee')
rule = json_response
expect(rule['name']).to eq(params[:name])
expect(rule['approvals_required']).to eq(params[:approvals_required])
expect(rule['rule_type']).to eq('regular')
expect(rule['contains_hidden_groups']).to eq(false)
expect(rule['source_rule']).to be_nil
expect(rule['eligible_approvers']).to be_empty
expect(rule['users']).to be_empty
expect(rule['groups']).to be_empty
end
context 'users are passed' do
let(:user_ids) { [approver.id] }
it 'includes users' do
rule = json_response
expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)])
expect(rule['users']).to match([hash_including('id' => approver.id)])
end
end
context 'groups are passed' do
let(:group_ids) { [group.id] }
it 'includes groups' do
rule = json_response
expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)])
expect(rule['groups']).to match([hash_including('id' => group.id)])
end
end
context 'approval_project_rule_id is passed' do
let(:approval_project_rule) do
create(
:approval_project_rule,
project: project,
users: [approver],
groups: [group]
)
end
let(:approval_project_rule_id) { approval_project_rule.id }
it 'copies the attributes from the project rule execpt approvals_required' do
rule = json_response
expect(rule['name']).to eq(approval_project_rule.name)
expect(rule['approvals_required']).to eq(params[:approvals_required])
expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required)
expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)])
expect(rule['users']).to match([hash_including('id' => approver.id)])
expect(rule['groups']).to match([hash_including('id' => group.id)])
end
end
end
end
describe 'PUT /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id' do
let(:current_user) { user }
let(:existing_approver) { create(:user) }
let(:existing_group) { create(:group) }
let(:approval_rule) do
create(
:approval_merge_request_rule,
merge_request: merge_request,
name: 'Old Name',
approvals_required: 2,
users: [existing_approver],
groups: [existing_group]
)
end
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules/#{approval_rule.id}" }
let(:new_approver) { create(:user) }
let(:new_group) { create(:group) }
let(:user_ids) { [] }
let(:group_ids) { [] }
let(:remove_hidden_groups) { nil }
let(:params) do
{
name: 'Test',
approvals_required: 1,
user_ids: user_ids,
group_ids: group_ids,
remove_hidden_groups: remove_hidden_groups
}
end
let(:action) { put api(url, current_user), params: params }
it_behaves_like 'a protected API endpoint for merge request approval rule action'
context 'when user can update merge request and approval rules can be overridden' do
before do
project.update!(disable_overriding_approvers_per_merge_request: false)
project.add_developer(existing_approver)
project.add_developer(new_approver)
existing_group.add_developer(existing_approver)
new_group.add_developer(new_approver)
action
end
it_behaves_like 'a protected API endpoint that only allows action on regular merge request approval rule'
it 'matches the response schema' do
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/merge_request_approval_rule', dir: 'ee')
rule = json_response
expect(rule['name']).to eq(params[:name])
expect(rule['approvals_required']).to eq(params[:approvals_required])
expect(rule['rule_type']).to eq(approval_rule.rule_type)
expect(rule['contains_hidden_groups']).to eq(false)
expect(rule['source_rule']).to be_nil
expect(rule['eligible_approvers']).to be_empty
expect(rule['users']).to be_empty
expect(rule['groups']).to be_empty
end
context 'users are passed' do
let(:user_ids) { [new_approver.id] }
it 'changes users' do
rule = json_response
expect(rule['eligible_approvers']).to match([hash_including('id' => new_approver.id)])
expect(rule['users']).to match([hash_including('id' => new_approver.id)])
end
end
context 'groups are passed' do
let(:group_ids) { [new_group.id] }
it 'changes groups' do
rule = json_response
expect(rule['eligible_approvers']).to match([hash_including('id' => new_approver.id)])
expect(rule['groups']).to match([hash_including('id' => new_group.id)])
end
end
context 'remove_hidden_groups is passed' do
let(:existing_group) { create(:group, :private) }
context 'when set to true' do
let(:remove_hidden_groups) { true }
it 'removes the existing private group' do
expect(approval_rule.reload.groups).not_to include(existing_group)
end
end
context 'when set to false' do
let(:remove_hidden_groups) { false }
it 'does not remove the existing private group' do
expect(approval_rule.reload.groups).to include(existing_group)
end
end
end
end
end
describe 'DELETE /projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id' do
let(:current_user) { user }
let(:approval_rule) { create(:approval_merge_request_rule, merge_request: merge_request) }
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approval_rules/#{approval_rule.id}" }
let(:action) { delete api(url, current_user) }
it_behaves_like 'a protected API endpoint for merge request approval rule action'
context 'when user can update merge request and approval rules can be overridden' do
before do
project.update!(disable_overriding_approvers_per_merge_request: false)
action
end
it_behaves_like 'a protected API endpoint that only allows action on regular merge request approval rule'
it 'responds with 204' do
expect(response).to have_gitlab_http_status(204)
end
end
end
end
......@@ -106,38 +106,59 @@ describe ApprovalRules::CreateService do
it_behaves_like "creatable"
context 'when project rule id is present' do
let(:project_rule) { create(:approval_project_rule, project: project) }
let(:project_rule) do
create(
:approval_project_rule,
project: project,
name: 'bar',
approvals_required: 1,
users: [create(:user)],
groups: [create(:group)]
)
end
it 'associates with project rule' do
result = described_class.new(target, user, {
let(:result) do
described_class.new(target, user, {
name: 'foo',
approvals_required: 1,
approval_project_rule_id: project_rule.id
approvals_required: 0,
approval_project_rule_id: project_rule.id,
user_ids: [],
group_ids: []
}).execute
end
expect(result[:status]).to eq(:success)
rule = result[:rule]
let(:rule) { result[:rule] }
it 'associates with project rule' do
expect(result[:status]).to eq(:success)
expect(rule.approvals_required).to eq(0)
expect(rule.approval_project_rule).to eq(project_rule)
end
end
context "when project rule id is not the same as MR's project" do
let(:project_rule) { create(:approval_project_rule) }
it 'copies properties from the project rule' do
expect(rule.name).to eq(project_rule.name)
expect(rule.users).to match(project_rule.users)
expect(rule.groups).to match(project_rule.groups)
end
it 'ignores assignment' do
result = described_class.new(target, user, {
name: 'foo',
approvals_required: 1,
approval_project_rule_id: project_rule.id
}).execute
context 'when project rule is under the same project as MR' do
let(:another_project) { create(:project) }
expect(result[:status]).to eq(:success)
before do
project_rule.update!(project: another_project)
end
rule = result[:rule]
it 'ignores assignment' do
expect(result[:status]).to eq(:success)
expect(rule.approvals_required).to eq(0)
expect(rule.approval_project_rule).to eq(nil)
end
expect(rule.approval_project_rule).to eq(nil)
it 'does not copy properties from project rule' do
expect(rule.name).to eq('foo')
expect(rule.users).to be_empty
expect(rule.groups).to be_empty
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe ApprovalRules::MergeRequestRuleDestroyService do
let(:rule) { create(:approval_merge_request_rule) }
let(:user) { create(:user) }
subject(:result) { described_class.new(rule, user).execute }
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability)
.to receive(:allowed?)
.with(user, :edit_approval_rule, rule)
.at_least(:once)
.and_return(can_edit?)
end
context 'user cannot edit approval rule' do
let(:can_edit?) { false }
it 'returns error status' do
expect(result[:status]).to eq(:error)
end
end
context 'user can edit approval rule' do
let(:can_edit?) { true }
context 'when rule successfully deleted' do
it 'returns successful status' do
expect(result[:status]).to eq(:success)
end
end
context 'when rule not successfully deleted' do
before do
allow(rule).to receive(:destroyed?).and_return(false)
end
it 'returns error status' do
expect(result[:status]).to eq(:error)
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