Commit f6b1f00a authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262395-devops-adoption-mutations' into 'master'

GraphQL mutations for Devops Adoption Segment

See merge request gitlab-org/gitlab!47066
parents 9e372d91 a0c53384
......@@ -7,7 +7,7 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
has_many :groups, through: :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validate :validate_segment_count
validate :validate_segment_count, on: :create
accepts_nested_attributes_for :segment_selections, allow_destroy: true
......
......@@ -14,7 +14,7 @@ class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
validates :group_id, uniqueness: { scope: :segment_id, if: :group }
validate :exclusive_project_or_group
validate :validate_selection_count
validate :validate_selection_count, on: :create
private
......@@ -27,9 +27,9 @@ class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
def validate_selection_count
return unless segment
selection_count_for_segment = self.class.where(segment: segment).count
if selection_count_for_segment >= ALLOWED_SELECTIONS_PER_SEGMENT
# handle single model creation and bulk creation from accepts_nested_attributes_for
selections = segment.segment_selections + [self]
if selections.reject(&:marked_for_destruction?).uniq.size > ALLOWED_SELECTIONS_PER_SEGMENT
errors.add(:segment, s_('DevopsAdoptionSegmentSelection|The maximum number of selections has been reached'))
end
end
......
---
title: Add GraphQL mutations for Devops Adoption Segment
merge_request: 47066
author:
type: added
......@@ -932,6 +932,11 @@ type AlertTodoCreatePayload {
todo: Todo
}
"""
Identifier of Analytics::DevopsAdoption::Segment
"""
scalar AnalyticsDevopsAdoptionSegmentID
"""
User availability status
"""
......@@ -3894,6 +3899,46 @@ type CreateCustomEmojiPayload {
errors: [String!]!
}
"""
Autogenerated input type of CreateDevopsAdoptionSegment
"""
input CreateDevopsAdoptionSegmentInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The array of group IDs to set for the segment
"""
groupIds: [GroupID!]
"""
Name of the segment
"""
name: String!
}
"""
Autogenerated return type of CreateDevopsAdoptionSegment
"""
type CreateDevopsAdoptionSegmentPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The segment after mutation
"""
segment: DevopsAdoptionSegment
}
"""
Autogenerated input type of CreateDiffNote
"""
......@@ -5298,6 +5343,36 @@ type DeleteAnnotationPayload {
errors: [String!]!
}
"""
Autogenerated input type of DeleteDevopsAdoptionSegment
"""
input DeleteDevopsAdoptionSegmentInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
ID of the segment
"""
id: AnalyticsDevopsAdoptionSegmentID!
}
"""
Autogenerated return type of DeleteDevopsAdoptionSegment
"""
type DeleteDevopsAdoptionSegmentPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
The response from the AdminSidekiqQueuesDeleteJobs mutation
"""
......@@ -13704,6 +13779,7 @@ type Mutation {
. Available only when feature flag `custom_emoji` is enabled
"""
createCustomEmoji(input: CreateCustomEmojiInput!): CreateCustomEmojiPayload
createDevopsAdoptionSegment(input: CreateDevopsAdoptionSegmentInput!): CreateDevopsAdoptionSegmentPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
......@@ -13723,6 +13799,7 @@ type Mutation {
dastSiteTokenCreate(input: DastSiteTokenCreateInput!): DastSiteTokenCreatePayload
dastSiteValidationCreate(input: DastSiteValidationCreateInput!): DastSiteValidationCreatePayload
deleteAnnotation(input: DeleteAnnotationInput!): DeleteAnnotationPayload
deleteDevopsAdoptionSegment(input: DeleteDevopsAdoptionSegmentInput!): DeleteDevopsAdoptionSegmentPayload
designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
designManagementMove(input: DesignManagementMoveInput!): DesignManagementMovePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
......@@ -13804,6 +13881,7 @@ type Mutation {
updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload
updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateDevopsAdoptionSegment(input: UpdateDevopsAdoptionSegmentInput!): UpdateDevopsAdoptionSegmentPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
"""
......@@ -22167,6 +22245,51 @@ type UpdateContainerExpirationPolicyPayload {
errors: [String!]!
}
"""
Autogenerated input type of UpdateDevopsAdoptionSegment
"""
input UpdateDevopsAdoptionSegmentInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The array of group IDs to set for the segment
"""
groupIds: [GroupID!]
"""
ID of the segment
"""
id: AnalyticsDevopsAdoptionSegmentID!
"""
Name of the segment
"""
name: String!
}
"""
Autogenerated return type of UpdateDevopsAdoptionSegment
"""
type UpdateDevopsAdoptionSegmentPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The segment after mutation
"""
segment: DevopsAdoptionSegment
}
input UpdateDiffImagePositionInput {
"""
Total height of the image
......
......@@ -643,6 +643,16 @@ Autogenerated return type of CreateCustomEmoji.
| `customEmoji` | CustomEmoji | The new custom emoji |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### CreateDevopsAdoptionSegmentPayload
Autogenerated return type of CreateDevopsAdoptionSegment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `segment` | DevopsAdoptionSegment | The segment after mutation |
### CreateDiffNotePayload
Autogenerated return type of CreateDiffNote.
......@@ -892,6 +902,15 @@ Autogenerated return type of DeleteAnnotation.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DeleteDevopsAdoptionSegmentPayload
Autogenerated return type of DeleteDevopsAdoptionSegment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DeleteJobsResponse
The response from the AdminSidekiqQueuesDeleteJobs mutation.
......@@ -3325,6 +3344,16 @@ Autogenerated return type of UpdateContainerExpirationPolicy.
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateDevopsAdoptionSegmentPayload
Autogenerated return type of UpdateDevopsAdoptionSegment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `segment` | DevopsAdoptionSegment | The segment after mutation |
### UpdateEpicPayload
Autogenerated return type of UpdateEpic.
......
......@@ -45,6 +45,9 @@ module EE
mount_mutation ::Mutations::Security::CiConfiguration::ConfigureSast
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::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
prepend(Types::DeprecatedMutations)
end
......
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
include Mixins::CommonArguments
graphql_name 'CreateDevopsAdoptionSegment'
def resolve(name:, group_ids: [], **)
groups = GlobalID::Locator.locate_many(group_ids)
response = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { name: name, groups: groups })
.execute
resolve_segment(response)
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: "ID of the segment"
def resolve(id:, **)
response = ::Analytics::DevopsAdoption::Segments::DeleteService
.new(segment: id.find, current_user: current_user)
.execute
{ errors: errors_on_object(response.payload[:segment]) }
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 ::Gitlab::Graphql::Errors::ResourceNotAvailable, FEATURE_UNAVAILABLE_MESSAGE
end
unless current_user&.admin?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, ADMIN_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
end
module CommonArguments
extend ActiveSupport::Concern
included do
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the segment'
argument :group_ids, [::Types::GlobalIDType[::Group]],
required: false,
description: 'The array of group IDs to set for the segment'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation'
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Update < BaseMutation
include Mixins::CommonMethods
include Mixins::CommonArguments
graphql_name 'UpdateDevopsAdoptionSegment'
argument :id, ::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment],
required: true,
description: "ID of the segment"
def resolve(id:, name:, group_ids: nil, **)
groups = GlobalID::Locator.locate_many(group_ids) if group_ids
segment = ::Analytics::DevopsAdoption::Segments::UpdateService
.new(current_user: current_user, segment: id.find, params: { name: name, groups: groups })
.execute
resolve_segment(segment)
end
end
end
end
end
end
end
......@@ -23,6 +23,7 @@ module EE
enable :read_licenses
enable :destroy_licenses
enable :read_all_geo
enable :manage_devops_adoption_segments
end
rule { admin & pages_size_limit_available }.enable :update_max_pages_size
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class CreateService
include Gitlab::Allowable
def initialize(segment: Analytics::DevopsAdoption::Segment.new, params: {}, current_user:)
@segment = segment
@params = params
@current_user = current_user
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
segment.assign_attributes(attributes)
if segment.save
ServiceResponse.success(payload: response_payload)
else
ServiceResponse.error(message: 'Validation error', payload: response_payload)
end
end
private
attr_reader :segment, :params, :current_user
def response_payload
{ segment: @segment }
end
def attributes
{ name: params[:name], segment_selections_attributes: segment_selections_attributes }.compact
end
def segment_selections_attributes
groups.map { |group| { group: group } }
end
def groups
@groups ||= Array(params[:groups]).uniq
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class DeleteService
include Gitlab::Allowable
def initialize(segment:, current_user:)
@segment = segment
@current_user = current_user
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
begin
segment.destroy!
ServiceResponse.success(payload: response_payload)
rescue ActiveRecord::RecordNotDestroyed
ServiceResponse.error(message: 'Devops Adoption Segment deletion error', payload: response_payload)
end
end
private
attr_reader :segment, :current_user
def response_payload
{ segment: @segment }
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class UpdateService < CreateService
extend ::Gitlab::Utils::Override
private
override :segment_selections_attributes
def segment_selections_attributes
return if params[:groups].nil?
existing_selections_by_group_id = segment.segment_selections.index_by(&:group_id)
groups_by_id = groups.index_by(&:id)
selection_attributes = groups.map do |group|
{ group: group }.tap do |attrs|
attrs[:id] = existing_selections_by_group_id[group.id].id if existing_selections_by_group_id[group.id]
end
end
existing_selections_by_group_id.each do |group_id, selection|
unless groups_by_id[group_id]
selection_attributes << { id: selection.id, _destroy: '1' }
end
end
selection_attributes
end
end
end
end
end
......@@ -37,4 +37,24 @@ RSpec.describe Analytics::DevopsAdoption::Segment, type: :model do
expect(subject).to eq([segment_2, segment_1])
end
end
describe 'length validation on accepts_nested_attributes_for for segment_selections' do
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
subject { described_class.create!(name: 'test', segment_selections_attributes: [{ group: group_1 }]) }
before do
stub_const("Analytics::DevopsAdoption::SegmentSelection::ALLOWED_SELECTIONS_PER_SEGMENT", 1)
end
it 'validates the number of segment_selections' do
selections = [{ group: group_1, _destroy: 1 }, { group: group_2 }]
subject.assign_attributes(segment_selections_attributes: selections)
expect(subject).to be_invalid
expect(subject.errors[:"segment_selections.segment"]).to eq([s_('DevopsAdoptionSegmentSelection|The maximum number of selections has been reached')])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Create do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group_1) { create(:group, name: 'bbbb') }
let_it_be(:group_2) { create(:group, name: 'cccc') }
let_it_be(:group_3) { create(:group, name: 'aaaa') }
let(:variables) { { name: 'my segment', group_ids: [group_1.to_gid.to_s, group_2.to_gid.to_s, group_3.to_gid.to_s] } }
let(:mutation) do
graphql_mutation(:create_devops_adoption_segment, variables) do
<<-QL.strip_heredoc
clientMutationId
errors
segment {
id
name
groups {
nodes {
id
name
}
}
}
QL
end
end
def mutation_response
graphql_mutation_response(:create_devops_adoption_segment)
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
end
it_behaves_like 'DevOps Adoption top level errors'
it 'creates the segment with the groups' do
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
segment = mutation_response['segment']
expect(segment['name']).to eq('my segment')
group_names = segment['groups']['nodes'].map { |node| node['name'] }
expect(group_names).to match_array(%w[aaaa bbbb cccc])
end
context 'when group_ids is missing' do
before do
variables.delete(:group_ids)
post_graphql_mutation(mutation, current_user: admin)
end
it 'creates an empty segment' do
expect(mutation_response['segment']['groups']['nodes']).to be_empty
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group_1) { create(:group, name: 'bbbb') }
let(:segment) { create(:devops_adoption_segment, name: 'my segment') }
let(:variables) { { id: segment.to_gid.to_s } }
let(:mutation) do
graphql_mutation(:delete_devops_adoption_segment, variables) do
<<~QL
clientMutationId
errors
QL
end
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
create(:devops_adoption_segment_selection, :group, segment: segment, group: group_1)
end
def mutation_response
graphql_mutation_response(:delete_devops_adoption_segment)
end
it_behaves_like 'DevOps Adoption top level errors'
it 'deletes the segment' do
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
expect(::Analytics::DevopsAdoption::Segment.find_by_id(segment.id)).to eq(nil)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Update do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group_1) { create(:group, name: 'bbbb') }
let_it_be(:group_2) { create(:group, name: 'cccc') }
let_it_be(:group_3) { create(:group, name: 'aaaa') }
let(:segment) { create(:devops_adoption_segment, name: 'my segment') }
let(:variables) { { id: segment.to_gid.to_s, name: 'new name', group_ids: [group_1.to_gid.to_s, group_2.to_gid.to_s, group_3.to_gid.to_s] } }
let(:mutation) do
graphql_mutation(:update_devops_adoption_segment, variables) do
<<-QL.strip_heredoc
clientMutationId
errors
segment {
id
name
groups {
nodes {
id
name
}
}
}
QL
end
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
create(:devops_adoption_segment_selection, :group, segment: segment, group: group_1)
end
def mutation_response
graphql_mutation_response(:update_devops_adoption_segment)
end
it_behaves_like 'DevOps Adoption top level errors'
it 'updates the segment name and the groups' do
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
segment = mutation_response['segment']
expect(segment['name']).to eq('new name')
group_names = segment['groups']['nodes'].map { |node| node['name'] }
expect(group_names).to match_array(%w[aaaa bbbb cccc])
end
context 'when group_ids is missing' do
before do
variables.delete(:group_ids)
end
it 'does not update the group ids' do
expect { post_graphql_mutation(mutation, current_user: admin) }.not_to change { segment.segment_selections }
end
end
context 'when group_ids is empty' do
before do
variables[:group_ids] = []
post_graphql_mutation(mutation, current_user: admin)
end
it 'removes all selections' do
expect(mutation_response['segment']['groups']['nodes']).to be_empty
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::CreateService do
include AdminModeHelper
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let(:params) { { name: 'my service', groups: [group] } }
let(:segment) { subject.payload[:segment] }
subject { described_class.new(params: params, current_user: admin).execute }
before do
enable_admin_mode!(admin)
end
it 'persists the segment' do
expect(subject).to be_success
expect(segment.name).to eq('my service')
expect(segment.groups).to eq([group])
end
context 'when user is not an admin' do
let(:user) { build(:user) }
subject { described_class.new(params: params, current_user: user).execute }
it 'does not persist the segment' do
expect(subject).to be_error
expect(subject.message).to eq('Forbidden')
expect(segment).not_to be_persisted
end
end
context 'when params are invalid' do
before do
params.delete(:name)
end
it 'does not persist the segment' do
expect(subject).to be_error
expect(segment.errors[:name]).not_to be_empty
end
end
context 'when groups are not given' do
before do
params.delete(:groups)
end
it 'persists the segment without groups' do
expect(subject).to be_success
expect(segment.segment_selections).to be_empty
end
end
context 'when duplicated groups are given' do
before do
params[:groups] = [group] * 5
end
it 'persists the segments with unique groups' do
expect(subject).to be_success
expect(segment.groups).to eq([group])
end
end
end
# frozen_string_literal: true
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(:other_group) { create(:group) }
let(:segment) { create(:devops_adoption_segment, name: 'segment', segment_selections: [build(:devops_adoption_segment_selection, :group, group: group)]) }
subject { described_class.new(segment: segment, current_user: user).execute }
before do
enable_admin_mode!(user)
end
it 'deletes the segment' do
expect(subject).to be_success
expect(segment).not_to be_persisted
end
context 'when deletion fails' 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')
end
end
context 'when the user is not admin' do
let(:user) { build(:user) }
it 'returns error response' do
expect(subject).to be_error
expect(subject.message).to eq('Forbidden')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::UpdateService do
include AdminModeHelper
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:other_group) { create(:group) }
let_it_be_with_refind(:segment) { create(:devops_adoption_segment, name: 'segment', segment_selections: [build(:devops_adoption_segment_selection, :group, group: group)]) }
let(:params) { { name: 'new name', groups: [group, other_group] } }
subject { described_class.new(segment: segment, params: params, current_user: admin).execute }
before do
enable_admin_mode!(admin)
end
it 'persists the segment' do
expect(subject).to be_success
expect(segment.name).to eq('new name')
expect(segment.groups).to eq([group, other_group])
end
context 'when user is not an admin' do
let(:user) { build(:user) }
subject { described_class.new(params: params, current_user: user).execute }
it 'does not persist the segment' do
expect(subject).to be_error
expect(subject.message).to eq('Forbidden')
end
end
context 'when params are invalid' do
before do
params[:name] = ''
end
it 'does not persist the segment' do
expect(subject).to be_error
expect(segment.errors[:name]).not_to be_empty
end
end
context 'when groups are not given' do
before do
params.delete(:groups)
end
it 'does not change the groups' do
expect(subject).to be_success
expect(segment.groups).to eq([group])
end
end
end
# frozen_string_literal: true
RSpec.shared_examples_for 'DevOps Adoption top level errors' do
context 'when the user is not an admin' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ADMIN_MESSAGE]
end
context 'when the feature is not available' do
let(:current_user) { admin }
before do
stub_licensed_features(instance_level_devops_adoption: false)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::FEATURE_UNAVAILABLE_MESSAGE]
end
end
......@@ -51,11 +51,13 @@ RSpec.describe Analytics::DevopsAdoption::SegmentSelection, type: :model do
context 'limit the number of segment selections' do
let_it_be(:segment) { create(:devops_adoption_segment) }
subject { build(:devops_adoption_segment_selection, segment: segment, project: project) }
subject { build(:devops_adoption_segment_selection, project: project, segment: segment) }
before do
create(:devops_adoption_segment_selection, :project, segment: segment)
segment.reload
stub_const("#{described_class}::ALLOWED_SELECTIONS_PER_SEGMENT", 1)
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