Commit 76c7788b authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '212226-move-vsm-controller-to-group-level' into 'master'

Make VSM controllers available at the Group level

Closes #212226

See merge request gitlab-org/gitlab!28641
parents 33ed8a64 d16700bd
......@@ -89,7 +89,6 @@ export const fetchStageMedianValues = ({ state, dispatch, getters }) => {
const { stages } = state;
const params = {
group_id: currentGroupPath,
created_after,
created_before,
project_ids,
......@@ -191,8 +190,7 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
selectedGroup: { fullPath },
} = state;
return Api.cycleAnalyticsSummaryData({
group_id: fullPath,
return Api.cycleAnalyticsSummaryData(fullPath, {
created_after,
created_before,
project_ids,
......@@ -230,11 +228,10 @@ export const fetchTopRankedGroupLabels = ({
dispatch('requestTopRankedGroupLabels');
const { subject } = state.tasksByType;
return Api.cycleAnalyticsTopLabels({
return Api.cycleAnalyticsTopLabels(currentGroupPath, {
subject,
created_after,
created_before,
group_id: currentGroupPath,
})
.then(({ data }) => dispatch('receiveTopRankedGroupLabelsSuccess', data))
.catch(error =>
......@@ -359,7 +356,6 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
// dont request if we have no labels selected...for now
if (selectedLabelIds.length) {
const params = {
group_id: currentGroupPath,
created_after,
created_before,
project_ids,
......@@ -369,7 +365,7 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
dispatch('requestTasksByTypeData');
return Api.cycleAnalyticsTasksByType(params)
return Api.cycleAnalyticsTasksByType(currentGroupPath, params)
.then(({ data }) => dispatch('receiveTasksByTypeDataSuccess', data))
.catch(error => dispatch('receiveTasksByTypeDataError', error));
}
......@@ -472,8 +468,7 @@ export const fetchDurationData = ({ state, dispatch, getters }) => {
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(slug, {
group_id: fullPath,
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after,
created_before,
project_ids,
......@@ -521,8 +516,7 @@ export const fetchDurationMedianData = ({ state, dispatch, getters }) => {
stages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(slug, {
group_id: fullPath,
return Api.cycleAnalyticsDurationChart(fullPath, slug, {
created_after: dateFormat(offsetCreatedAfter, dateFormats.isoDate),
created_before: dateFormat(offsetCreatedBefore, dateFormats.isoDate),
project_ids,
......
......@@ -13,15 +13,17 @@ export default {
groupPackagesPath: '/api/:version/groups/:id/packages',
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
cycleAnalyticsTasksByTypePath: '/-/analytics/type_of_work/tasks_by_type',
cycleAnalyticsTopLabelsPath: '/-/analytics/type_of_work/tasks_by_type/top_labels',
cycleAnalyticsSummaryDataPath: '/-/analytics/value_stream_analytics/summary',
cycleAnalyticsGroupStagesAndEventsPath: '/-/analytics/value_stream_analytics/stages',
cycleAnalyticsStageEventsPath: '/-/analytics/value_stream_analytics/stages/:stage_id/records',
cycleAnalyticsStageMedianPath: '/-/analytics/value_stream_analytics/stages/:stage_id/median',
cycleAnalyticsStagePath: '/-/analytics/value_stream_analytics/stages/:stage_id',
cycleAnalyticsTasksByTypePath: '/groups/:id/-/analytics/type_of_work/tasks_by_type',
cycleAnalyticsTopLabelsPath: '/groups/:id/-/analytics/type_of_work/tasks_by_type/top_labels',
cycleAnalyticsSummaryDataPath: '/groups/:id/-/analytics/value_stream_analytics/summary',
cycleAnalyticsGroupStagesAndEventsPath: '/groups/:id/-/analytics/value_stream_analytics/stages',
cycleAnalyticsStageEventsPath:
'/groups/:id/-/analytics/value_stream_analytics/stages/:stage_id/records',
cycleAnalyticsStageMedianPath:
'/groups/:id/-/analytics/value_stream_analytics/stages/:stage_id/median',
cycleAnalyticsStagePath: '/groups/:id/-/analytics/value_stream_analytics/stages/:stage_id',
cycleAnalyticsDurationChartPath:
'/-/analytics/value_stream_analytics/stages/:stage_id/duration_chart',
'/groups/:id/-/analytics/value_stream_analytics/stages/:stage_id/duration_chart',
cycleAnalyticsGroupLabelsPath: '/groups/:namespace_path/-/labels.json',
codeReviewAnalyticsPath: '/api/:version/analytics/code_review',
groupActivityIssuesPath: '/api/:version/analytics/group_activity/issues_count',
......@@ -131,69 +133,74 @@ export default {
return axios.delete(url);
},
cycleAnalyticsTasksByType(params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsTasksByTypePath);
cycleAnalyticsTasksByType(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsTasksByTypePath).replace(':id', groupId);
return axios.get(url, { params });
},
cycleAnalyticsTopLabels(params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsTopLabelsPath);
cycleAnalyticsTopLabels(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsTopLabelsPath).replace(':id', groupId);
return axios.get(url, { params });
},
cycleAnalyticsSummaryData(params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsSummaryDataPath);
cycleAnalyticsSummaryData(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsSummaryDataPath).replace(':id', groupId);
return axios.get(url, { params });
},
cycleAnalyticsGroupStagesAndEvents(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath);
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath).replace(':id', groupId);
return axios.get(url, {
params: { group_id: groupId, ...params },
});
return axios.get(url, { params });
},
cycleAnalyticsStageEvents(groupId, stageId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsStageEventsPath).replace(':stage_id', stageId);
return axios.get(url, { params: { ...params, group_id: groupId } });
const url = Api.buildUrl(this.cycleAnalyticsStageEventsPath)
.replace(':id', groupId)
.replace(':stage_id', stageId);
return axios.get(url, { params });
},
cycleAnalyticsStageMedian(groupId, stageId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsStageMedianPath).replace(':stage_id', stageId);
return axios.get(url, { params: { ...params, group_id: groupId } });
const url = Api.buildUrl(this.cycleAnalyticsStageMedianPath)
.replace(':id', groupId)
.replace(':stage_id', stageId);
return axios.get(url, { params: { ...params } });
},
cycleAnalyticsCreateStage(groupId, data) {
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath);
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath).replace(':id', groupId);
return axios.post(url, data, {
params: { group_id: groupId },
});
return axios.post(url, data);
},
cycleAnalyticsStageUrl(stageId) {
return Api.buildUrl(this.cycleAnalyticsStagePath).replace(':stage_id', stageId);
cycleAnalyticsStageUrl(stageId, groupId) {
return Api.buildUrl(this.cycleAnalyticsStagePath)
.replace(':id', groupId)
.replace(':stage_id', stageId);
},
cycleAnalyticsUpdateStage(stageId, groupId, data) {
const url = this.cycleAnalyticsStageUrl(stageId);
const url = this.cycleAnalyticsStageUrl(stageId, groupId);
return axios.put(url, data, {
params: { group_id: groupId },
});
return axios.put(url, data);
},
cycleAnalyticsRemoveStage(stageId, groupId) {
const url = this.cycleAnalyticsStageUrl(stageId);
const url = this.cycleAnalyticsStageUrl(stageId, groupId);
return axios.delete(url, {
params: { group_id: groupId },
});
return axios.delete(url);
},
cycleAnalyticsDurationChart(stageSlug, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsDurationChartPath).replace(':stage_id', stageSlug);
cycleAnalyticsDurationChart(groupId, stageSlug, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsDurationChartPath)
.replace(':id', groupId)
.replace(':stage_id', stageSlug);
return axios.get(url, {
params,
......
......@@ -53,7 +53,7 @@ module Analytics
def duration_chart
return render_403 unless can?(current_user, :read_group_stage, @group)
render json: Analytics::CycleAnalytics::DurationChartItemEntity.represent(data_collector.duration_chart_data)
render json: ::Analytics::CycleAnalytics::DurationChartItemEntity.represent(data_collector.duration_chart_data)
end
private
......@@ -81,35 +81,35 @@ module Analytics
end
def stage
@stage ||= Analytics::CycleAnalytics::StageFinder.new(parent: @group, stage_id: params[:id]).execute
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: @group, stage_id: params[:id]).execute
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
::Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end
def list_service
Stages::ListService.new(parent: @group, current_user: current_user)
::Analytics::CycleAnalytics::Stages::ListService.new(parent: @group, current_user: current_user)
end
def create_service
Stages::CreateService.new(parent: @group, current_user: current_user, params: create_params)
::Analytics::CycleAnalytics::Stages::CreateService.new(parent: @group, current_user: current_user, params: create_params)
end
def update_service
Stages::UpdateService.new(parent: @group, current_user: current_user, params: update_params)
::Analytics::CycleAnalytics::Stages::UpdateService.new(parent: @group, current_user: current_user, params: update_params)
end
def delete_service
Stages::DeleteService.new(parent: @group, current_user: current_user, params: delete_params)
::Analytics::CycleAnalytics::Stages::DeleteService.new(parent: @group, current_user: current_user, params: delete_params)
end
def render_stage_service_result(result)
if result.success?
stage = StagePresenter.new(result.payload[:stage])
render json: Analytics::CycleAnalytics::StageEntity.new(stage), status: result.http_status
render json: ::Analytics::CycleAnalytics::StageEntity.new(stage), status: result.http_status
else
render json: { message: result.message, errors: result.payload[:errors] }, status: result.http_status
end
......
# frozen_string_literal: true
module Groups
module Analytics
module CycleAnalytics
class StagesController < ::Analytics::CycleAnalytics::StagesController
end
end
end
end
# frozen_string_literal: true
module Groups
module Analytics
module CycleAnalytics
class SummaryController < ::Analytics::CycleAnalytics::SummaryController
end
end
end
end
# frozen_string_literal: true
class Groups::Analytics::TasksByTypeController < ::Analytics::TasksByTypeController
end
# frozen_string_literal: true
class Groups::CycleAnalytics::EventsController < Groups::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
before_action :authorize_group_cycle_analytics!
def issue
render_events(:issue)
end
def plan
render_events(:plan)
end
def code
render_events(:code)
end
def test
render_events(:test)
end
def review
render_events(:review)
end
def staging
render_events(:staging)
end
def production
render_events(:production)
end
private
def render_events(stage)
respond_to do |format|
format.html
format.json { render json: { events: cycle_analytics[stage].events } }
end
end
def cycle_analytics
@cycle_analytics ||= ::CycleAnalytics::GroupLevel.new(group: group, options: options(cycle_analytics_group_params))
end
def authorize_group_cycle_analytics!
unless can?(current_user, :read_group_cycle_analytics, group)
render_403
end
end
end
# frozen_string_literal: true
class Groups::CycleAnalyticsController < Groups::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_group_cycle_analytics!
def show
respond_to do |format|
format.json { render json: cycle_analytics_json }
end
end
private
def cycle_analytics_json
{
summary: cycle_analytics_stats.summary,
stats: cycle_analytics_stats.stats,
permissions: cycle_analytics_stats.permissions(user: current_user)
}
end
def cycle_analytics_stats
@cycle_analytics_stats ||= ::CycleAnalytics::GroupLevel.new(group: group, options: options(cycle_analytics_group_params))
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42671')
end
def authorize_group_cycle_analytics!
unless can?(current_user, :read_group_cycle_analytics, group)
render_403
end
end
end
......@@ -19,21 +19,32 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
get '/analytics', to: redirect('groups/%{group_id}/-/contribution_analytics')
resource :contribution_analytics, only: [:show]
resource :cycle_analytics, only: [:show], path: 'value_stream_analytics'
namespace :analytics do
resource :productivity_analytics, only: :show, constraints: -> (req) { Gitlab::Analytics.productivity_analytics_enabled? }
constraints(::Constraints::FeatureConstrainer.new(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG, default_enabled: Gitlab::Analytics.feature_enabled_by_default?(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG))) do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
scope :events, controller: 'events' do
get :issue
get :plan
get :code
get :test
get :review
get :staging
get :production
resources :stages, only: [:index, :create, :update, :destroy] do
member do
get :duration_chart
get :median
get :records
end
end
resource :summary, controller: :summary, only: :show
end
get '/cycle_analytics', to: redirect('-/analytics/value_stream_analytics')
end
constraints(::Constraints::FeatureConstrainer.new(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG)) do
scope :type_of_work do
resource :tasks_by_type, controller: :tasks_by_type, only: :show do
get :top_labels
end
end
end
namespace :analytics do
resource :productivity_analytics, only: :show, constraints: -> (req) { Gitlab::Analytics.productivity_analytics_enabled? }
resource :cycle_analytics, path: 'value_stream_analytics', only: :show, constraints: -> (req) { Gitlab::Analytics.cycle_analytics_enabled? }
end
resource :ldap, only: [] do
......
......@@ -95,7 +95,7 @@ describe Analytics::TasksByTypeController do
it 'succeeds' do
subject
expect(response).to be_successful
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/tasks_by_type', dir: 'ee')
end
......@@ -128,7 +128,7 @@ describe Analytics::TasksByTypeController do
it 'succeeds' do
subject
expect(response).to be_successful
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/tasks_by_type_top_labels', dir: 'ee')
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::Analytics::CycleAnalytics::StagesController do
let_it_be(:user) { create(:user) }
let_it_be(:group, refind: true) { create(:group) }
let(:params) { { group_id: group } }
before do
stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true)
stub_licensed_features(cycle_analytics_for_groups: true)
group.add_reporter(user)
sign_in(user)
end
describe 'GET #index' do
subject { get :index, params: params }
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/cycle_analytics/stages', dir: 'ee')
end
it 'returns correct start events' do
subject
response_start_events = json_response['stages'].map { |s| s['start_event_identifier'] }
start_events = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map { |s| s['start_event_identifier'] }
expect(response_start_events).to eq(start_events)
end
it 'returns correct event names' do
subject
response_event_names = json_response['events'].map { |s| s['name'] }
event_names = Gitlab::Analytics::CycleAnalytics::StageEvents.events.map(&:name).sort
expect(response_event_names).to eq(event_names)
end
it 'succeeds for subgroups' do
subgroup = create(:group, parent: group)
params[:group_id] = subgroup.full_path
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'renders `forbidden` based on the response of the service object' do
expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false)
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
include_examples 'group permission check on the controller level'
end
describe 'POST #create' do
subject { post :create, params: params }
include_examples 'group permission check on the controller level'
context 'when valid parameters are given' do
before do
params.merge!({
name: 'my new stage',
start_event_identifier: :merge_request_created,
end_event_identifier: :merge_request_merged
})
end
it 'creates the stage' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('analytics/cycle_analytics/stage', dir: 'ee')
end
end
include_context 'when invalid stage parameters are given'
end
describe 'PUT #update' do
let(:stage) { create(:cycle_analytics_group_stage, parent: group, relative_position: 15) }
subject { put :update, params: params.merge(id: stage.id) }
include_examples 'group permission check on the controller level'
context 'when valid parameters are given' do
before do
params.merge!({
name: 'my updated stage',
start_event_identifier: :merge_request_created,
end_event_identifier: :merge_request_merged
})
end
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/cycle_analytics/stage', dir: 'ee')
end
it 'updates the name attribute' do
subject
stage.reload
expect(stage.name).to eq(params[:name])
end
context 'hidden attribute' do
before do
params[:hidden] = true
end
it 'updates the hidden attribute' do
subject
stage.reload
expect(stage.hidden).to eq(true)
end
end
context 'when positioning parameter is given' do
before do
params[:move_before_id] = create(:cycle_analytics_group_stage, parent: group, relative_position: 10).id
end
it 'moves the stage before the last place' do
subject
before_last = group.cycle_analytics_stages.ordered[-2]
expect(before_last.id).to eq(stage.id)
end
end
end
include_context 'when invalid stage parameters are given'
end
describe 'DELETE #destroy' do
let(:stage) { create(:cycle_analytics_group_stage, parent: group) }
subject { delete :destroy, params: params }
before do
params[:id] = stage.id
end
include_examples 'group permission check on the controller level'
context 'when persisted stage id is passed' do
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'deletes the record' do
subject
expect(group.reload.cycle_analytics_stages.find_by(id: stage.id)).to be_nil
end
end
context 'when default stage id is passed' do
before do
params[:id] = Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first
end
it 'fails with `forbidden` response' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'data endpoints' do
let(:stage) { create(:cycle_analytics_group_stage, parent: group) }
before do
params[:id] = stage.id
end
describe 'GET #median' do
subject { get :median, params: params }
it 'matches the response schema' do
subject
expect(response).to match_response_schema('analytics/cycle_analytics/median', dir: 'ee')
end
include_examples 'cycle analytics data endpoint examples'
end
describe 'GET #records' do
subject { get :records, params: params }
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
describe 'GET #duration_chart' do
subject { get :duration_chart, params: params }
it 'matches the response schema' do
fake_result = [double(MergeRequest, duration_in_seconds: 10, finished_at: Time.now)]
expect_any_instance_of(Gitlab::Analytics::CycleAnalytics::DataForDurationChart).to receive(:load).and_return(fake_result)
subject
expect(response).to match_response_schema('analytics/cycle_analytics/duration_chart', dir: 'ee')
end
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Analytics::CycleAnalyticsController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'usage counter' do
it 'increments usage counter' do
expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views)
get(:show)
expect(response).to be_successful
end
end
describe 'GET show' do
it 'renders `show` template' do
stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true)
get :show
expect(response).to render_template :show
end
it 'renders `404` when feature flag is disabled' do
stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => false)
get :show
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Analytics::TasksByTypeController do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
let(:label) { create(:group_label, group: group) }
let!(:issue) { create(:labeled_issue, created_at: 5.days.ago, project: create(:project, group: group), labels: [label]) }
before do
stub_licensed_features(type_of_work_analytics: true)
stub_feature_flags(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG => true)
group.add_reporter(user)
sign_in(user)
end
shared_examples 'expects unprocessable_entity response' do
it 'returns unprocessable_entity as response' do
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
shared_examples 'parameter validation' do
context 'when user access level is lower than reporter' do
before do
group.add_guest(user)
end
it 'returns forbidden as response' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when license is missing' do
before do
stub_licensed_features(type_of_work_analytics: false)
end
it 'returns forbidden as response' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG => false)
end
it 'returns not_found as response' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when `created_after` parameter is invalid' do
before do
params[:created_after] = 'invalid_date'
end
it_behaves_like 'expects unprocessable_entity response'
end
context 'when `created_after` parameter is missing' do
before do
params.delete(:created_after)
end
it_behaves_like 'expects unprocessable_entity response'
end
context 'when `created_after` date is later than `created_before` date' do
before do
params[:created_after] = 1.year.ago.to_date
params[:created_before] = 2.years.ago.to_date
end
it_behaves_like 'expects unprocessable_entity response'
end
end
describe 'GET #show' do
let(:params) { { group_id: group, label_ids: [label.id], created_after: 10.days.ago, subject: 'Issue' } }
subject { get :show, params: params }
context 'when valid parameters are given' do
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/tasks_by_type', dir: 'ee')
end
it 'returns valid count' do
subject
date, count = json_response.first['series'].first
expect(Date.parse(date)).to eq(issue.created_at.to_date)
expect(count).to eq(1)
end
end
context 'when `label_id` is missing' do
before do
params.delete(:label_ids)
end
it_behaves_like 'expects unprocessable_entity response'
end
it_behaves_like 'parameter validation'
end
describe 'GET #top_labels' do
let(:params) { { group_id: group.full_path, created_after: 10.days.ago, subject: 'Issue' } }
subject { get :top_labels, params: params }
context 'when valid parameters are given' do
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/tasks_by_type_top_labels', dir: 'ee')
end
it 'returns valid count' do
subject
label_item = json_response.first
expect(label_item['title']).to eq(label.title)
expect(json_response.count).to eq(1)
end
end
it_behaves_like 'parameter validation'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec::Matchers.define :nested_hash_including do |path_to_hash, value|
match { |actual| actual.dig(*path_to_hash) == value }
end
describe Groups::CycleAnalytics::EventsController do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) }
let(:group_service) { instance_double(::CycleAnalytics::GroupLevel) }
let(:events_service) { double }
before do
stub_licensed_features(cycle_analytics_for_groups: true)
sign_in(user)
end
describe 'cycle analytics' do
context 'with proper permission' do
before do
group.add_owner(user)
end
it 'calls service' do
expect(events_service).to receive(:events)
expect(group_service).to receive(:[]).and_return(events_service)
expect(::CycleAnalytics::GroupLevel).to receive(:new).and_return(group_service)
get(:issue,
params: {
group_id: group.name
},
format: :json)
expect(response).to be_successful
end
it 'calls service with specific params' do
expect(events_service).to receive(:events)
expect(group_service).to receive(:[]).and_return(events_service)
expect(::CycleAnalytics::GroupLevel).to receive(:new)
.with(nested_hash_including([:options, :projects], [project.id.to_s]))
.and_return(group_service)
get(:issue,
params: {
group_id: group.name,
project_ids: [project.id]
},
format: :json)
expect(response).to be_successful
end
end
context 'as guest' do
before do
group.add_guest(user)
end
it 'returns 403' do
get(:issue,
params: {
group_id: group.name
},
format: :json)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec::Matchers.define :nested_hash_including do |path_to_hash, value|
match { |actual| actual.dig(*path_to_hash) == value }
end
describe Groups::CycleAnalyticsController do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) }
let(:group_service) { instance_double(::CycleAnalytics::GroupLevel) }
before do
stub_licensed_features(cycle_analytics_for_groups: true)
sign_in(user)
end
describe 'cycle analytics' do
context 'with proper permission' do
before do
group.add_owner(user)
end
it 'calls service' do
expect(group_service).to receive(:summary)
expect(group_service).to receive(:stats)
expect(group_service).to receive(:permissions)
expect(::CycleAnalytics::GroupLevel).to receive(:new).and_return(group_service)
get(:show,
params: {
group_id: group.name
},
format: :json)
expect(response).to be_successful
end
it 'calls service with specific params' do
expect(group_service).to receive(:summary)
expect(group_service).to receive(:stats)
expect(group_service).to receive(:permissions)
expect(::CycleAnalytics::GroupLevel).to receive(:new)
.with(nested_hash_including([:options, :projects], [project.id.to_s]))
.and_return(group_service)
get(:show,
params: {
group_id: group.name,
project_ids: [project.id]
},
format: :json)
expect(response).to be_successful
end
end
context 'as guest' do
before do
group.add_guest(user)
end
it 'returns 403' do
get(:show,
params: {
group_id: group.name
},
format: :json)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
......@@ -33,7 +33,8 @@ const selectedGroup = { fullPath: group.path };
const [selectedStage] = stages;
const selectedStageSlug = selectedStage.slug;
const stageEndpoint = ({ stageId }) => `/-/analytics/value_stream_analytics/stages/${stageId}`;
const stageEndpoint = ({ stageId }) =>
`/groups/${group.full_path}/-/analytics/value_stream_analytics/stages/${stageId}`;
describe('Cycle analytics actions', () => {
let state;
......
......@@ -276,8 +276,8 @@ describe('Api', () => {
const createdBefore = '2019-11-18';
const createdAfter = '2019-08-18';
const stageId = 'thursday';
const dummyCycleAnalyticsUrlRoot = `${dummyUrlRoot}/groups/${groupId}`;
const defaultParams = {
group_id: groupId,
created_after: createdAfter,
created_before: createdBefore,
};
......@@ -321,7 +321,7 @@ describe('Api', () => {
const expectedUrl = analyticsMockData.endpoints.tasksByTypeData;
mock.onGet(expectedUrl).reply(200, tasksByTypeResponse);
Api.cycleAnalyticsTasksByType(params)
Api.cycleAnalyticsTasksByType(groupId, params)
.then(({ data, config: { params: reqParams } }) => {
expect(data).toEqual(tasksByTypeResponse);
expect(reqParams).toEqual(params);
......@@ -345,7 +345,7 @@ describe('Api', () => {
const expectedUrl = analyticsMockData.endpoints.tasksByTypeTopLabelsData;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsTopLabels(params)
Api.cycleAnalyticsTopLabels(groupId, params)
.then(({ data, config: { url, params: reqParams } }) => {
expect(data).toEqual(response);
expect(url).toMatch(expectedUrl);
......@@ -363,10 +363,10 @@ describe('Api', () => {
...defaultParams,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/summary`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/summary`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsSummaryData(params)
Api.cycleAnalyticsSummaryData(groupId, params)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
......@@ -387,7 +387,7 @@ describe('Api', () => {
'cycle_analytics[created_after]': createdAfter,
'cycle_analytics[created_before]': createdBefore,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsGroupStagesAndEvents(groupId, params)
......@@ -409,7 +409,7 @@ describe('Api', () => {
const params = {
...defaultParams,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}/records`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}/records`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsStageEvents(groupId, stageId, params)
......@@ -431,7 +431,7 @@ describe('Api', () => {
const params = {
...defaultParams,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}/median`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}/median`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsStageMedian(groupId, stageId, params)
......@@ -457,13 +457,12 @@ describe('Api', () => {
end_event_identifier: 'issue_closed',
end_event_label_id: null,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages`;
mock.onPost(expectedUrl).reply(200, response);
Api.cycleAnalyticsCreateStage(groupId, customStage)
.then(({ data, config: { params: reqParams, data: reqData, url } }) => {
.then(({ data, config: { data: reqData, url } }) => {
expect(data).toEqual(response);
expect(reqParams).toEqual({ group_id: groupId });
expect(JSON.parse(reqData)).toMatchObject(customStage);
expect(url).toEqual(expectedUrl);
})
......@@ -479,13 +478,12 @@ describe('Api', () => {
name: 'nice-stage',
hidden: true,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}`;
mock.onPut(expectedUrl).reply(200, response);
Api.cycleAnalyticsUpdateStage(stageId, groupId, stageData)
.then(({ data, config: { params: reqParams, data: reqData, url } }) => {
.then(({ data, config: { data: reqData, url } }) => {
expect(data).toEqual(response);
expect(reqParams).toEqual({ group_id: groupId });
expect(JSON.parse(reqData)).toMatchObject(stageData);
expect(url).toEqual(expectedUrl);
})
......@@ -497,13 +495,12 @@ describe('Api', () => {
describe('cycleAnalyticsRemoveStage', () => {
it('deletes the specified data', done => {
const response = { id: stageId, hidden: true, custom: true };
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages/${stageId}`;
mock.onDelete(expectedUrl).reply(200, response);
Api.cycleAnalyticsRemoveStage(stageId, groupId)
.then(({ data, config: { params: reqParams, url } }) => {
.then(({ data, config: { url } }) => {
expect(data).toEqual(response);
expect(reqParams).toEqual({ group_id: groupId });
expect(url).toEqual(expectedUrl);
})
......@@ -518,10 +515,10 @@ describe('Api', () => {
const params = {
...defaultParams,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/value_stream_analytics/stages/thursday/duration_chart`;
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages/thursday/duration_chart`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsDurationChart(stageId, params)
Api.cycleAnalyticsDurationChart(groupId, stageId, params)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
......
......@@ -125,51 +125,6 @@ describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
clean_frontend_fixtures('cycle_analytics/')
end
default_stages = %w[issue plan review code test staging production]
describe Groups::CycleAnalytics::EventsController, type: :controller do
render_views
before do
stub_licensed_features(cycle_analytics_for_groups: true)
prepare_cycle_analytics_data
create_deployment
sign_in(user)
end
default_stages.each do |endpoint|
it "value_stream_analytics/events/#{endpoint}.json" do
get endpoint, params: { group_id: group, format: :json }
expect(response).to be_successful
end
end
end
describe Groups::CycleAnalyticsController, type: :controller do
render_views
before do
stub_licensed_features(cycle_analytics_for_groups: true)
prepare_cycle_analytics_data
create_deployment
sign_in(user)
end
it 'value_stream_analytics/mock_data.json' do
get(:show, params: {
group_id: group.name,
cycle_analytics: { start_date: 30 }
}, format: :json)
expect(response).to be_successful
end
end
describe Analytics::CycleAnalytics::StagesController, type: :controller do
render_views
......
# frozen_string_literal: true
require 'spec_helper'
describe 'value stream analytics events' do
let(:user) { create(:user) }
let(:group) { create(:group)}
let(:project) { create(:project, :repository, namespace: group, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
describe 'GET /:namespace/-/value_stream_analytics/events/:stage' do
before do
stub_licensed_features(cycle_analytics_for_groups: true)
group.add_developer(user)
project.add_developer(user)
3.times do |count|
Timecop.freeze(Time.now + count.days) do
create_cycle
end
end
deploy_master(user, project)
login_as(user)
end
context 'when date range parameters are given' do
it 'filter by `created_after`' do
params = { created_after: issue.created_at - 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json)
expect(json_response['events']).not_to be_empty
end
it 'filters by `created_after` where no events should be found' do
params = { created_after: issue.created_at + 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json)
expect(json_response['events']).to be_empty
end
it 'filter by `created_after` and `created_before`' do
params = { created_after: issue.created_at - 5.days, created_before: issue.created_at + 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json)
expect(json_response['events']).not_to be_empty
end
it 'raises error when date cannot be parsed' do
params = { created_after: 'invalid' }
expect do
get group_cycle_analytics_issue_path(group, params: params, format: :json)
end.to raise_error(ArgumentError)
end
end
it 'lists the issue events' do
get group_cycle_analytics_issue_path(group, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
it 'lists the plan events' do
get group_cycle_analytics_plan_path(group, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
it 'lists the code events' do
get group_cycle_analytics_code_path(group, format: :json)
expect(json_response['events']).not_to be_empty
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
it 'lists the test events', :sidekiq_might_not_need_inline do
get group_cycle_analytics_test_path(group, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the review events' do
get group_cycle_analytics_review_path(group, format: :json)
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
it 'lists the staging events', :sidekiq_might_not_need_inline do
get group_cycle_analytics_staging_path(group, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the production events', :sidekiq_might_not_need_inline do
get group_cycle_analytics_production_path(group, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
context 'specific branch' do
it 'lists the test events', :sidekiq_might_not_need_inline do
branch = project.merge_requests.first.source_branch
get group_cycle_analytics_test_path(group, format: :json, branch: branch)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
end
end
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
pipeline.run
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
end
# frozen_string_literal: true
RSpec.shared_examples 'group permission check on the controller level' do
context 'when `group_id` is not provided' do
before do
params[:group_id] = nil
end
it 'renders `not_found` when group_id is not provided' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when `group_id` is not found' do
before do
params[:group_id] = 'missing_group'
......
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