Commit f24f2db0 authored by Vladimir Shushlin's avatar Vladimir Shushlin Committed by Robert Speicher

Add audit logs for feature flags

* Move feature flag CRUD into service objects
* Create audit logs FF CRUD
* Calculate environment scope difference and add audit logs for changed
* scopes on update
* Add specs
* Use custom_message in feature flag audit events
* feature flags html actions and @feature_flag variable
parent 18bb17cf
...@@ -28,7 +28,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -28,7 +28,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
end end
def new def new
@feature_flag = project.operations_feature_flags.new
end end
def show def show
...@@ -36,23 +35,21 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -36,23 +35,21 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) Gitlab::PollingInterval.set_header(response, interval: 10_000)
render_success_json render_success_json(feature_flag)
end end
end end
end end
def create def create
@feature_flag = project.operations_feature_flags.create(create_params) result = FeatureFlags::CreateService.new(project, current_user, create_params).execute
if @feature_flag.persisted? if result[:status] == :success
respond_to do |format| respond_to do |format|
format.html { redirect_to_index(notice: 'Feature flag was successfully created.') } format.json { render_success_json(result[:feature_flag]) }
format.json { render_success_json }
end end
else else
respond_to do |format| respond_to do |format|
format.html { render :new } format.json { render_error_json(result[:message]) }
format.json { render_error_json }
end end
end end
end end
...@@ -61,29 +58,31 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -61,29 +58,31 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
end end
def update def update
if feature_flag.update(update_params) result = FeatureFlags::UpdateService.new(project, current_user, update_params).execute(feature_flag)
if result[:status] == :success
respond_to do |format| respond_to do |format|
format.html { redirect_to_index(notice: 'Feature flag was successfully updated.') } format.json { render_success_json(result[:feature_flag]) }
format.json { render_success_json }
end end
else else
respond_to do |format| respond_to do |format|
format.html { render :edit } format.json { render_error_json(result[:message]) }
format.json { render_error_json }
end end
end end
end end
def destroy def destroy
if feature_flag.destroy result = FeatureFlags::DestroyService.new(project, current_user).execute(feature_flag)
if result[:status] == :success
respond_to do |format| respond_to do |format|
format.html { redirect_to_index(notice: 'Feature flag was successfully removed.') } format.html { redirect_to_index(notice: 'Feature flag was successfully removed.') }
format.json { render_success_json } format.json { render_success_json(feature_flag) }
end end
else else
respond_to do |format| respond_to do |format|
format.html { redirect_to_index(alert: 'Feature flag was not removed.') } format.html { redirect_to_index(alert: 'Feature flag was not removed.') }
format.json { render_error_json } format.json { render_error_json(result[:message]) }
end end
end end
end end
...@@ -106,7 +105,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -106,7 +105,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
scopes_attributes: [:id, :environment_scope, :active, :_destroy]) scopes_attributes: [:id, :environment_scope, :active, :_destroy])
end end
def feature_flag_json def feature_flag_json(feature_flag)
FeatureFlagSerializer FeatureFlagSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.represent(feature_flag) .represent(feature_flag)
...@@ -129,12 +128,12 @@ class Projects::FeatureFlagsController < Projects::ApplicationController ...@@ -129,12 +128,12 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
redirect_to project_feature_flags_path(@project), status: :found, **args redirect_to project_feature_flags_path(@project), status: :found, **args
end end
def render_success_json def render_success_json(feature_flag)
render json: feature_flag_json, status: :ok render json: feature_flag_json(feature_flag), status: :ok
end end
def render_error_json def render_error_json(messages)
render json: { message: feature_flag.errors.full_messages }, render json: { message: messages },
status: :bad_request status: :bad_request
end end
end end
# frozen_string_literal: true
module FeatureFlags
class BaseService < ::BaseService
AUDITABLE_ATTRIBUTES = %w(name description).freeze
protected
def audit_enabled?
Feature.enabled?(:feature_flag_audit, project, default_enabled: true)
end
def audit_event(feature_flag)
return unless audit_enabled?
message = audit_message(feature_flag)
return if message.blank?
details =
{
custom_message: message,
target_id: feature_flag.id,
target_type: feature_flag.class.name,
target_details: feature_flag.name
}
::AuditEventService.new(
current_user,
feature_flag.project,
details
)
end
def save_audit_event(audit_event)
return unless audit_event # feature_flag_audit is disabled or audit_message is blank
audit_event.security_event
end
def created_scope_message(scope)
"Created rule <strong>#{scope.environment_scope}</strong> "\
"and set it as <strong>#{scope.active ? "active" : "inactive"}</strong>."
end
end
end
# frozen_string_literal: true
module FeatureFlags
class CreateService < FeatureFlags::BaseService
def execute
ActiveRecord::Base.transaction do
feature_flag = project.operations_feature_flags.new(params)
if feature_flag.save
save_audit_event(audit_event(feature_flag))
success(feature_flag: feature_flag)
else
error(feature_flag.errors.full_messages)
end
end
end
private
def audit_message(feature_flag)
message_parts = ["Created feature flag <strong>#{feature_flag.name}</strong>",
"with description <strong>\"#{feature_flag.description}\"</strong>."]
message_parts += feature_flag.scopes.map do |scope|
created_scope_message(scope)
end
message_parts.join(" ")
end
end
end
# frozen_string_literal: true
module FeatureFlags
class DestroyService < FeatureFlags::BaseService
def execute(feature_flag)
ActiveRecord::Base.transaction do
if feature_flag.destroy
save_audit_event(audit_event(feature_flag))
success(feature_flag: feature_flag)
else
error(feature_flag.errors.full_messages)
end
end
end
private
def audit_message(feature_flag)
"Deleted feature flag <strong>#{feature_flag.name}</strong>."
end
end
end
# frozen_string_literal: true
module FeatureFlags
class UpdateService < FeatureFlags::BaseService
AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES = {
'active' => 'active state',
'environment_scope' => 'environment scope'
}.freeze
def execute(feature_flag)
ActiveRecord::Base.transaction do
feature_flag.assign_attributes(params)
audit_event = audit_event(feature_flag)
if feature_flag.save
save_audit_event(audit_event)
success(feature_flag: feature_flag)
else
error(feature_flag.errors.full_messages)
end
end
end
private
def audit_message(feature_flag)
changes = changed_attributes_messages(feature_flag)
changes += changed_scopes_messages(feature_flag)
return if changes.empty?
"Updated feature flag <strong>#{feature_flag.name}</strong>. " + changes.join(" ")
end
def changed_attributes_messages(feature_flag)
feature_flag.changes.slice(*AUDITABLE_ATTRIBUTES).map do |attribute_name, changes|
"Updated #{attribute_name} "\
"from <strong>\"#{changes.first}\"</strong> to "\
"<strong>\"#{changes.second}\"</strong>."
end
end
def changed_scopes_messages(feature_flag)
feature_flag.scopes.map do |scope|
if scope.new_record?
created_scope_message(scope)
elsif scope.marked_for_destruction?
deleted_scope_message(scope)
else
updated_scope_message(scope)
end
end.compact # updated_scope_message can return nil if nothing has been changed
end
def deleted_scope_message(scope)
"Deleted rule <strong>#{scope.environment_scope}</strong>."
end
def updated_scope_message(scope)
changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys)
return if changes.empty?
message = "Updated rule <strong>#{scope.environment_scope}</strong> "
message += changes.map do |attribute_name, change|
name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name]
"#{name} from <strong>#{change.first}</strong> to <strong>#{change.second}</strong>"
end.join(' ')
message + '.'
end
end
end
---
title: Add audit log for managing feature flags
merge_request: 9487
author:
type: added
...@@ -300,24 +300,6 @@ describe Projects::FeatureFlagsController do ...@@ -300,24 +300,6 @@ describe Projects::FeatureFlagsController do
end end
end end
describe 'POST create' do
render_views
subject { post(:create, params: params) }
context 'when creating a new feature flag' do
let(:params) do
view_params.merge(operations_feature_flag: { name: 'my_feature_flag', active: true })
end
it 'creates and redirects to list' do
subject
expect(response).to redirect_to(project_feature_flags_path(project))
end
end
end
describe 'POST create.json' do describe 'POST create.json' do
subject { post(:create, params: params, format: :json) } subject { post(:create, params: params, format: :json) }
...@@ -435,29 +417,6 @@ describe Projects::FeatureFlagsController do ...@@ -435,29 +417,6 @@ describe Projects::FeatureFlagsController do
end end
end end
describe 'PUT update' do
let!(:feature_flag) { create(:operations_feature_flag, project: project, name: 'my_feature_flag') }
render_views
subject { put(:update, params: params) }
context 'when updating an existing feature flag' do
let(:params) do
view_params.merge(
id: feature_flag.id,
operations_feature_flag: { name: 'my_feature_flag_v2', active: true }
)
end
it 'updates and redirects to list' do
subject
expect(response).to redirect_to(project_feature_flags_path(project))
end
end
end
describe 'DELETE destroy.json' do describe 'DELETE destroy.json' do
subject { delete(:destroy, params: params, format: :json) } subject { delete(:destroy, params: params, format: :json) }
......
...@@ -15,113 +15,182 @@ describe 'User creates feature flag', :js do ...@@ -15,113 +15,182 @@ describe 'User creates feature flag', :js do
end end
context 'when creates without changing scopes' do context 'when creates without changing scopes' do
before do shared_examples 'succesfully creates feature flag' do
visit(new_project_feature_flag_path(project)) before do
set_feature_flag_info('ci_live_trace', 'For live trace') visit(new_project_feature_flag_path(project))
click_button 'Create feature flag' set_feature_flag_info('ci_live_trace', 'For live trace')
end click_button 'Create feature flag'
expect(page).to have_current_path(project_feature_flags_path(project))
end
it 'shows the created feature flag' do it 'shows the created feature flag' do
within_feature_flag_row(1) do within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace') expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect(page).to have_css('.js-feature-flag-status .badge-success') expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active')
end
end end
end end
end end
context 'when feature flag audit enabled' do
include_examples 'succesfully creates feature flag'
it 'records audit event' do
visit(project_audit_events_path(project))
expect(page).to have_text("Created feature flag ci live trace with description \"For live trace\".")
end
end
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
include_examples 'succesfully creates feature flag'
it 'does not record audit event' do
visit(project_audit_events_path(project))
expect(page).to have_no_text("Created feature flag")
end
end
end end
context 'when creates with disabling the default scope' do context 'when creates with disabling the default scope' do
before do shared_examples 'succesfully creates feature flag' do
visit(new_project_feature_flag_path(project)) before do
set_feature_flag_info('ci_live_trace', 'For live trace') visit(new_project_feature_flag_path(project))
set_feature_flag_info('ci_live_trace', 'For live trace')
within_scope_row(1) do within_scope_row(1) do
within_status { find('.project-feature-toggle').click } within_status { find('.project-feature-toggle').click }
end
click_button 'Create feature flag'
end end
click_button 'Create feature flag' it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect(page).to have_css('.js-feature-flag-status .badge-danger')
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive')
end
end
end
end end
it 'shows the created feature flag' do context 'when feature flag audit enabled' do
within_feature_flag_row(1) do include_examples 'succesfully creates feature flag'
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace') end
expect(page).to have_css('.js-feature-flag-status .badge-danger')
within_feature_flag_scopes do context 'when feature flag audit is disabled' do
expect(page.find('.badge:nth-child(1)')).to have_content('*') before do
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive') stub_feature_flags(feature_flag_audit: false)
end
end end
include_examples 'succesfully creates feature flag'
end end
end end
context 'when creates with an additional scope' do context 'when creates with an additional scope' do
before do shared_examples 'succesfully creates feature flag' do
visit(new_project_feature_flag_path(project)) before do
set_feature_flag_info('mr_train', '') visit(new_project_feature_flag_path(project))
set_feature_flag_info('mr_train', '')
within_scope_row(2) do
within_environment_spec do within_scope_row(2) do
find('.js-env-input').set("review/*") within_environment_spec do
find('.js-create-button').click find('.js-env-input').set("review/*")
find('.js-create-button').click
end
end end
end
within_scope_row(2) do within_scope_row(2) do
within_status { find('.project-feature-toggle').click } within_status { find('.project-feature-toggle').click }
end
click_button 'Create feature flag'
end end
click_button 'Create feature flag' it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('mr_train')
expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-active')
end
end
end
end end
it 'shows the created feature flag' do context 'when feature flag audit enabled' do
within_feature_flag_row(1) do include_examples 'succesfully creates feature flag'
expect(page.find('.feature-flag-name')).to have_content('mr_train') end
expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do context 'when feature flag audit is disabled' do
expect(page.find('.badge:nth-child(1)')).to have_content('*') before do
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') stub_feature_flags(feature_flag_audit: false)
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-active')
end
end end
include_examples 'succesfully creates feature flag'
end end
end end
context 'when searches an environment name for scope creation' do context 'when searches an environment name for scope creation' do
let!(:environment) { create(:environment, name: 'production', project: project) } let!(:environment) { create(:environment, name: 'production', project: project) }
before do shared_examples 'succesfully creates feature flag' do
visit(new_project_feature_flag_path(project)) before do
set_feature_flag_info('mr_train', '') visit(new_project_feature_flag_path(project))
set_feature_flag_info('mr_train', '')
within_scope_row(2) do within_scope_row(2) do
within_environment_spec do within_environment_spec do
find('.js-env-input').set('prod') find('.js-env-input').set('prod')
click_button 'production' click_button 'production'
end
end end
click_button 'Create feature flag'
end end
click_button 'Create feature flag' it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('mr_train')
expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active')
expect(page.find('.badge:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end
end
end
end end
it 'shows the created feature flag' do context 'when feature flag audit enabled' do
within_feature_flag_row(1) do include_examples 'succesfully creates feature flag'
expect(page.find('.feature-flag-name')).to have_content('mr_train') end
expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do context 'when feature flag audit is disabled' do
expect(page.find('.badge:nth-child(1)')).to have_content('*') before do
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') stub_feature_flags(feature_flag_audit: false)
expect(page.find('.badge:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end
end end
include_examples 'succesfully creates feature flag'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User deletes feature flag', :js do
include FeatureFlagHelpers
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:feature_flag) do
create_flag(project, 'ci_live_trace', false,
description: 'For live trace feature')
end
before do
project.add_developer(user)
stub_licensed_features(feature_flags: true)
sign_in(user)
visit(project_feature_flags_path(project))
find('.js-feature-flag-delete-button').click
click_button('Delete feature flag')
expect(page).to have_current_path(project_feature_flags_path(project))
end
it 'user does not see feature flag' do
expect(page).to have_no_content('ci_live_trace')
end
it 'records audit event' do
visit(project_audit_events_path(project))
expect(page).to have_text("Deleted feature flag ci live trace.")
end
end
...@@ -37,66 +37,153 @@ describe 'User updates feature flag', :js do ...@@ -37,66 +37,153 @@ describe 'User updates feature flag', :js do
end end
context 'when user updates a status of a scope' do context 'when user updates a status of a scope' do
before do shared_examples 'succesfully updates feature flag' do
within_scope_row(2) do before do
within_status { find('.project-feature-toggle').click } within_scope_row(2) do
within_status { find('.project-feature-toggle').click }
end
click_button 'Save changes'
expect(page).to have_current_path(project_feature_flags_path(project))
end end
click_button 'Save changes' it 'shows the updated feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect(page).to have_css('.js-feature-flag-status .badge-danger')
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end
end
end
end end
it 'shows the updated feature flag' do context 'when feature flag audit enabled' do
within_feature_flag_row(1) do include_examples 'succesfully updates feature flag'
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect(page).to have_css('.js-feature-flag-status .badge-danger')
within_feature_flag_scopes do it 'records audit event' do
expect(page.find('.badge:nth-child(1)')).to have_content('*') visit(project_audit_events_path(project))
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*') expect(page).to(
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive') have_text("Updated feature flag ci live trace. Updated rule review/* active state from true to false.")
end )
end
end
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
include_examples 'succesfully updates feature flag'
it 'does not record audit event' do
visit(project_audit_events_path(project))
expect(page).to have_no_text("Updated feature flag")
end end
end end
end end
context 'when user adds a new scope' do context 'when user adds a new scope' do
before do shared_examples 'succesfully updates feature flag' do
within_scope_row(3) do before do
within_environment_spec do within_scope_row(3) do
find('.js-env-input').set('production') within_environment_spec do
find('.js-create-button').click find('.js-env-input').set('production')
find('.js-create-button').click
end
end end
click_button 'Save changes'
expect(page).to have_current_path(project_feature_flags_path(project))
end end
click_button 'Save changes' it 'shows the newly created scope' do
within_feature_flag_row(1) do
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(3)')).to have_content('production')
expect(page.find('.badge:nth-child(3)')['class']).to include('badge-inactive')
end
end
end
end end
it 'shows the newly created scope' do context 'when feature flag audit enabled' do
within_feature_flag_row(1) do include_examples 'succesfully updates feature flag'
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(3)')).to have_content('production') it 'records audit event' do
expect(page.find('.badge:nth-child(3)')['class']).to include('badge-inactive') visit(project_audit_events_path(project))
end
expect(page).to(
have_text("Updated feature flag ci live trace")
)
end
end
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
include_examples 'succesfully updates feature flag'
it 'does not record audit event' do
visit(project_audit_events_path(project))
expect(page).to have_no_text("Updated feature flag")
end end
end end
end end
context 'when user deletes a scope' do context 'when user deletes a scope' do
before do shared_examples 'succesfully updates feature flag' do
within_scope_row(2) do before do
within_delete { find('.js-delete-scope').click } within_scope_row(2) do
within_delete { find('.js-delete-scope').click }
end
click_button 'Save changes'
expect(page).to have_current_path(project_feature_flags_path(project))
end
it 'shows the updated feature flag' do
within_feature_flag_row(1) do
within_feature_flag_scopes do
expect(page).to have_css('.badge:nth-child(1)')
expect(page).not_to have_css('.badge:nth-child(2)')
end
end
end end
end
context 'when feature flag audit enabled' do
include_examples 'succesfully updates feature flag'
click_button 'Save changes' it 'records audit event' do
visit(project_audit_events_path(project))
expect(page).to(
have_text("Updated feature flag ci live trace")
)
end
end end
it 'shows the updated feature flag' do context 'when feature flag audit is disabled' do
within_feature_flag_row(1) do before do
within_feature_flag_scopes do stub_feature_flags(feature_flag_audit: false)
expect(page).to have_css('.badge:nth-child(1)') end
expect(page).not_to have_css('.badge:nth-child(2)')
end include_examples 'succesfully updates feature flag'
it 'does not record audit event' do
visit(project_audit_events_path(project))
expect(page).to have_no_text("Updated feature flag")
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe FeatureFlags::CreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
describe '#execute' do
subject do
described_class.new(project, user, params).execute
end
let(:feature_flag) { subject[:feature_flag] }
context 'when feature flag can not be created' do
let(:params) { {} }
it 'returns status error' do
expect(subject[:status]).to eq(:error)
end
it 'returns validation errors' do
expect(subject[:message]).to include("Name can't be blank")
end
it 'does not create audit log' do
expect { subject }.not_to change { AuditEvent.count }
end
end
context 'when feature flag is saved correctly' do
let(:params) do
{
name: 'feature_flag',
description: 'description',
scopes_attributes: [{ environment_scope: '*', active: true },
{ environment_scope: 'production', active: false }]
}
end
it 'returns status success' do
expect(subject[:status]).to eq(:success)
end
it 'creates feature flag' do
expect { subject }.to change { Operations::FeatureFlag.count }.by(1)
end
it 'creates audit event' do
expected_message = "Created feature flag <strong>feature flag</strong> "\
"with description <strong>\"description\"</strong>. "\
"Created rule <strong>*</strong> and set it as <strong>active</strong>. "\
"Created rule <strong>production</strong> and set it as <strong>inactive</strong>."
expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.present.action).to eq(expected_message)
end
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
it { expect(subject[:status]).to eq(:success) }
it 'creates feature flag' do
expect { subject }.to change { Operations::FeatureFlag.count }.by(1)
end
it 'does not create audit log' do
expect { subject }.not_to change { AuditEvent.count }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe FeatureFlags::DestroyService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:feature_flag) { create(:operations_feature_flag) }
describe '#execute' do
subject { described_class.new(project, user).execute(feature_flag) }
let(:audit_event_message) { AuditEvent.last.present.action }
it 'returns status success' do
expect(subject[:status]).to eq(:success)
end
it 'destroys feature flag' do
expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
end
it 'creates audit log' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to eq("Deleted feature flag <strong>#{feature_flag.name.tr('_', ' ')}</strong>.")
end
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
it 'works successfully' do
expect(subject[:status]).to eq(:success)
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
context 'when feature flag can not be destroyed' do
before do
allow(feature_flag).to receive(:destroy).and_return(false)
end
it 'returns status error' do
expect(subject[:status]).to eq(:error)
end
it 'does not create audit log' do
expect { subject }.not_to change { AuditEvent.count }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe FeatureFlags::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:feature_flag) { create(:operations_feature_flag) }
describe '#execute' do
subject { described_class.new(project, user, params).execute(feature_flag) }
let(:params) { { name: 'new_name' } }
let(:audit_event_message) do
AuditEvent.last.present.action
end
it 'returns success status' do
expect(subject[:status]).to eq(:success)
end
it 'creates audit event with correct message' do
name_was = feature_flag.name
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
eq("Updated feature flag <strong>new name</strong>. "\
"Updated name from <strong>\"#{name_was.tr('_', ' ')}\"</strong> "\
"to <strong>\"new name\"</strong>.")
)
end
shared_examples 'disabled feature flag audit' do
context 'when feature flag audit is disabled' do
before do
stub_feature_flags(feature_flag_audit: false)
end
it 'returns success status' do
expect(subject[:status]).to eq(:success)
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
end
include_examples 'disabled feature flag audit'
context 'with invalid params' do
let(:params) { { name: nil } }
it 'returns error status' do
expect(subject[:status]).to eq(:error)
end
it 'returns error messages' do
expect(subject[:message]).to include("Name can't be blank")
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
context 'when nothing is changed' do
let(:params) { {} }
it 'returns success status' do
expect(subject[:status]).to eq(:success)
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
context 'description is being changed' do
let(:params) { { description: 'new description' } }
it 'creates audit event with changed description' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated description from <strong>\"\"</strong>"\
" to <strong>\"new description\"</strong>.")
)
end
include_examples 'disabled feature flag audit'
end
context 'when active state is changed' do
let(:params) do
{
scopes_attributes: [{ id: feature_flag.scopes.first.id, active: false }]
}
end
it 'creates audit event about changing active stae' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated rule <strong>*</strong> active state "\
"from <strong>true</strong> to <strong>false</strong>.")
)
end
include_examples 'disabled feature flag audit'
end
context 'when scope is renamed' do
let(:changed_scope) { feature_flag.scopes.create!(environment_scope: 'review', active: true) }
let(:params) do
{
scopes_attributes: [{ id: changed_scope.id, environment_scope: 'staging' }]
}
end
it 'creates audit event with changed name' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated rule <strong>staging</strong> environment scope "\
"from <strong>review</strong> to <strong>staging</strong>.")
)
end
include_examples 'disabled feature flag audit'
context 'when scope can not be updated' do
let(:params) do
{
scopes_attributes: [{ id: changed_scope.id, environment_scope: '' }]
}
end
it 'returns error status' do
expect(subject[:status]).to eq(:error)
end
it 'returns error messages' do
expect(subject[:message]).to include("Scopes environment scope can't be blank")
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
end
context 'when scope is deleted' do
let(:deleted_scope) { feature_flag.scopes.create!(environment_scope: 'review', active: true) }
let(:params) do
{
scopes_attributes: [{ id: deleted_scope.id, '_destroy': true }]
}
end
it 'creates audit event with deleted scope' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to include("Deleted rule <strong>review</strong>.")
end
include_examples 'disabled feature flag audit'
context 'when scope can not be deleted' do
RSpec::Matchers.define_negated_matcher :not_change, :change
before do
allow(deleted_scope).to receive(:destroy).and_return(false)
end
it 'does not create audit event' do
expect do
subject
end.to not_change { AuditEvent.count }.and raise_error(ActiveRecord::RecordNotDestroyed)
end
end
end
context 'when new scope is being added' do
let(:new_environment_scope) { 'review' }
let(:params) do
{
scopes_attributes: [{ environment_scope: new_environment_scope, active: true }]
}
end
it 'creates audit event with new scope' do
subject
expect(audit_event_message).to(
include("Created rule <strong>review</strong> and set it as <strong>active</strong>.")
)
end
include_examples 'disabled feature flag audit'
context 'when scope can not be created' do
let(:new_environment_scope) { '' }
it 'returns error status' do
expect(subject[:status]).to eq(:error)
end
it 'returns error messages' do
expect(subject[:message]).to include("Scopes environment scope can't be blank")
end
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment