Commit f4ae6d5d authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add delete on-call schedule GraphQL mutation

Add a GraphQL mutation to remove on-call schedules
parent c0e4f65d
...@@ -14057,6 +14057,7 @@ type Mutation { ...@@ -14057,6 +14057,7 @@ type Mutation {
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
...@@ -14684,6 +14685,46 @@ type OncallScheduleCreatePayload { ...@@ -14684,6 +14685,46 @@ type OncallScheduleCreatePayload {
oncallSchedule: IncidentManagementOncallSchedule oncallSchedule: IncidentManagementOncallSchedule
} }
"""
Autogenerated input type of OncallScheduleDestroy
"""
input OncallScheduleDestroyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The on-call schedule internal ID to remove
"""
iid: String!
"""
The project to remove the on-call schedule from
"""
projectPath: ID!
}
"""
Autogenerated return type of OncallScheduleDestroy
"""
type OncallScheduleDestroyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The on-call schedule
"""
oncallSchedule: IncidentManagementOncallSchedule
}
""" """
Represents a package Represents a package
""" """
......
...@@ -40867,6 +40867,33 @@ ...@@ -40867,6 +40867,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "oncallScheduleDestroy",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "OncallScheduleDestroyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "OncallScheduleDestroyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "pipelineCancel", "name": "pipelineCancel",
"description": null, "description": null,
...@@ -43617,6 +43644,122 @@ ...@@ -43617,6 +43644,122 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "OncallScheduleDestroyInput",
"description": "Autogenerated input type of OncallScheduleDestroy",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to remove the on-call schedule from",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The on-call schedule internal ID to remove",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "OncallScheduleDestroyPayload",
"description": "Autogenerated return type of OncallScheduleDestroy",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallSchedule",
"description": "The on-call schedule",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "IncidentManagementOncallSchedule",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Package", "name": "Package",
...@@ -2237,6 +2237,16 @@ Autogenerated return type of OncallScheduleCreate. ...@@ -2237,6 +2237,16 @@ Autogenerated return type of OncallScheduleCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule | | `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule |
### OncallScheduleDestroyPayload
Autogenerated return type of OncallScheduleDestroy.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule |
### Package ### Package
Represents a package. Represents a package.
......
...@@ -11,7 +11,10 @@ module IncidentManagement ...@@ -11,7 +11,10 @@ module IncidentManagement
def execute def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed? return IncidentManagement::OncallSchedule.none unless available? && allowed?
project.incident_management_oncall_schedules collection = project.incident_management_oncall_schedules
collection = by_iid(collection)
collection
end end
private private
...@@ -26,5 +29,11 @@ module IncidentManagement ...@@ -26,5 +29,11 @@ module IncidentManagement
def allowed? def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project) Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end end
def by_iid(collection)
return collection unless params[:iid]
collection.for_iid(params[:iid])
end
end end
end end
...@@ -49,6 +49,7 @@ module EE ...@@ -49,6 +49,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
end end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class Destroy < OncallScheduleBase
graphql_name 'OncallScheduleDestroy'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to remove the on-call schedule from'
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: 'The on-call schedule internal ID to remove'
def resolve(project_path:, iid:)
oncall_schedule = authorized_find!(project_path: project_path, iid: iid)
response ::IncidentManagement::OncallSchedules::DestroyService.new(
oncall_schedule,
current_user
).execute
end
end
end
end
end
...@@ -19,6 +19,14 @@ module Mutations ...@@ -19,6 +19,14 @@ module Mutations
errors: result.errors errors: result.errors
} }
end end
def find_object(project_path:, **args)
project = Project.find_by_full_path(project_path)
return unless project
::IncidentManagement::OncallSchedulesFinder.new(current_user, project, args).execute.first
end
end end
end end
end end
......
...@@ -18,6 +18,8 @@ module IncidentManagement ...@@ -18,6 +18,8 @@ module IncidentManagement
validates :description, length: { maximum: DESCRIPTION_LENGTH } validates :description, length: { maximum: DESCRIPTION_LENGTH }
validates :timezone, presence: true, inclusion: { in: :timezones } validates :timezone, presence: true, inclusion: { in: :timezones }
scope :for_iid, -> (iid) { where(iid: iid) }
private private
def timezones def timezones
......
# frozen_string_literal: true
module IncidentManagement
module OncallSchedules
class DestroyService
# @param oncall_schedule [IncidentManagement::OncallSchedule]
# @param user [User]
def initialize(oncall_schedule, user)
@oncall_schedule = oncall_schedule
@user = user
@project = oncall_schedule.project
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
if oncall_schedule.destroy
success
else
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
private
attr_reader :oncall_schedule, :user, :project
def allowed?
user&.can?(:admin_incident_management_oncall_schedule, project)
end
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def error(message)
ServiceResponse.error(message: message)
end
def success
ServiceResponse.success(payload: { oncall_schedule: oncall_schedule })
end
def error_no_permissions
error(_('You have insufficient permissions to remove an on-call schedule from this project'))
end
def error_no_license
error(_('Your license does not support on-call schedules'))
end
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
FactoryBot.define do FactoryBot.define do
factory :incident_management_oncall_schedule, class: 'IncidentManagement::OncallSchedule' do factory :incident_management_oncall_schedule, class: 'IncidentManagement::OncallSchedule' do
project project
name { 'Default On-call Schedule' } sequence(:name) { |n| "On-call Schedule ##{n}" }
description { 'On-call description' } description { 'On-call description' }
timezone { 'Europe/Berlin' } timezone { 'Europe/Berlin' }
end end
......
...@@ -6,10 +6,12 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do ...@@ -6,10 +6,12 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:project) { create(:project) } let_it_be_with_refind(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) } let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:another_oncall_schedule) { create(:incident_management_oncall_schedule) } let_it_be(:another_oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:oncall_schedule_from_another_project) { create(:incident_management_oncall_schedule) }
let(:params) { {} }
describe '#execute' do describe '#execute' do
subject(:execute) { described_class.new(current_user, project).execute } subject(:execute) { described_class.new(current_user, project, params).execute }
context 'when feature is available' do context 'when feature is available' do
before do before do
...@@ -22,7 +24,15 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do ...@@ -22,7 +24,15 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do
end end
it 'returns project on-call schedules' do it 'returns project on-call schedules' do
is_expected.to contain_exactly(oncall_schedule) is_expected.to contain_exactly(oncall_schedule, another_oncall_schedule)
end
context 'when iid given' do
let(:params) { { iid: oncall_schedule.iid } }
it 'returns an on-call schedule for iid' do
is_expected.to contain_exactly(oncall_schedule)
end
end end
context 'when feature flag is disabled' do context 'when feature flag is disabled' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallSchedule::Destroy do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) { { project_path: project.full_path, iid: oncall_schedule.iid.to_s } }
specify { expect(described_class).to require_graphql_authorizations(:admin_incident_management_oncall_schedule) }
before do
stub_licensed_features(oncall_schedules: true)
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
context 'user has access to project' do
before do
project.add_maintainer(current_user)
end
context 'when OncallSchedules::DestroyService responds with success' do
it 'returns the on-call schedule with no errors' do
expect(resolve).to eq(
oncall_schedule: oncall_schedule,
errors: []
)
end
end
context 'when OncallSchedules::DestroyService responds with an error' do
before do
allow_any_instance_of(::IncidentManagement::OncallSchedules::DestroyService)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_schedule: nil }, message: 'An error has occurred'))
end
it 'returns errors' do
expect(resolve).to eq(
oncall_schedule: nil,
errors: ['An error has occurred']
)
end
end
end
context 'when resource is not accessible to the user' do
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
...@@ -11,8 +11,9 @@ RSpec.describe IncidentManagement::OncallSchedule do ...@@ -11,8 +11,9 @@ RSpec.describe IncidentManagement::OncallSchedule do
describe '.validations' do describe '.validations' do
let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } } let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } }
let(:name) { 'Default on-call schedule' }
subject { build(:incident_management_oncall_schedule, project: project) } subject { build(:incident_management_oncall_schedule, project: project, name: name) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(200) } it { is_expected.to validate_length_of(:name).is_at_most(200) }
...@@ -22,7 +23,7 @@ RSpec.describe IncidentManagement::OncallSchedule do ...@@ -22,7 +23,7 @@ RSpec.describe IncidentManagement::OncallSchedule do
context 'when the oncall schedule with the same name exists' do context 'when the oncall schedule with the same name exists' do
before do before do
create(:incident_management_oncall_schedule, project: project) create(:incident_management_oncall_schedule, project: project, name: name)
end end
it 'has validation errors' do it 'has validation errors' do
...@@ -39,4 +40,13 @@ RSpec.describe IncidentManagement::OncallSchedule do ...@@ -39,4 +40,13 @@ RSpec.describe IncidentManagement::OncallSchedule do
let(:scope_attrs) { { project: instance.project } } let(:scope_attrs) { { project: instance.project } }
let(:usage) { :incident_management_oncall_schedules } let(:usage) { :incident_management_oncall_schedules }
end end
describe '.for_iid' do
let_it_be(:oncall_schedule1) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:oncall_schedule2) { create(:incident_management_oncall_schedule, project: project) }
it 'returns only records with that IID' do
expect(described_class.for_iid(oncall_schedule1.iid)).to contain_exactly(oncall_schedule1)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Removing an on-call schedule' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:variables) { { project_path: project.full_path, iid: oncall_schedule.iid.to_s } }
let(:mutation) do
graphql_mutation(:oncall_schedule_destroy, variables) do
<<~QL
clientMutationId
errors
oncallSchedule {
iid
name
description
timezone
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_schedule_destroy) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user)
end
it 'removes the on-call schedule' do
post_graphql_mutation(mutation, current_user: user)
oncall_schedule_response = mutation_response['oncallSchedule']
expect(response).to have_gitlab_http_status(:success)
expect(oncall_schedule_response.slice(*%w[iid name description timezone])).to eq(
'iid' => oncall_schedule.iid.to_s,
'name' => oncall_schedule.name,
'description' => oncall_schedule.description,
'timezone' => oncall_schedule.timezone
)
expect { oncall_schedule.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedules::DestroyService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_refind(:project) { create(:project) }
let!(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:current_user) { user_with_permissions }
let(:params) { {} }
let(:service) { described_class.new(oncall_schedule, current_user) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user_with_permissions)
end
describe '#execute' do
shared_examples 'error response' do |message|
it 'has an informative message' do
expect(execute).to be_error
expect(execute.message).to eq(message)
end
end
subject(:execute) { service.execute }
context 'when the current_user is anonymous' do
let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to remove an on-call schedule from this project'
end
context 'when the current_user does not have permissions to remove on-call schedules' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to remove an on-call schedule from this project'
end
context 'when feature is not available' do
before do
stub_licensed_features(oncall_schedules: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when an error occurs during removal' do
before do
allow(oncall_schedule).to receive(:destroy).and_return(false)
oncall_schedule.errors.add(:name, 'cannot be removed')
end
it_behaves_like 'error response', 'Name cannot be removed'
end
it 'successfully returns the integration' do
expect(execute).to be_success
oncall_schedule_result = execute.payload[:oncall_schedule]
expect(oncall_schedule_result).to be_a(::IncidentManagement::OncallSchedule)
expect(oncall_schedule_result.name).to eq(oncall_schedule.name)
expect(oncall_schedule_result.description).to eq(oncall_schedule.description)
expect(oncall_schedule_result.timezone).to eq(oncall_schedule.timezone)
end
end
end
...@@ -31242,6 +31242,9 @@ msgstr "" ...@@ -31242,6 +31242,9 @@ msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project" msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to remove an on-call schedule from this project"
msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration" msgid "You have insufficient permissions to remove this HTTP integration"
msgstr "" msgstr ""
......
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