diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index 0c603c2d5e614d1f0d08ec97eb9c28f33da39438..0011ba10a9aed2405501642915b9ceab51cea92a 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -7,6 +7,7 @@ module Stage included do validates :name, presence: true + validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :start_event_identifier, presence: true validates :end_event_identifier, presence: true validate :validate_stage_event_pairs @@ -15,6 +16,7 @@ module Stage enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier alias_attribute :custom_stage?, :custom + scope :default_stages, -> { where(custom: false) } end def parent=(_) diff --git a/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb b/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb index 9bc7d27e651abdd06abf04b5cd7d17d5c9654cd7..b5f2dafe7bd1f51ec8b3712de3f04488d6646fa4 100644 --- a/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb +++ b/ee/app/controllers/analytics/cycle_analytics/stages_controller.rb @@ -31,7 +31,7 @@ def cycle_analytics_configuration(stages) end def stage_list_service - Analytics::CycleAnalytics::StageListService.new( + Analytics::CycleAnalytics::Stages::ListService.new( parent: @group, current_user: current_user ) diff --git a/ee/app/finders/analytics/cycle_analytics/stage_finder.rb b/ee/app/finders/analytics/cycle_analytics/stage_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..5293fd3f3c6ba20f793f5861b9d16c1b66381285 --- /dev/null +++ b/ee/app/finders/analytics/cycle_analytics/stage_finder.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class StageFinder + NUMBERS_ONLY = /\A\d+\z/.freeze + + def initialize(parent:, stage_id:) + @parent = parent + @stage_id = stage_id + end + + def execute + if in_memory_default_stage? + build_in_memory_stage_by_name + else + parent.cycle_analytics_stages.find(stage_id) + end + end + + private + + attr_reader :parent, :stage_id + + def in_memory_default_stage? + !NUMBERS_ONLY.match?(stage_id.to_s) + end + + def build_in_memory_stage_by_name + parent.cycle_analytics_stages.build(find_in_memory_stage) + end + + def find_in_memory_stage + # raise ActiveRecord::RecordNotFound, so it will behave similarly to AR models and produce 404 response in the controller + raw_stage = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.find do |hash| + hash[:name].eql?(stage_id) + end + + raise(ActiveRecord::RecordNotFound, "Stage with id '#{stage_id}' could not be found") unless raw_stage + + raw_stage + end + end + end +end diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index e6bea33964f879105bcecf449da6c22c15cf9bed..fb73d671d8a5759ac0c73891ecbbd878b2e426a0 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -69,8 +69,9 @@ module GroupPolicy rule { can?(:read_group) & contribution_analytics_available } .enable :read_group_contribution_analytics - rule { reporter & cycle_analytics_available } - .enable :read_group_cycle_analytics + rule { reporter & cycle_analytics_available }.policy do + enable :read_group_cycle_analytics, :create_group_stage, :read_group_stage, :update_group_stage, :delete_group_stage + end rule { can?(:read_group) & dependency_proxy_available } .enable :read_dependency_proxy diff --git a/ee/app/services/analytics/cycle_analytics/stage_list_service.rb b/ee/app/services/analytics/cycle_analytics/stage_list_service.rb deleted file mode 100644 index 3bef8eda67d99207f24bf2b5b473a3f9bd7f0825..0000000000000000000000000000000000000000 --- a/ee/app/services/analytics/cycle_analytics/stage_list_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Analytics - module CycleAnalytics - class StageListService - include Gitlab::Allowable - - def initialize(parent:, current_user:) - @parent = parent - @current_user = current_user - end - - def execute - return forbidden unless allowed? - - success(build_default_stages) - end - - private - - attr_reader :parent, :current_user - - def build_default_stages - Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |params| - parent.cycle_analytics_stages.build(params) - end - end - - def success(stages) - ServiceResponse.success(payload: { stages: stages }) - end - - def forbidden - ServiceResponse.error(message: 'Forbidden', http_status: :forbidden) - end - - def allowed? - can?(current_user, :read_group_cycle_analytics, parent) - end - end - end -end diff --git a/ee/app/services/analytics/cycle_analytics/stages/base_service.rb b/ee/app/services/analytics/cycle_analytics/stages/base_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..625d45dbaaec3f21960e80c2ff12db532cb5340c --- /dev/null +++ b/ee/app/services/analytics/cycle_analytics/stages/base_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class BaseService + include Gitlab::Allowable + + def initialize(parent:, current_user:, params: {}) + @parent = parent + @current_user = current_user + end + + def execute + raise NotImplementedError + end + + private + + attr_reader :parent, :current_user, :params + + def success(stage, http_status = :created) + ServiceResponse.success(payload: { stage: stage }, http_status: http_status) + end + + def error(stage) + ServiceResponse.error(message: 'Invalid parameters', payload: { errors: stage.errors }, http_status: :unprocessable_entity) + end + + def not_found + ServiceResponse.error(message: 'Stage not found', payload: {}, http_status: :not_found) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden) + end + + def persist_default_stages! + persisted_default_stages = parent.cycle_analytics_stages.default_stages + + # make sure that we persist default stages only once + stages_to_persist = build_default_stages.select do |new_default_stage| + !persisted_default_stages.find { |s| s.name.eql?(new_default_stage.name) } + end + + stages_to_persist.each(&:save!) + end + + def build_default_stages + Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |params| + parent.cycle_analytics_stages.build(params) + end + end + end + end +end diff --git a/ee/app/services/analytics/cycle_analytics/stages/create_service.rb b/ee/app/services/analytics/cycle_analytics/stages/create_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..84e46b71195013c4c2c9ab1a10bed9975d298d21 --- /dev/null +++ b/ee/app/services/analytics/cycle_analytics/stages/create_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stages + class CreateService < BaseService + def initialize(parent:, current_user:, params:) + super + + @stage = parent.cycle_analytics_stages.build(params) + end + + def execute + return forbidden unless can?(current_user, :create_group_stage, parent) + return error(stage) unless stage.valid? + + parent.class.transaction do + persist_default_stages! + stage.save! + end + + success(stage) + end + + private + + attr_reader :stage + end + end + end +end diff --git a/ee/app/services/analytics/cycle_analytics/stages/list_service.rb b/ee/app/services/analytics/cycle_analytics/stages/list_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..05d3807ab6224c57f0844de60e96fbd0df786d2b --- /dev/null +++ b/ee/app/services/analytics/cycle_analytics/stages/list_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stages + class ListService < BaseService + def execute + return forbidden unless can?(current_user, :read_group_cycle_analytics, parent) + + success(persisted_stages.presence || build_default_stages) + end + + private + + def success(stages) + ServiceResponse.success(payload: { stages: stages }) + end + + def persisted_stages + parent.cycle_analytics_stages + end + end + end + end +end diff --git a/ee/app/services/analytics/cycle_analytics/stages/update_service.rb b/ee/app/services/analytics/cycle_analytics/stages/update_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..2be090553812e26788309b818f96de496a155211 --- /dev/null +++ b/ee/app/services/analytics/cycle_analytics/stages/update_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stages + class UpdateService < BaseService + def initialize(parent:, current_user:, params:) + super + + @params = params + end + + def execute + return forbidden unless can?(current_user, :update_group_stage, parent) + + parent.cycle_analytics_stages.model.transaction do + persist_default_stages! + + @stage = find_stage + @stage.assign_attributes(filtered_params) + + raise ActiveRecord::Rollback unless @stage.valid? + + @stage.save! + end + + @stage.valid? ? success(@stage, :ok) : error(@stage) + end + + private + + def filtered_params + {}.tap do |new_params| + if default_stage? + new_params[:hidden] = params[:hidden] # for default stage only hidden parameter is allowed + else + new_params.merge!(params) + end + end.compact + end + + def default_stage? + Gitlab::Analytics::CycleAnalytics::DefaultStages.names.include?(params[:id]) + end + + # rubocop: disable CodeReuse/ActiveRecord + def find_stage + if default_stage? + # default stages are already persisted + parent.cycle_analytics_stages.find_by!(name: params[:id]) + else + parent.cycle_analytics_stages.find(params[:id]) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb b/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb index b227c636f09dbb0b7362de47412777b64a511fdc..9084596fd9da791048749e6c82650a3c0a326e68 100644 --- a/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb +++ b/ee/spec/controllers/analytics/cycle_analytics/stages_controller_spec.rb @@ -93,7 +93,7 @@ end it 'renders 403 based on the response of the service object' do - expect_any_instance_of(Analytics::CycleAnalytics::StageListService).to receive(:allowed?).and_return(false) + expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false) subject diff --git a/ee/spec/factories/analytics/cycle_analytics/group_stages.rb b/ee/spec/factories/analytics/cycle_analytics/group_stages.rb new file mode 100644 index 0000000000000000000000000000000000000000..1a94cfbc2c2d384984665db7c59f49d0b082ae51 --- /dev/null +++ b/ee/spec/factories/analytics/cycle_analytics/group_stages.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cycle_analytics_group_stage, class: Analytics::CycleAnalytics::GroupStage do + sequence(:name) { |n| "Stage ##{n}" } + start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier } + end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier } + group + end +end diff --git a/ee/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb b/ee/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d97a3bc388a6fdd92a15d506073bba753743d940 --- /dev/null +++ b/ee/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::StageFinder do + let_it_be(:group) { create(:group) } + let(:stage_id) { { id: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first } } + + subject { described_class.new(parent: group, stage_id: stage_id[:id]).execute } + + context 'when looking up in-memory default stage by name exists' do + it { expect(subject).not_to be_persisted } + it { expect(subject.name).to eq(stage_id[:id]) } + end + + context 'when in-memory default stage cannot be found' do + before do + stage_id[:id] = 'unknown_default_stage' + end + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + + context 'when persisted stage exists' do + let(:stage) { create(:cycle_analytics_group_stage, group: group) } + + before do + stage_id[:id] = stage.id + end + + it { expect(subject).to be_persisted } + it { expect(subject.name).to eq(stage.name) } + end + + context 'when persisted stage cannot be found' do + before do + stage_id[:id] = -1 + end + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end +end diff --git a/ee/spec/services/analytics/cycle_analytics/stage_list_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stage_list_service_spec.rb deleted file mode 100644 index 3025282b4822226f717c6ef931e5ea3e7cde7195..0000000000000000000000000000000000000000 --- a/ee/spec/services/analytics/cycle_analytics/stage_list_service_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Analytics::CycleAnalytics::StageListService do - let(:group) { create(:group) } - let(:user) { create(:user) } - subject { described_class.new(parent: group, current_user: user) } - - context 'succeeds' do - let(:stages) { subject.execute.payload[:stages] } - - before do - stub_licensed_features(cycle_analytics_for_groups: true) - - group.add_reporter(user) - end - - it 'returns only the default stages' do - expect(stages.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) - end - - it 'provides the default stages as non-persisted objects' do - stage_ids = stages.map(&:id) - expect(stage_ids.all?(&:nil?)).to eq(true) - end - end - - it 'returns forbidden response' do - result = subject.execute - - expect(result).to be_error - expect(result.http_status).to eq(:forbidden) - end -end diff --git a/ee/spec/services/analytics/cycle_analytics/stages/create_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stages/create_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..386d4df0236826cbf988c5140396cfb686886aa6 --- /dev/null +++ b/ee/spec/services/analytics/cycle_analytics/stages/create_service_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::Stages::CreateService do + let_it_be(:group, refind: true) { create(:group) } + let_it_be(:user, refind: true) { create(:user) } + let(:params) { { name: 'my stage', start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged } } + + before_all do + group.add_user(user, :reporter) + end + + before do + stub_licensed_features(cycle_analytics_for_groups: true) + end + + subject { described_class.new(parent: group, params: params, current_user: user).execute } + + it_behaves_like 'permission check for cycle analytics stage services', :cycle_analytics_for_groups + + describe 'custom stage creation' do + context 'when service response is successful' do + let(:stage) { subject.payload[:stage] } + + it { expect(subject).to be_success } + it { expect(subject.http_status).to eq(:created) } + it { expect(stage).to be_present } + it { expect(stage).to be_persisted } + it { expect(stage.start_event_identifier).to eq(params[:start_event_identifier].to_s) } + it { expect(stage.end_event_identifier).to eq(params[:end_event_identifier].to_s) } + end + end + + context 'when params are invalid' do + before do + params.delete(:name) + end + + it { expect(subject).to be_error } + it { expect(subject.http_status).to eq(:unprocessable_entity) } + it { expect(subject.payload[:errors].keys).to eq([:name]) } + end + + describe 'persistence of default stages' do + let(:persisted_stages) { group.cycle_analytics_stages } + let(:customized_stages) { group.cycle_analytics_stages.where(custom: true) } + let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages.all } + let(:expected_stage_count) { default_stages.count + customized_stages.count } + + context 'when creating custom stages' do + it { expect(subject).to be_success } + + it 'persists all default stages' do + subject + + expect(persisted_stages.count).to eq(expected_stage_count) + end + + context 'when creating two custom stages' do + before do + described_class.new(parent: group, params: params.merge(name: 'other stage'), current_user: user).execute + end + + it 'creates two customized stages' do + subject + + expect(customized_stages.count).to eq(2) + end + + it 'creates records for the default stages only once plus two customized stage records' do + expect(group.cycle_analytics_stages.count).to eq(expected_stage_count) + end + end + end + + context 'when params are invalid' do + before do + params.delete(:name) + end + + it { expect(subject).to be_error } + + it 'skips persisting default stages on validation error' do + expect(group.cycle_analytics_stages.count).to eq(0) + end + end + end +end diff --git a/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..87f2de3a9856c2782b4af67e70b1dcd9301076f2 --- /dev/null +++ b/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::Stages::ListService do + let_it_be(:group, refind: true) { create(:group) } + let_it_be(:user) { create(:user) } + let(:stages) { subject.payload[:stages] } + + subject { described_class.new(parent: group, current_user: user).execute } + + before_all do + group.add_reporter(user) + end + + before do + stub_licensed_features(cycle_analytics_for_groups: true) + end + + it_behaves_like 'permission check for cycle analytics stage services', :cycle_analytics_for_groups + + it 'returns only the default stages' do + expect(stages.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) + end + + it 'provides the default stages as non-persisted objects' do + expect(stages.map(&:id)).to all(be_nil) + end + + context 'when there are persisted stages' do + let!(:stage1) { create(:cycle_analytics_group_stage, parent: group) } + let!(:stage2) { create(:cycle_analytics_group_stage, parent: group) } + + it 'returns the persisted stages' do + expect(stages).to contain_exactly(stage1, stage2) + end + end +end diff --git a/ee/spec/services/analytics/cycle_analytics/stages/update_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stages/update_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..764e82c3c3d1ca95a11ec5691c9b03b56c353902 --- /dev/null +++ b/ee/spec/services/analytics/cycle_analytics/stages/update_service_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::Stages::UpdateService do + let_it_be(:group, refind: true) { create(:group) } + let_it_be(:user, refind: true) { create(:user) } + let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages.all } + let(:params) { {} } + + subject { described_class.new(parent: group, params: params, current_user: user).execute } + + before_all do + group.add_user(user, :reporter) + end + + before do + stub_licensed_features(cycle_analytics_for_groups: true) + end + + it_behaves_like 'permission check for cycle analytics stage services', :cycle_analytics_for_groups + + context 'when updating a default stage' do + let(:stage) { Analytics::CycleAnalytics::GroupStage.new(default_stages.first.merge(group: group)) } + let(:params) { { id: stage.name, hidden: true } } + let(:updated_stage) { subject.payload[:stage] } + + context 'when hiding a default stage' do + it { expect(subject).to be_success } + it { expect(updated_stage).to be_persisted } + it { expect(updated_stage).to be_hidden } + end + + context 'when other parameters than "hidden" are given' do + before do + params[:name] = 'should not be updated' + end + + it { expect(subject).to be_success } + it { expect(updated_stage.name).not_to eq(params[:name]) } + end + + context 'when the first update happens on a default stage' do + let(:persisted_stages) { group.reload.cycle_analytics_stages } + + it { expect(subject).to be_success } + + it 'persists all default stages' do + subject + + expect(persisted_stages.count).to eq(default_stages.count) + expect(persisted_stages).to all(be_persisted) + end + + it 'matches with the configured default stage name' do + subject + + default_stage_names = default_stages.map { |s| s[:name] } + expect(default_stage_names).to include(updated_stage.name) + end + + context 'when the update fails' do + before do + invalid_stage = Analytics::CycleAnalytics::GroupStage.new(name: '') + expect_any_instance_of(described_class).to receive(:find_stage).and_return(invalid_stage) + end + + it 'returns unsuccessful service response' do + subject + + expect(subject).not_to be_success + end + + it 'does not persist the default stages if the stage is invalid' do + subject + + expect(persisted_stages).not_to include(be_persisted) + end + end + end + + context 'when updating an already persisted default stage' do + let(:persisted_stage) { subject.payload[:stage] } + + let(:updated_stage) do + described_class + .new(parent: group, params: { id: persisted_stage.id, hidden: false }, current_user: user) + .execute + .payload[:stage] + end + + it { expect(updated_stage).to be_persisted } + it { expect(updated_stage).not_to be_hidden } + end + end + + context 'when updating a custom stage' do + let_it_be(:stage) { create(:cycle_analytics_group_stage, group: group) } + let(:params) { { id: stage.id, name: 'my new stage name' } } + + it { expect(subject).to be_success } + it { expect(subject.http_status).to eq(:ok) } + it { expect(subject.payload[:stage].name).to eq(params[:name]) } + + context 'when params are invalid' do + before do + params[:name] = '' + end + + it { expect(subject).to be_error } + it { expect(subject.http_status).to eq(:unprocessable_entity) } + it { expect(subject.payload[:errors].keys).to eq([:name]) } + end + end +end diff --git a/ee/spec/support/shared_examples/services/analytics/cycle_analytics/stages_shared_examples.rb b/ee/spec/support/shared_examples/services/analytics/cycle_analytics/stages_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..616ca3fe8cef14c9962716ef46e3bd9008f785f3 --- /dev/null +++ b/ee/spec/support/shared_examples/services/analytics/cycle_analytics/stages_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +shared_examples 'permission check for cycle analytics stage services' do |required_license| + context 'when user has no access' do + before do + group.add_user(user, :guest) + end + + it { expect(subject).to be_error } + it { expect(subject.http_status).to eq(:forbidden) } + end + + context 'when license is missing' do + before do + stub_licensed_features(required_license => false) + end + + it { expect(subject).to be_error } + it { expect(subject.http_status).to eq(:forbidden) } + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb index 286c393005ffb74506342fdaf63333adfa352fcc..711645800fb5ffb87f42215c10ad69b8c3774f31 100644 --- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb +++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb @@ -23,6 +23,10 @@ def self.all ] end + def self.names + all.map { |stage| stage[:name] } + end + def self.params_for_issue_stage { name: 'issue', diff --git a/spec/support/shared_examples/cycle_analytics_stage_examples.rb b/spec/support/shared_examples/cycle_analytics_stage_examples.rb index 151f5325e84bf0acd414da5ef33c6fc69987d099..dc2ea229171076da4efcd7b63ae2bfe56ce0e04d 100644 --- a/spec/support/shared_examples/cycle_analytics_stage_examples.rb +++ b/spec/support/shared_examples/cycle_analytics_stage_examples.rb @@ -46,6 +46,13 @@ expect(stage).not_to be_valid expect(stage.errors.details[:end_event]).to eq([{ error: :not_allowed_for_the_given_start_event }]) end + + context 'disallows default stage names when creating custom stage' do + let(:invalid_params) { valid_params.merge(name: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first, custom: true) } + let(:stage) { described_class.new(invalid_params) } + + it { expect(stage).not_to be_valid } + end end describe '#subject_model' do