Commit af0cc1f3 authored by Tan Le's avatar Tan Le

Extract project push rule to service class

Push rules can be treated as a type of settings, such as
application_settings, project_settings, etc. There must be only one for
a given project. Therefore, it would be better to just expose the update
interface and let the creation handled behind the scene.

This commit extracts project push rule create and update logic to
a dedicated service class. This change allows to centralize auditing
and logging of actions from Project Push Rule controller and API.

The POST and PUT API can handle both create and update without raising
`unprocessable_entity` and `not_found` error respectively. Since the
user can only manage push rule via project id and they can only ever
create one, this change reduces coordinating POST request before PUT
request.
parent dd6ec7c3
...@@ -14,16 +14,19 @@ class Projects::PushRulesController < Projects::ApplicationController ...@@ -14,16 +14,19 @@ class Projects::PushRulesController < Projects::ApplicationController
feature_category :source_code_management feature_category :source_code_management
def update def update
@push_rule = project.push_rule service_response = PushRules::CreateOrUpdateService.new(
@push_rule.update(push_rule_params) container: project,
current_user: current_user,
params: push_rule_params
).execute
if @push_rule.valid? if service_response.success?
flash[:notice] = _('Push Rules updated successfully.') flash[:notice] = _('Push Rules updated successfully.')
else else
flash[:alert] = @push_rule.errors.full_messages.join(', ').html_safe flash[:alert] = service_response.message
end end
redirect_to_repository_settings(@project, anchor: 'js-push-rules') redirect_to_repository_settings(project, anchor: 'js-push-rules')
end end
private private
......
...@@ -38,7 +38,7 @@ module EE ...@@ -38,7 +38,7 @@ module EE
has_one :repository_state, class_name: 'ProjectRepositoryState', inverse_of: :project has_one :repository_state, class_name: 'ProjectRepositoryState', inverse_of: :project
has_one :project_registry, class_name: 'Geo::ProjectRegistry', inverse_of: :project has_one :project_registry, class_name: 'Geo::ProjectRegistry', inverse_of: :project
has_one :push_rule, ->(project) { project&.feature_available?(:push_rules) ? all : none } has_one :push_rule, ->(project) { project&.feature_available?(:push_rules) ? all : none }, inverse_of: :project
has_one :index_status has_one :index_status
has_one :github_service has_one :github_service
......
...@@ -19,7 +19,7 @@ class PushRule < ApplicationRecord ...@@ -19,7 +19,7 @@ class PushRule < ApplicationRecord
branch_name_regex branch_name_regex
].freeze ].freeze
belongs_to :project belongs_to :project, inverse_of: :push_rule
validates :max_file_size, numericality: { greater_than_or_equal_to: 0, only_integer: true } validates :max_file_size, numericality: { greater_than_or_equal_to: 0, only_integer: true }
validates(*REGEX_COLUMNS, untrusted_regexp: true) validates(*REGEX_COLUMNS, untrusted_regexp: true)
......
# frozen_string_literal: true
module PushRules
class CreateOrUpdateService < BaseContainerService
def execute
push_rule = container.push_rule || container.build_push_rule
if push_rule.update(params)
ServiceResponse.success(payload: { push_rule: push_rule })
else
error_message = push_rule.errors.full_messages.to_sentence
ServiceResponse.error(message: error_message, payload: { push_rule: push_rule })
end
end
end
end
...@@ -8,6 +8,24 @@ module API ...@@ -8,6 +8,24 @@ module API
before { check_project_feature_available!(:push_rules) } before { check_project_feature_available!(:push_rules) }
before { authorize_change_param(user_project, :commit_committer_check, :reject_unsigned_commits) } before { authorize_change_param(user_project, :commit_committer_check, :reject_unsigned_commits) }
helpers do
def create_or_update_push_rule
service_response = PushRules::CreateOrUpdateService.new(
container: user_project,
current_user: current_user,
params: declared_params(include_missing: false)
).execute
push_rule = service_response.payload[:push_rule]
if service_response.success?
present(push_rule, with: EE::API::Entities::ProjectPushRule, user: current_user)
else
render_validation_error!(push_rule)
end
end
end
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
...@@ -48,12 +66,8 @@ module API ...@@ -48,12 +66,8 @@ module API
use :push_rule_params use :push_rule_params
end end
post ":id/push_rule" do post ":id/push_rule" do
if user_project.push_rule unprocessable_entity!('Project push rule exists') if user_project.push_rule
error!("Project push rule exists", 422) create_or_update_push_rule
else
push_rule = user_project.create_push_rule(declared_params(include_missing: false))
present push_rule, with: EE::API::Entities::ProjectPushRule, user: current_user
end
end end
desc 'Update an existing project push rule' do desc 'Update an existing project push rule' do
...@@ -63,14 +77,8 @@ module API ...@@ -63,14 +77,8 @@ module API
use :push_rule_params use :push_rule_params
end end
put ":id/push_rule" do put ":id/push_rule" do
push_rule = user_project.push_rule not_found!('Push Rule') unless user_project.push_rule
not_found!('Push Rule') unless push_rule create_or_update_push_rule
if push_rule.update(declared_params(include_missing: false))
present push_rule, with: EE::API::Entities::ProjectPushRule, user: current_user
else
render_validation_error!(push_rule)
end
end end
desc 'Deletes project push rule' desc 'Deletes project push rule'
......
...@@ -25,6 +25,7 @@ RSpec.describe Project do ...@@ -25,6 +25,7 @@ RSpec.describe Project do
it { is_expected.to have_one(:import_state).class_name('ProjectImportState') } it { is_expected.to have_one(:import_state).class_name('ProjectImportState') }
it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) } it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) }
it { is_expected.to have_one(:push_rule).inverse_of(:project) }
it { is_expected.to have_one(:status_page_setting).class_name('StatusPage::ProjectSetting') } it { is_expected.to have_one(:status_page_setting).class_name('StatusPage::ProjectSetting') }
it { is_expected.to have_one(:compliance_framework_setting).class_name('ComplianceManagement::ComplianceFramework::ProjectSettings') } it { is_expected.to have_one(:compliance_framework_setting).class_name('ComplianceManagement::ComplianceFramework::ProjectSettings') }
it { is_expected.to have_one(:compliance_management_framework).class_name('ComplianceManagement::Framework') } it { is_expected.to have_one(:compliance_management_framework).class_name('ComplianceManagement::Framework') }
......
...@@ -11,7 +11,7 @@ RSpec.describe PushRule do ...@@ -11,7 +11,7 @@ RSpec.describe PushRule do
let(:project) { Projects::CreateService.new(user, { name: 'test', namespace: user.namespace }).execute } let(:project) { Projects::CreateService.new(user, { name: 'test', namespace: user.namespace }).execute }
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project).inverse_of(:push_rule) }
end end
describe "Validation" do describe "Validation" do
......
...@@ -4,9 +4,10 @@ require 'spec_helper' ...@@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) } let_it_be(:user) { create(:user) }
let!(:project) { create(:project, :repository, creator_id: user.id, namespace: user.namespace) } let_it_be(:user3) { create(:user) }
let_it_be(:project) { create(:project, :repository, creator_id: user.id, namespace: user.namespace) }
before do before do
stub_licensed_features(push_rules: push_rules_enabled, stub_licensed_features(push_rules: push_rules_enabled,
...@@ -192,6 +193,15 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -192,6 +193,15 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
end end
end end
end end
context 'invalid params', :aggregate_failures do
let(:rules_params) { { max_file_size: -10 } }
it 'returns an error' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to match('max_file_size' => ['must be greater than or equal to 0'])
end
end
end end
it 'adds push rule to project with no file size' do it 'adds push rule to project with no file size' do
...@@ -217,16 +227,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -217,16 +227,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end
describe "POST /projects/:id/push_rule" do context "with existing push rule" do
before do before do
create(:push_rule, project: project) create(:push_rule, project: project)
end end
context "with existing push rule" do it 'returns an error response' do
it "does not add push rule to project" do post api("/projects/#{project.id}/push_rule", user), params: rules_params
post api("/projects/#{project.id}/push_rule", user), params: { deny_delete_tag: true }
expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(response).to have_gitlab_http_status(:unprocessable_entity)
end end
...@@ -234,22 +242,27 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -234,22 +242,27 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
end end
describe "PUT /projects/:id/push_rule" do describe "PUT /projects/:id/push_rule" do
before do subject(:request) do
create(:push_rule, project: project,
deny_delete_tag: true, commit_message_regex: 'Mended')
put api("/projects/#{project.id}/push_rule", user), params: new_settings put api("/projects/#{project.id}/push_rule", user), params: new_settings
end end
context 'with existing push rule' do
let_it_be(:push_rule) { create(:push_rule, project: project, deny_delete_tag: true, commit_message_regex: 'Mended') }
context "setting deny_delete_tag and commit_message_regex" do context "setting deny_delete_tag and commit_message_regex" do
let(:new_settings) do let(:new_settings) do
{ deny_delete_tag: false, commit_message_regex: 'Fixes \d+\..*' } { deny_delete_tag: false, commit_message_regex: 'Fixes \d+\..*' }
end end
it "is successful" do it "is successful" do
request
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'includes the expected settings' do it 'includes the expected settings' do
request
subset = new_settings.transform_keys(&:to_s) subset = new_settings.transform_keys(&:to_s)
expect(json_response).to include(subset) expect(json_response).to include(subset)
end end
...@@ -259,10 +272,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -259,10 +272,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
let(:new_settings) { { commit_committer_check: true } } let(:new_settings) { { commit_committer_check: true } }
it "is successful" do it "is successful" do
request
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it "sets the commit_committer_check" do it "sets the commit_committer_check" do
request
expect(json_response).to include('commit_committer_check' => true) expect(json_response).to include('commit_committer_check' => true)
end end
...@@ -270,6 +287,8 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -270,6 +287,8 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
let(:ccc_enabled) { false } let(:ccc_enabled) { false }
it "is an error to provide this parameter" do it "is an error to provide this parameter" do
request
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
...@@ -279,10 +298,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -279,10 +298,14 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
let(:new_settings) { { reject_unsigned_commits: true } } let(:new_settings) { { reject_unsigned_commits: true } }
it "is successful" do it "is successful" do
request
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it "sets the reject_unsigned_commits" do it "sets the reject_unsigned_commits" do
request
expect(json_response).to include('reject_unsigned_commits' => true) expect(json_response).to include('reject_unsigned_commits' => true)
end end
...@@ -290,6 +313,8 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -290,6 +313,8 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
let(:ruc_enabled) { false } let(:ruc_enabled) { false }
it "is an error to provide the this parameter" do it "is an error to provide the this parameter" do
request
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
...@@ -299,30 +324,44 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -299,30 +324,44 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
let(:new_settings) { {} } let(:new_settings) { {} }
it "is an error" do it "is an error" do
request
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'invalid params', :aggregate_failures do
let(:new_settings) { { max_file_size: -10 } }
it 'returns an error' do
request
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to match('max_file_size' => ['must be greater than or equal to 0'])
end end
end end
end end
describe "PUT /projects/:id/push_rule" do context 'without existing push rule' do
it "gets error on non existing project push rule" do let(:new_settings) { { commit_committer_check: true } }
put api("/projects/#{project.id}/push_rule", user),
params: { deny_delete_tag: false, commit_message_regex: 'Fixes \d+\..*' } it 'returns an error response', :aggregate_failures do
expect { request }.not_to change { PushRule.count }
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end
it "does not update push rule for unauthorized user" do it "does not update push rule for unauthorized user" do
post api("/projects/#{project.id}/push_rule", user3), params: { deny_delete_tag: true } put api("/projects/#{project.id}/push_rule", user3), params: { deny_delete_tag: true }
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
describe "DELETE /projects/:id/push_rule" do describe "DELETE /projects/:id/push_rule" do
before do context 'for existing push rule' do
create(:push_rule, project: project) let_it_be(:push_rule) { create(:push_rule, project: project) }
end
context "maintainer" do context "maintainer" do
it "deletes push rule from project" do it "deletes push rule from project" do
...@@ -341,7 +380,6 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do ...@@ -341,7 +380,6 @@ RSpec.describe API::ProjectPushRule, 'ProjectPushRule', api: true do
end end
end end
describe "DELETE /projects/:id/push_rule" do
context "for non existing push rule" do context "for non existing push rule" do
it "deletes push rule from project" do it "deletes push rule from project" do
delete api("/projects/#{project.id}/push_rule", user) delete api("/projects/#{project.id}/push_rule", user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PushRules::CreateOrUpdateService, '#execute' do
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:params) { { max_file_size: 28 } }
subject { described_class.new(container: project, current_user: user, params: params) }
shared_examples 'a failed update' do
let(:params) { { max_file_size: -28 } }
it 'responds with an error service response', :aggregate_failures do
response = subject.execute
expect(response).to be_error
expect(response.message).to eq('Max file size must be greater than or equal to 0')
expect(response.payload).to match(push_rule: project.push_rule)
end
end
context 'with existing push rule' do
let_it_be(:push_rule) { create(:push_rule, project: project) }
it 'updates existing push rule' do
expect { subject.execute }
.to change { PushRule.count }.by(0)
.and change { push_rule.reload.max_file_size }.to(28)
end
it 'responds with a successful service response', :aggregate_failures do
response = subject.execute
expect(response).to be_success
expect(response.payload).to match(push_rule: push_rule)
end
it_behaves_like 'a failed update'
end
context 'without existing push rule' do
it 'creates a new push rule', :aggregate_failures do
expect { subject.execute }.to change { PushRule.count }.by(1)
expect(project.push_rule.max_file_size).to eq(28)
end
it 'responds with a successful service response', :aggregate_failures do
response = subject.execute
expect(response).to be_success
expect(response.payload).to match(push_rule: project.push_rule)
end
it_behaves_like 'a failed update'
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