Commit 893e6633 authored by Pavel Shutsin's avatar Pavel Shutsin

Introduce group level API for devops adoption

Now not only admins can enable devops adoption
but also reporter+ for any group
parent b1f78cf2
......@@ -86,8 +86,10 @@ Returns [`DevopsAdoptionSegmentConnection`](#devopsadoptionsegmentconnection).
| ---- | ---- | ----------- |
| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
| `directDescendantsOnly` | [`Boolean`](#boolean) | Limits segments to direct descendants of specified parent. |
| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
| `parentNamespaceId` | [`NamespaceID`](#namespaceid) | Filter by ancestor namespace. |
### `echo`
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
class SegmentsFinder
attr_reader :params, :current_user
def initialize(current_user, params:)
@current_user = current_user
@params = params
end
def execute
scope = ::Analytics::DevopsAdoption::Segment.ordered_by_name
if direct_descendants_only?
scope = scope.for_namespaces(parent_with_direct_descendants)
else
scope = scope.for_parent(parent_namespace) if parent_namespace
end
scope
end
private
def parent_with_direct_descendants
parent_namespace ? [parent_namespace] + parent_namespace.children : ::Group.top_most
end
def parent_namespace
params[:parent_namespace]
end
def direct_descendants_only?
params[:direct_descendants_only]
end
end
end
end
......@@ -60,9 +60,9 @@ module EE
mount_mutation ::Mutations::DastSiteTokens::Create
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
mount_mutation ::Mutations::QualityManagement::TestCases::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
......
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: "One or many IDs of the segments to delete."
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
module Mixins
# This module ensures that the mutations are admin only
module CommonMethods
ADMIN_MESSAGE = 'You must be an admin to use this mutation'
FEATURE_UNAVAILABLE_MESSAGE = 'Feature is not available'
def ready?(**args)
unless License.feature_available?(:instance_level_devops_adoption)
raise_resource_not_available_error!(FEATURE_UNAVAILABLE_MESSAGE)
end
super
end
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!(ADMIN_MESSAGE)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: 'One or many IDs of the segments to delete.'
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
module Mixins
module CommonMethods
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!
end
end
end
end
end
end
end
......@@ -6,31 +6,44 @@ module Resolvers
module DevopsAdoption
class SegmentsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include Gitlab::Allowable
type Types::Admin::Analytics::DevopsAdoption::SegmentType, null: true
def resolve
authorize!
argument :parent_namespace_id, ::Types::GlobalIDType[::Namespace],
required: false,
description: 'Filter by ancestor namespace.'
if segments_feature_available?
::Analytics::DevopsAdoption::Segment.ordered_by_name
else
::Analytics::DevopsAdoption::Segment.none
end
argument :direct_descendants_only, ::GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Limits segments to direct descendants of specified parent.'
def resolve(parent_namespace_id: nil, direct_descendants_only: false, **)
parent = GlobalID::Locator.locate(parent_namespace_id) if parent_namespace_id
authorize!(parent)
::Analytics::DevopsAdoption::SegmentsFinder.new(current_user, params: {
parent_namespace: parent, direct_descendants_only: direct_descendants_only
}).execute
end
private
def segments_feature_available?
License.feature_available?(:instance_level_devops_adoption)
def authorize!(parent)
parent ? authorize_with_namespace!(parent) : authorize_global!
end
def authorize!
admin? || raise_resource_not_available_error!
def authorize_global!
unless can?(current_user, :view_instance_devops_adoption)
raise_resource_not_available_error!
end
end
def admin?
context[:current_user].present? && context[:current_user].admin?
def authorize_with_namespace!(parent)
unless can?(current_user, :view_group_devops_adoption, parent)
raise_resource_not_available_error!
end
end
end
end
......
......@@ -12,4 +12,13 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
validates :namespace, uniqueness: true, presence: true
scope :ordered_by_name, -> { includes(:namespace).order('"namespaces"."name" ASC') }
scope :for_namespaces, -> (namespaces) { where(namespace_id: namespaces) }
scope :for_parent, -> (namespace) {
if Feature.enabled?(:recursive_namespace_lookup_as_inner_join, namespace)
join_sql = namespace.self_and_descendants.to_sql
joins("INNER JOIN (#{join_sql}) namespaces ON namespaces.id=#{self.arel_table.name}.namespace_id")
else
for_namespaces(namespace.self_and_descendants)
end
}
end
......@@ -26,6 +26,7 @@ class License < ApplicationRecord
group_webhooks
group_level_devops_adoption
instance_level_devops_adoption
group_level_devops_adoption
issuable_default_templates
issue_weights
iterations
......
......@@ -29,13 +29,21 @@ module EE
end
end
condition(:instance_devops_adoption_available) do
::License.feature_available?(:instance_level_devops_adoption)
end
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
rule { admin & instance_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_instance_devops_adoption
end
rule { admin }.policy do
enable :read_licenses
enable :destroy_licenses
enable :read_all_geo
enable :manage_devops_adoption_segments
enable :manage_subscription
end
......
......@@ -197,6 +197,7 @@ module EE
end
rule { reporter & group_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_group_devops_adoption
end
......
......@@ -4,21 +4,19 @@ module Analytics
module DevopsAdoption
module Segments
class BulkDeleteService
include CommonMethods
def initialize(segments:, current_user:)
@segments = segments
@current_user = current_user
end
def execute
authorize!
deletion_services.map(&:authorize!)
result = nil
ActiveRecord::Base.transaction do
segments.each do |segment|
response = delete_segment(segment)
deletion_services.each do |service|
response = service.execute
if response.error?
result = ServiceResponse.error(message: response.message, payload: response_payload)
......@@ -40,8 +38,10 @@ module Analytics
{ segments: segments }
end
def delete_segment(segment)
DeleteService.new(current_user: current_user, segment: segment).execute
def deletion_services
@deletion_services ||= segments.map do |segment|
DeleteService.new(current_user: current_user, segment: segment)
end
end
end
end
......
......@@ -4,8 +4,6 @@ module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
......@@ -14,20 +12,26 @@ module Analytics
def execute
authorize!
segments = params[:namespaces].map do |namespace|
response = FindOrCreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
response.payload[:segment]
segments = services.map do |service|
service.execute.payload[:segment]
end
ServiceResponse.success(payload: { segments: segments })
end
def authorize!
services.each(&:authorize!)
end
private
attr_reader :params, :current_user
def services
@services ||= params[:namespaces].map do |namespace|
FindOrCreateService.new(current_user: current_user, params: { namespace: namespace })
end
end
end
end
end
......
......@@ -7,7 +7,7 @@ module Analytics
include Gitlab::Allowable
def authorize!
unless can?(current_user, :manage_devops_adoption_segments, :global)
unless can?(current_user, :manage_devops_adoption_segments, namespace)
raise AuthorizationError.new(self, 'Forbidden')
end
end
......
......@@ -15,7 +15,7 @@ module Analytics
def execute
authorize!
segment.assign_attributes(attributes)
segment.assign_attributes(namespace: namespace)
if segment.save
Analytics::DevopsAdoption::CreateSnapshotWorker.perform_async(segment.id)
......@@ -34,8 +34,8 @@ module Analytics
{ segment: segment }
end
def attributes
params.slice(:namespace, :namespace_id)
def namespace
params[:namespace]
end
end
end
......
......@@ -19,7 +19,7 @@ module Analytics
ServiceResponse.success(payload: response_payload)
rescue ActiveRecord::RecordNotDestroyed
ServiceResponse.error(message: 'Devops Adoption Segment deletion error', payload: response_payload)
ServiceResponse.error(message: 'DevOps Adoption Segment deletion error', payload: response_payload)
end
end
......@@ -28,7 +28,11 @@ module Analytics
attr_reader :segment, :current_user
def response_payload
{ segment: @segment }
{ segment: segment }
end
def namespace
segment.namespace
end
end
end
......
......@@ -14,21 +14,29 @@ module Analytics
def execute
authorize!
segment = Analytics::DevopsAdoption::Segment.find_by_namespace_id(namespace_id)
segment = Analytics::DevopsAdoption::Segment.find_by_namespace_id(namespace.id)
if segment
ServiceResponse.success(payload: { segment: segment })
else
CreateService.new(current_user: current_user, params: params).execute
create_service.execute
end
end
def authorize!
create_service.authorize!
end
private
attr_reader :params, :current_user
def namespace_id
params.fetch(:namespace_id, params[:namespace]&.id)
def namespace
params[:namespace]
end
def create_service
@create_service ||= CreateService.new(current_user: current_user, params: params)
end
end
end
......
---
title: Introduce group-level API for DevOps adoption
merge_request: 55479
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::SegmentsFinder do
let_it_be(:admin_user) { create(:user, :admin) }
subject(:finder_segments) { described_class.new(admin_user, params: params).execute }
let(:params) { {} }
describe '#execute' do
let_it_be(:root_group_1) { create(:group, name: 'bbb') }
let_it_be(:root_group_2) { create(:group, name: 'aaa') }
let_it_be(:segment_1) { create(:devops_adoption_segment, namespace: root_group_1) }
let_it_be(:segment_2) { create(:devops_adoption_segment, namespace: root_group_2) }
let_it_be(:direct_subgroup) { create(:group, name: 'ccc', parent: root_group_1) }
let_it_be(:direct_subgroup_segment) do
create(:devops_adoption_segment, namespace: direct_subgroup)
end
let_it_be(:indirect_subgroup) { create(:group, name: 'ddd', parent: direct_subgroup) }
let_it_be(:indirect_subgroup_segment) do
create(:devops_adoption_segment, namespace: indirect_subgroup)
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
stub_licensed_features(group_level_devops_adoption: true)
end
context 'for instance level' do
it 'returns segments ordered by name' do
expect(finder_segments).to eq([segment_2, segment_1, direct_subgroup_segment, indirect_subgroup_segment])
end
context 'with direct_descendants_only' do
let(:params) { super().merge(direct_descendants_only: true) }
it 'returns direct descendants only' do
expect(finder_segments).to eq([segment_2, segment_1])
end
end
end
context 'for group level' do
let(:params) { super().merge(parent_namespace: segment_1.namespace) }
it 'returns segments scoped to given namespace ordered by name' do
expect(finder_segments).to eq([segment_1, direct_subgroup_segment, indirect_subgroup_segment])
end
context 'with direct_descendants_only' do
let(:params) { super().merge(direct_descendants_only: true) }
it 'returns direct descendants only' do
expect(finder_segments).to eq([segment_1, direct_subgroup_segment])
end
end
end
end
end
......@@ -6,7 +6,6 @@ RSpec.describe Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver do
include GraphqlHelpers
let_it_be(:admin_user) { create(:user, :admin) }
let(:current_user) { admin_user }
def resolve_segments(args = {}, context = {})
resolve(described_class, args: args, ctx: context)
......@@ -14,49 +13,105 @@ RSpec.describe Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver do
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:segment_1) { create(:devops_adoption_segment, namespace: create(:group, name: 'bbb')) }
let_it_be(:segment_2) { create(:devops_adoption_segment, namespace: create(:group, name: 'aaa')) }
let_it_be(:root_group_1) { create(:group, name: 'bbb') }
let_it_be(:root_group_2) { create(:group, name: 'aaa') }
let_it_be(:segment_1) { create(:devops_adoption_segment, namespace: root_group_1) }
let_it_be(:segment_2) { create(:devops_adoption_segment, namespace: root_group_2) }
let_it_be(:direct_subgroup) { create(:group, name: 'ccc', parent: root_group_1) }
let_it_be(:direct_subgroup_segment) do
create(:devops_adoption_segment, namespace: direct_subgroup)
end
subject { resolve_segments({}, { current_user: current_user }) }
let_it_be(:indirect_subgroup) { create(:group, name: 'ddd', parent: direct_subgroup) }
let_it_be(:indirect_subgroup_segment) do
create(:devops_adoption_segment, namespace: indirect_subgroup)
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
stub_licensed_features(group_level_devops_adoption: true)
end
context 'when requesting project count measurements' do
context 'as an admin user' do
subject(:resolved_segments) { resolve_segments(params, { current_user: current_user }) }
let(:params) { {} }
context 'for instance level', :enable_admin_mode do
let(:current_user) { admin_user }
it 'returns the records, ordered by name' do
expect(subject).to eq([segment_2, segment_1])
context 'as an admin user' do
it 'returns segments for all groups, ordered by name' do
expect(resolved_segments).to eq([segment_2, segment_1, direct_subgroup_segment, indirect_subgroup_segment])
end
context 'with direct_descendants_only' do
let(:params) { super().merge(direct_descendants_only: true) }
it 'returns segments for root groups, ordered by name' do
expect(resolved_segments).to eq([segment_2, segment_1])
end
end
end
context 'when the feature is not available' do
let(:current_user) { admin_user }
context 'as a non-admin user' do
let(:current_user) { user }
it 'raises ResourceNotAvailable error' do
expect { resolved_segments }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the feature is not available' do
before do
stub_licensed_features(instance_level_devops_adoption: false)
end
it 'returns the records, ordered by name' do
expect(subject).to be_empty
it 'raises ResourceNotAvailable error' do
expect { resolved_segments }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'as a non-admin user' do
context 'for group level' do
let(:params) { { parent_namespace_id: root_group_1.to_gid.to_s } }
let(:current_user) { user }
context 'for reporter+' do
before do
root_group_1.add_reporter(user)
end
it 'returns segments for given parent group and its descendants' do
expect(resolved_segments).to eq([segment_1, direct_subgroup_segment, indirect_subgroup_segment])
end
context 'with direct_descendants_only' do
let(:params) { super().merge(direct_descendants_only: true) }
it 'returns segments for given parent group and its direct descendants' do
expect(resolved_segments).to eq([segment_1, direct_subgroup_segment])
end
end
end
context 'for guests' do
before do
root_group_1.add_guest(user)
end
it 'raises ResourceNotAvailable error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { resolved_segments }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'as an unauthenticated user' do
let(:current_user) { nil }
context 'when the feature is not available' do
before do
stub_licensed_features(instance_level_devops_adoption: false)
end
it 'raises ResourceNotAvailable error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { resolved_segments }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
......
......@@ -28,6 +28,39 @@ RSpec.describe Analytics::DevopsAdoption::Segment, type: :model do
end
end
describe '.for_namespaces' do
subject(:segments) { described_class.for_namespaces(namespaces) }
let_it_be(:segment1) { create(:devops_adoption_segment) }
let_it_be(:segment2) { create(:devops_adoption_segment) }
let_it_be(:segment3) { create(:devops_adoption_segment) }
let_it_be(:namespaces) { [segment1.namespace, segment2.namespace]}
it 'selects segments for given namespaces only' do
expect(segments).to match_array([segment1, segment2])
end
end
describe '.for_parent' do
let_it_be(:group) { create :group }
let_it_be(:subgroup) { create :group, parent: group }
let_it_be(:group2) { create :group }
let_it_be(:segment1) { create(:devops_adoption_segment, namespace: group) }
let_it_be(:segment2) { create(:devops_adoption_segment, namespace: subgroup) }
let_it_be(:segment3) { create(:devops_adoption_segment, namespace: group2) }
subject(:segments) { described_class.for_parent(group) }
before do
stub_feature_flags(recursive_namespace_lookup_as_inner_join: true)
end
it 'selects segments for given namespace only' do
expect(segments).to match_array([segment1, segment2])
end
end
describe '.latest_snapshot' do
it 'loads the latest snapshot' do
segment = create(:devops_adoption_segment)
......
......@@ -310,4 +310,26 @@ RSpec.describe GlobalPolicy do
end
end
end
describe ':view_instance_devops_adoption & :manage_devops_adoption_segments', :enable_admin_mode do
let(:current_user) { admin }
context 'when license does not include the feature' do
before do
stub_licensed_features(instance_level_devops_adoption: false)
end
it { is_expected.to be_disallowed(:view_instance_devops_adoption, :manage_devops_adoption_segments) }
end
context 'when feature is enabled and license include the feature' do
it { is_expected.to be_allowed(:view_instance_devops_adoption, :manage_devops_adoption_segments) }
context 'for non-admins' do
let(:current_user) { user }
it { is_expected.to be_disallowed(:view_instance_devops_adoption, :manage_devops_adoption_segments) }
end
end
end
end
......@@ -2,15 +2,24 @@
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate do
RSpec.describe Mutations::Analytics::DevopsAdoption::Segments::BulkFindOrCreate do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create(:group, name: 'aaaa') }
let_it_be(:group2) { create(:group, name: 'bbbb') }
let_it_be(:group3) { create(:group, name: 'cccc') }
let_it_be(:reporter) do
create(:user).tap do |u|
group.add_reporter(u)
group2.add_reporter(u)
group3.add_reporter(u)
end
end
let_it_be(:existing_segment) { create :devops_adoption_segment, namespace: group3 }
let(:current_user) { reporter }
let(:variables) { { namespace_ids: [group.to_gid.to_s, group2.to_gid.to_s, group3.to_gid.to_s] } }
let(:mutation) do
......@@ -34,13 +43,25 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOr
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
stub_licensed_features(group_level_devops_adoption: true)
end
it_behaves_like 'DevOps Adoption top level errors'
context 'when the user cannot manage segments at least for one namespace' do
let(:current_user) { create(:user).tap { |u| group.add_reporter(u) } }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the feature is not available' do
before do
stub_licensed_features(group_level_devops_adoption: false)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
it 'creates the segment for each passed namespace or returns existing segment' do
post_graphql_mutation(mutation, current_user: admin)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to be_empty
......
......@@ -2,11 +2,13 @@
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Create do
RSpec.describe Mutations::Analytics::DevopsAdoption::Segments::Create do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create(:group, name: 'bbbb') }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
let(:current_user) { reporter }
let(:variables) { { namespace_id: group.to_gid.to_s } }
let(:mutation) do
......@@ -30,13 +32,25 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Create do
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
stub_licensed_features(group_level_devops_adoption: true)
end
context 'when the user cannot manage segments' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the feature is not available' do
before do
stub_licensed_features(group_level_devops_adoption: false)
end
it_behaves_like 'DevOps Adoption top level errors'
it_behaves_like 'a mutation that returns a top-level access error'
end
it 'creates the segment with the group' do
post_graphql_mutation(mutation, current_user: admin)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to be_empty
......
......@@ -2,12 +2,14 @@
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
RSpec.describe Mutations::Analytics::DevopsAdoption::Segments::Delete do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create :group }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
let(:current_user) { reporter }
let!(:segment) { create(:devops_adoption_segment, namespace: group) }
let!(:segment) { create(:devops_adoption_segment) }
let(:variables) { { id: segment.to_gid.to_s } }
let(:mutation) do
......@@ -20,17 +22,29 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
stub_licensed_features(group_level_devops_adoption: true)
end
def mutation_response
graphql_mutation_response(:delete_devops_adoption_segment)
end
it_behaves_like 'DevOps Adoption top level errors'
context 'when the user cannot manage segments' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the feature is not available' do
before do
stub_licensed_features(group_level_devops_adoption: false)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
it 'deletes the segment' do
post_graphql_mutation(mutation, current_user: admin)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to be_empty
expect(::Analytics::DevopsAdoption::Segment.find_by_id(segment.id)).to eq(nil)
......@@ -42,8 +56,13 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
let(:variables) { { id: [segment.to_gid.to_s, segment2.to_gid.to_s] } }
before do
segment2.namespace.add_reporter(current_user)
segment3.namespace.add_reporter(current_user)
end
it 'deletes the segments specified for deletion' do
post_graphql_mutation(mutation, current_user: admin)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to be_empty
expect(::Analytics::DevopsAdoption::Segment.where(id: [segment.id, segment2.id, segment3.id]))
......
......@@ -5,19 +5,20 @@ require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::BulkDeleteService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:admin) { create(:user, :admin) }
let(:segment) { create(:devops_adoption_segment, namespace: group) }
let(:segment2) { create(:devops_adoption_segment) }
let(:current_user) { admin }
subject { described_class.new(segments: [segment, segment2], current_user: user).execute }
subject(:response) { described_class.new(segments: [segment, segment2], current_user: current_user).execute }
before do
enable_admin_mode!(user)
enable_admin_mode!(admin)
end
it 'deletes the segments' do
expect(subject).to be_success
expect(response).to be_success
expect(segment).not_to be_persisted
expect(segment2).not_to be_persisted
end
......@@ -26,20 +27,23 @@ RSpec.describe Analytics::DevopsAdoption::Segments::BulkDeleteService do
it 'keeps records and returns error response' do
expect(segment).to receive(:destroy).and_raise(ActiveRecord::RecordNotDestroyed)
expect(subject).to be_error
expect(subject.message).to eq('Devops Adoption Segment deletion error')
expect(response).to be_error
expect(response.message).to eq('DevOps Adoption Segment deletion error')
expect(segment).to be_persisted
expect(segment2).to be_persisted
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
it 'authorizes for manage_devops_adoption' do
expect(::Ability).to receive(:allowed?)
.with(current_user, :manage_devops_adoption_segments, group)
.at_least(1)
.and_return(true)
expect(::Ability).to receive(:allowed?)
.with(current_user, :manage_devops_adoption_segments, segment2.namespace)
.at_least(1)
.and_return(true)
response
end
end
......@@ -3,37 +3,45 @@
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::BulkFindOrCreateService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:reporter) do
create(:user).tap do |u|
group.add_reporter(u)
group2.add_reporter(u)
end
end
let_it_be(:segment) { create :devops_adoption_segment, namespace: group }
let(:current_user) { reporter }
let(:params) { { namespaces: [group, group2] } }
let!(:segment) { create :devops_adoption_segment, namespace: group }
subject { described_class.new(params: params, current_user: user).execute }
subject(:response) { described_class.new(params: params, current_user: current_user).execute }
before do
enable_admin_mode!(user)
end
it 'authorizes for manage_devops_adoption' do
expect(::Ability).to receive(:allowed?)
.with(current_user, :manage_devops_adoption_segments, group)
.at_least(1)
.and_return(true)
expect(::Ability).to receive(:allowed?)
.with(current_user, :manage_devops_adoption_segments, group2)
.at_least(1)
.and_return(true)
context 'for admins' do
it 'returns existing segments for namespaces and creates new one if none exists' do
expect do
subject
end.to change { ::Analytics::DevopsAdoption::Segment.count }.by(1)
expect(subject.payload.fetch(:segments)).to include(segment)
end
response
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
context 'when the user cannot manage segments at least for one namespace' do
let(:current_user) { create(:user).tap { |u| group.add_reporter(u) } }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
expect { response }.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
it 'returns existing segments for namespaces and creates new one if none exists' do
expect { response }.to change { ::Analytics::DevopsAdoption::Segment.count }.by(1)
expect(response.payload.fetch(:segments)).to include(segment)
end
end
......@@ -3,52 +3,39 @@
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::CreateService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
let(:current_user) { reporter }
let(:params) { { namespace: group } }
let(:segment) { subject.payload[:segment] }
subject { described_class.new(params: params, current_user: user).execute }
before do
enable_admin_mode!(user)
end
subject(:response) { described_class.new(params: params, current_user: current_user).execute }
it 'persists the segment' do
expect(subject).to be_success
expect(response).to be_success
expect(segment.namespace).to eq(group)
end
it 'schedules for snapshot creation' do
allow(Analytics::DevopsAdoption::CreateSnapshotWorker).to receive(:perform_async).and_call_original
subject
response
expect(Analytics::DevopsAdoption::CreateSnapshotWorker).to have_received(:perform_async).with(Analytics::DevopsAdoption::Segment.last.id)
end
context 'when namespace is not given' do
before do
params.delete(:namespace)
end
it 'authorizes for manage_devops_adoption' do
expect(::Ability).to receive(:allowed?).with(current_user, :manage_devops_adoption_segments, group).and_return true
it "doesn't save the segment" do
expect(subject).to be_error
expect(subject.message).to eq('Validation error')
expect(segment.namespace).to be_nil
end
response
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
context 'for guests' do
let(:current_user) { create(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
expect { response }.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
......@@ -3,20 +3,15 @@
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::DeleteService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
let(:segment) { create(:devops_adoption_segment, namespace: group) }
let(:current_user) { reporter }
subject { described_class.new(segment: segment, current_user: user).execute }
before do
enable_admin_mode!(user)
end
subject(:response) { described_class.new(segment: segment, current_user: current_user).execute }
it 'deletes the segment' do
expect(subject).to be_success
expect(response).to be_success
expect(segment).not_to be_persisted
end
......@@ -24,18 +19,22 @@ RSpec.describe Analytics::DevopsAdoption::Segments::DeleteService do
it 'returns error response' do
expect(segment).to receive(:destroy).and_raise(ActiveRecord::RecordNotDestroyed)
expect(subject).to be_error
expect(subject.message).to eq('Devops Adoption Segment deletion error')
expect(response).to be_error
expect(response.message).to eq('DevOps Adoption Segment deletion error')
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'authorizes for manage_devops_adoption' do
expect(::Ability).to receive(:allowed?).with(current_user, :manage_devops_adoption_segments, group).and_return true
response
end
context 'when user cannot manage segments for the namespace' do
let(:current_user) { create(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
expect { response }.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
......@@ -3,28 +3,23 @@
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::FindOrCreateService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:reporter) { create(:user).tap { |u| group.add_reporter(u) } }
let(:current_user) { reporter }
let(:params) { { namespace: group } }
let(:segment) { subject.payload[:segment] }
subject { described_class.new(params: params, current_user: user).execute }
subject(:response) { described_class.new(params: params, current_user: current_user).execute }
before do
enable_admin_mode!(user)
stub_licensed_features(group_level_devops_adoption: true)
end
context 'for admins' do
context 'when segment for given namespace already exists' do
let!(:segment) { create :devops_adoption_segment, namespace: group }
it 'returns existing segment' do
expect do
subject
end.not_to change { Analytics::DevopsAdoption::Segment.count }
expect { response }.not_to change { Analytics::DevopsAdoption::Segment.count }
expect(subject.payload.fetch(:segment)).to eq(segment)
end
......@@ -32,22 +27,28 @@ RSpec.describe Analytics::DevopsAdoption::Segments::FindOrCreateService do
context 'when segment for given namespace does not exist' do
it 'calls for segment creation' do
expect_next_instance_of(Analytics::DevopsAdoption::Segments::CreateService, current_user: user, params: { namespace: group }) do |instance|
expect_next_instance_of(Analytics::DevopsAdoption::Segments::CreateService, current_user: current_user, params: { namespace: group }) do |instance|
expect(instance).to receive(:execute).and_return('create_response')
end
expect(subject).to eq 'create_response'
expect(response).to eq 'create_response'
end
end
it 'authorizes for manage_devops_adoption' do
expect(::Ability).to receive(:allowed?)
.with(current_user, :manage_devops_adoption_segments, group)
.at_least(1)
.and_return(true)
response
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
context 'when user cannot manage devops adoption for given namespace' do
let(:current_user) { create(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
expect { response }.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment