Commit cfbd9169 authored by Adam Hegyi's avatar Adam Hegyi

Expose default default VSA stages on project level

This change exposes the default VSA stages on the project level in CE.
parent 6f8c1e3b
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
respond_to :json
feature_category :planning_analytics
before_action :authorize_read_cycle_analytics!
before_action :only_default_value_stream_is_allowed!
def index
result = list_service.execute
if result.success?
render json: cycle_analytics_configuration(result.payload[:stages])
else
render json: { message: result.message }, status: result.http_status
end
end
private
def only_default_value_stream_is_allowed!
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
def value_stream
Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
end
def list_params
{ value_stream: value_stream }
end
def list_service
Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end
end
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::ApplicationController
respond_to :json
feature_category :planning_analytics
before_action :authorize_read_cycle_analytics!
def index
# FOSS users can only see the default value stream
value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)]
render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
end
end
......@@ -3,29 +3,19 @@
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
......@@ -43,3 +33,5 @@ module Analytics
end
end
end
Analytics::CycleAnalytics::StageFinder.prepend_mod_with('Analytics::CycleAnalytics::StageFinder')
......@@ -7,10 +7,13 @@ module Analytics
validates :project, presence: true
belongs_to :project
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
alias_attribute :value_stream_id, :project_value_stream_id
delegate :group, to: :project
validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
......
# frozen_string_literal: true
class Analytics::CycleAnalytics::ProjectValueStream < ApplicationRecord
belongs_to :project
has_many :stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
validates :project, :name, presence: true
validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :project_id }
def custom?
false
end
def stages
[]
end
def self.build_default_value_stream(project)
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, project: project)
end
end
......@@ -27,6 +27,7 @@ module Analytics
scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
end
def parent=(_)
......
......@@ -335,7 +335,8 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :project
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', inverse_of: :project
has_many :external_pull_requests, inverse_of: :project
......
......@@ -2,7 +2,7 @@
module Analytics
module CycleAnalytics
class GroupValueStreamEntity < Grape::Entity
class ValueStreamEntity < Grape::Entity
expose :name
expose :id
expose :is_custom do |object|
......
......@@ -2,8 +2,8 @@
module Analytics
module CycleAnalytics
class GroupValueStreamSerializer < BaseSerializer
entity ::Analytics::CycleAnalytics::GroupValueStreamEntity
class ValueStreamSerializer < BaseSerializer
entity ::Analytics::CycleAnalytics::ValueStreamEntity
end
end
end
......@@ -26,29 +26,10 @@ module Analytics
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.by_value_stream(value_stream).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 |stage_params|
parent.cycle_analytics_stages.build(stage_params.merge(value_stream: value_stream))
......@@ -56,9 +37,11 @@ module Analytics
end
def value_stream
@value_stream ||= params[:value_stream] || parent.value_streams.safe_find_or_create_by!(name: DEFAULT_VALUE_STREAM_NAME)
@value_stream ||= params[:value_stream]
end
end
end
end
end
Analytics::CycleAnalytics::Stages::BaseService.prepend_mod_with('Analytics::CycleAnalytics::Stages::BaseService')
......@@ -3,32 +3,25 @@
module Analytics
module CycleAnalytics
module Stages
class ListService < BaseService
extend ::Gitlab::Utils::Override
class ListService < Analytics::CycleAnalytics::Stages::BaseService
def execute
return forbidden unless can?(current_user, :read_group_cycle_analytics, parent)
return forbidden unless allowed?
success(persisted_stages.presence || build_default_stages)
success(build_default_stages)
end
private
def success(stages)
ServiceResponse.success(payload: { stages: stages })
end
def persisted_stages
scope = parent.cycle_analytics_stages
scope = scope.by_value_stream(params[:value_stream]) if params[:value_stream]
scope.for_list
def allowed?
can?(current_user, :read_cycle_analytics, parent)
end
override :value_stream
def value_stream
@value_stream ||= (params[:value_stream] || parent.value_streams.new(name: DEFAULT_VALUE_STREAM_NAME))
def success(stages)
ServiceResponse.success(payload: { stages: stages })
end
end
end
end
end
Analytics::CycleAnalytics::Stages::ListService.prepend_mod_with('Analytics::CycleAnalytics::Stages::ListService')
---
title: Create database structure to support project value streams
merge_request: 60925
author:
type: other
......@@ -267,6 +267,15 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
get '/cycle_analytics', to: redirect('%{namespace_id}/%{project_id}/-/value_stream_analytics')
namespace :analytics do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
resources :value_streams, only: [:index] do
resources :stages, only: [:index]
end
end
end
concerns :clusterable
namespace :serverless do
......
# frozen_string_literal: true
class CreateProjectValueStreams < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_analytics_ca_project_value_streams_on_project_id_and_name'
def up
create_table_with_constraints :analytics_cycle_analytics_project_value_streams do |t|
t.timestamps_with_timezone
t.references(:project,
null: false,
index: false,
foreign_key: { to_table: :projects, on_delete: :cascade }
)
t.text :name, null: false
t.index [:project_id, :name], unique: true, name: INDEX_NAME
t.text_limit :name, 100
end
end
def down
with_lock_retries do
drop_table :analytics_cycle_analytics_project_value_streams
end
end
end
# frozen_string_literal: true
class AddProjectValueStreamIdToProjectStages < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
INDEX_NAME = 'index_analytics_ca_project_stages_on_value_stream_id'
class ProjectValueStream < ActiveRecord::Base
self.table_name = 'analytics_cycle_analytics_project_stages'
include EachBatch
end
def up
ProjectValueStream.reset_column_information
# The table was never used, there is no user-facing code that modifies the table, it should be empty.
# Since there is no functionality present that depends on this data, it's safe to delete the rows.
ProjectValueStream.each_batch(of: 100) do |relation|
relation.delete_all
end
transaction do
add_reference :analytics_cycle_analytics_project_stages, :project_value_stream, null: false, index: { name: INDEX_NAME }, foreign_key: { on_delete: :cascade, to_table: :analytics_cycle_analytics_project_value_streams }, type: :bigint # rubocop: disable Migration/AddReference, Rails/NotNullColumn
end
end
def down
remove_reference :analytics_cycle_analytics_project_stages, :project_value_stream
end
end
de8bf6c02589bf308914d43e5cd44dae91d3bbabcdaafcebdb96fba0a09b20bc
\ No newline at end of file
2fdcb66e511d8322ea8fc4de66ecce859f8e91b2a9da22336281a1e784d9b4a5
\ No newline at end of file
......@@ -9051,7 +9051,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
end_event_label_id bigint,
hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL
name character varying(255) NOT NULL,
project_value_stream_id bigint NOT NULL
);
CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
......@@ -9063,6 +9064,24 @@ CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
ALTER SEQUENCE analytics_cycle_analytics_project_stages_id_seq OWNED BY analytics_cycle_analytics_project_stages.id;
CREATE TABLE analytics_cycle_analytics_project_value_streams (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
name text NOT NULL,
CONSTRAINT check_9b1970a898 CHECK ((char_length(name) <= 100))
);
CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id;
CREATE TABLE analytics_devops_adoption_segment_selections (
id bigint NOT NULL,
segment_id bigint NOT NULL,
......@@ -19369,6 +19388,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER COLUMN id S
ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_stages_id_seq'::regclass);
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
......@@ -20449,6 +20470,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id);
......@@ -22262,6 +22286,10 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON analytics
CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON analytics_cycle_analytics_project_stages USING btree (start_event_label_id);
CREATE INDEX index_analytics_ca_project_stages_on_value_stream_id ON analytics_cycle_analytics_project_stages USING btree (project_value_stream_id);
CREATE UNIQUE INDEX index_analytics_ca_project_value_streams_on_project_id_and_name ON analytics_cycle_analytics_project_value_streams USING btree (project_id, name);
CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true);
CREATE UNIQUE INDEX index_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
......@@ -26519,6 +26547,9 @@ ALTER TABLE ONLY namespace_admin_notes
ALTER TABLE ONLY web_hook_logs_archived
ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT fk_rails_669f4ba293 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY jira_imports
ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL;
......@@ -26621,6 +26652,9 @@ ALTER TABLE ONLY ci_subscriptions_projects
ALTER TABLE ONLY terraform_states
ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT fk_rails_796a7dbc9c FOREIGN KEY (project_value_stream_id) REFERENCES analytics_cycle_analytics_project_value_streams(id) ON DELETE CASCADE;
ALTER TABLE ONLY software_license_policies
ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES software_licenses(id) ON DELETE CASCADE;
......@@ -9,7 +9,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt
end
def index
render json: Analytics::CycleAnalytics::GroupValueStreamSerializer.new.represent(value_streams)
render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
end
def create
......@@ -75,7 +75,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt
end
def serialize_value_stream(result)
Analytics::CycleAnalytics::GroupValueStreamSerializer.new.represent(result.payload[:value_stream])
Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(result.payload[:value_stream])
end
def serialize_value_stream_error(result)
......
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module StageFinder
extend ::Gitlab::Utils::Override
NUMBERS_ONLY = /\A\d+\z/.freeze
def initialize(parent:, stage_id:)
@parent = parent
@stage_id = stage_id
end
override :execute
def execute
return super if in_memory_default_stage?
parent.cycle_analytics_stages.find(stage_id)
end
private
attr_reader :parent, :stage_id
def in_memory_default_stage?
!NUMBERS_ONLY.match?(stage_id.to_s)
end
end
end
end
end
......@@ -13,7 +13,7 @@ module Analytics
alias_attribute :parent, :group
alias_attribute :parent_id, :group_id
scope :by_value_stream, -> (value_stream) { where(group_value_stream_id: value_stream.id) }
alias_attribute :value_stream_id, :group_value_stream_id
def self.relative_positioning_query_base(stage)
where(group_id: stage.group_id)
......
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module Stages
module BaseService
extend ::Gitlab::Utils::Override
private
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', http_status: :not_found)
end
def persist_default_stages!
persisted_default_stages = parent.cycle_analytics_stages.by_value_stream(value_stream).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
override :value_stream
def value_stream
@value_stream ||= params[:value_stream] || parent.value_streams.safe_find_or_create_by!(name: ::Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME)
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module Stages
module ListService
extend ::Gitlab::Utils::Override
override :value_stream
def execute
return forbidden unless allowed?
success(persisted_stages.presence || build_default_stages)
end
private
override :allowed?
def allowed?
return super unless parent.is_a?(Group)
can?(current_user, :read_group_cycle_analytics, parent)
end
def persisted_stages
scope = parent.cycle_analytics_stages
scope = scope.by_value_stream(params[:value_stream]) if params[:value_stream]
scope.for_list
end
override :value_stream
def value_stream
@value_stream ||= (params[:value_stream] || parent.value_streams.new(name: ::Analytics::CycleAnalytics::Stages::ListService::DEFAULT_VALUE_STREAM_NAME))
end
end
end
end
end
end
......@@ -15,6 +15,7 @@ RSpec.describe Analytics::CycleAnalytics::GroupStage do
end
it_behaves_like 'value stream analytics stage' do
let(:factory) { :cycle_analytics_group_stage }
let(:parent) { create(:group) }
let(:parent_name) { :group }
end
......
......@@ -3,15 +3,8 @@
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEntity do
let(:stage) { build(:cycle_analytics_group_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json }
it 'exposes start and end event descriptions' do
expect(entity_json).to have_key(:start_event_html_description)
expect(entity_json).to have_key(:end_event_html_description)
end
context 'when label based event is given' do
let(:label) { create(:group_label, title: 'test label') }
let(:stage) { build(:cycle_analytics_group_stage, group: label.group, start_event_label: label, start_event_identifier: :merge_request_label_added, end_event_identifier: :merge_request_merged) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } }
before do
sign_in(user)
end
describe 'GET index' do
context 'when user is member of the project' do
before do
project.add_developer(user)
end
it 'succeeds' do
get :index, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'exposes the default stages' do
get :index, params: params
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end
context 'when list service fails' do
it 'renders 403' do
expect_next_instance_of(Analytics::CycleAnalytics::Stages::ListService) do |list_service|
expect(list_service).to receive(:allowed?).and_return(false)
end
get :index, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when invalid value stream id is given' do
before do
params[:value_stream_id] = 1
end
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is not member of the project' do
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Analytics::CycleAnalytics::ValueStreamsController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:params) { { namespace_id: group, project_id: project } }
before do
sign_in(user)
end
describe 'GET index' do
context 'when user is member of the project' do
before do
project.add_developer(user)
end
it 'succeeds' do
get :index, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'exposes the default value stream' do
get :index, params: params
expect(json_response.first['name']).to eq('default')
end
end
context 'when user is not member of the project' do
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......@@ -6,6 +6,7 @@ FactoryBot.define do
sequence(:name) { |n| "Stage ##{n}" }
hidden { false }
issue_stage
value_stream { association(:cycle_analytics_project_value_stream, project: project) }
trait :issue_stage do
start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier }
......
# frozen_string_literal: true
FactoryBot.define do
factory :cycle_analytics_project_value_stream, class: 'Analytics::CycleAnalytics::ProjectValueStream' do
sequence(:name) { |n| "Value Stream ##{n}" }
project
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageFinder do
let(:project) { build(:project) }
let(:stage_id) { { id: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first } }
subject { described_class.new(parent: project, 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
end
......@@ -352,6 +352,7 @@ project:
- cluster_project
- creator
- cycle_analytics_stages
- value_streams
- group
- namespace
- management_clusters
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20210503105845_add_project_value_stream_id_to_project_stages.rb')
RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:namespace) { table(:namespaces).create!(name: 'ns1', path: 'nsq1') }
before do
project = projects.create!(name: 'p1', namespace_id: namespace.id)
stages.create!(
project_id: project.id,
created_at: Time.now,
updated_at: Time.now,
start_event_identifier: 1,
end_event_identifier: 2,
name: 'stage 1'
)
stages.create!(
project_id: project.id,
created_at: Time.now,
updated_at: Time.now,
start_event_identifier: 3,
end_event_identifier: 4,
name: 'stage 2'
)
end
it 'deletes the existing rows' do
migrate!
expect(stages.count).to eq(0)
end
end
......@@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
end
it_behaves_like 'value stream analytics stage' do
let(:factory) { :cycle_analytics_project_stage }
let(:parent) { build(:project) }
let(:parent_name) { :project }
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:stages) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(100) }
it 'validates uniqueness of name' do
project = create(:project)
create(:cycle_analytics_project_value_stream, name: 'test', project: project)
value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project)
expect(value_stream).to be_invalid
expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
end
end
it 'is not custom' do
expect(described_class.new).not_to be_custom
end
describe '.build_default_value_stream' do
it 'builds the default value stream' do
project = build(:project)
value_stream = described_class.build_default_value_stream(project)
expect(value_stream.name).to eq('default')
end
end
end
......@@ -113,7 +113,8 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) }
it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) }
it { is_expected.to have_many(:value_streams).inverse_of(:project) }
it { is_expected.to have_many(:external_pull_requests) }
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEntity do
let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json }
it 'exposes start and end event descriptions' do
expect(entity_json).to have_key(:start_event_html_description)
expect(entity_json).to have_key(:end_event_html_description)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) }
let(:stages) { subject.payload[:stages] }
subject { described_class.new(parent: project, current_user: user).execute }
before_all do
project.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
expect(stages.map(&:id)).to all(be_nil)
end
end
......@@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do
it { expect(stage).not_to be_valid }
end
# rubocop: disable Rails/SaveBang
describe '.by_value_stream' do
it 'finds stages by value stream' do
stage1 = create(factory)
create(factory) # other stage with different value stream
result = described_class.by_value_stream(stage1.value_stream)
expect(result).to eq([stage1])
end
end
# rubocop: enable Rails/SaveBang
end
describe '#subject_class' do
......
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