Commit 743ab827 authored by Max Woolf's avatar Max Woolf

Merge branch '329208-project-level-value-stream' into 'master'

Expose default VSA stages on project level

See merge request gitlab-org/gitlab!60925
parents c83695a3 cfbd9169
# 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