Commit 5eab3096 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262850-delete-oncall-schedule-graphql' into 'master'

Add delete on-call schedule GraphQL mutation

See merge request gitlab-org/gitlab!48168
parents 44ae6a12 f4ae6d5d
......@@ -14057,6 +14057,7 @@ type Mutation {
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
......@@ -14684,6 +14685,46 @@ type OncallScheduleCreatePayload {
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
"""
......
......@@ -40867,6 +40867,33 @@
"isDeprecated": false,
"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",
"description": null,
......@@ -43617,6 +43644,122 @@
"enumValues": 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",
"name": "Package",
......@@ -2237,6 +2237,16 @@ Autogenerated return type of OncallScheduleCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `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
Represents a package.
......
......@@ -11,7 +11,10 @@ module IncidentManagement
def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed?
project.incident_management_oncall_schedules
collection = project.incident_management_oncall_schedules
collection = by_iid(collection)
collection
end
private
......@@ -26,5 +29,11 @@ module IncidentManagement
def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end
def by_iid(collection)
return collection unless params[:iid]
collection.for_iid(params[:iid])
end
end
end
......@@ -49,6 +49,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
prepend(Types::DeprecatedMutations)
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
errors: result.errors
}
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
......
......@@ -18,6 +18,8 @@ module IncidentManagement
validates :description, length: { maximum: DESCRIPTION_LENGTH }
validates :timezone, presence: true, inclusion: { in: :timezones }
scope :for_iid, -> (iid) { where(iid: iid) }
private
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 @@
FactoryBot.define do
factory :incident_management_oncall_schedule, class: 'IncidentManagement::OncallSchedule' do
project
name { 'Default On-call Schedule' }
sequence(:name) { |n| "On-call Schedule ##{n}" }
description { 'On-call description' }
timezone { 'Europe/Berlin' }
end
......
......@@ -6,10 +6,12 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:project) { create(: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
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
before do
......@@ -22,7 +24,15 @@ RSpec.describe IncidentManagement::OncallSchedulesFinder do
end
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
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
describe '.validations' do
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_length_of(:name).is_at_most(200) }
......@@ -22,7 +23,7 @@ RSpec.describe IncidentManagement::OncallSchedule do
context 'when the oncall schedule with the same name exists' do
before do
create(:incident_management_oncall_schedule, project: project)
create(:incident_management_oncall_schedule, project: project, name: name)
end
it 'has validation errors' do
......@@ -39,4 +40,13 @@ RSpec.describe IncidentManagement::OncallSchedule do
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :incident_management_oncall_schedules }
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
# 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 ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
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"
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