Commit 86395d32 authored by Markus Koller's avatar Markus Koller

Merge branch '262863-delete-oncall-rotation-graphql' into 'master'

Destroy On-call Rotation Mutation

See merge request gitlab-org/gitlab!51860
parents 82939e72 deb1702f
---
title: Add On-call Rotations destroy mutation to GraphQL
merge_request: 51860
author:
type: added
......@@ -15836,6 +15836,7 @@ type Mutation {
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallRotationCreate(input: OncallRotationCreateInput!): OncallRotationCreatePayload
oncallRotationDestroy(input: OncallRotationDestroyInput!): OncallRotationDestroyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
......@@ -16597,6 +16598,51 @@ input OncallRotationDateInputType {
time: String!
}
"""
Autogenerated input type of OncallRotationDestroy
"""
input OncallRotationDestroyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The ID of the on-call rotation to remove.
"""
id: IncidentManagementOncallRotationID!
"""
The project to remove the on-call schedule from.
"""
projectPath: ID!
"""
The IID of the on-call schedule to the on-call rotation belongs to.
"""
scheduleIid: String!
}
"""
Autogenerated return type of OncallRotationDestroy
"""
type OncallRotationDestroyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The on-call rotation.
"""
oncallRotation: IncidentManagementOncallRotation
}
"""
The rotation length of the on-call rotation
"""
......
......@@ -45793,6 +45793,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallRotationDestroy",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "OncallRotationDestroyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "OncallRotationDestroyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallScheduleCreate",
"description": null,
......@@ -49118,6 +49145,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OncallRotationDestroyInput",
"description": "Autogenerated input type of OncallRotationDestroy",
"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": "scheduleIid",
"description": "The IID of the on-call schedule to the on-call rotation belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "id",
"description": "The ID of the on-call rotation to remove.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "IncidentManagementOncallRotationID",
"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": "OncallRotationDestroyPayload",
"description": "Autogenerated return type of OncallRotationDestroy",
"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": "oncallRotation",
"description": "The on-call rotation.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "IncidentManagementOncallRotation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OncallRotationLengthInputType",
......@@ -2494,6 +2494,16 @@ Autogenerated return type of OncallRotationCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallRotation` | IncidentManagementOncallRotation | The on-call rotation. |
### OncallRotationDestroyPayload
Autogenerated return type of OncallRotationDestroy.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallRotation` | IncidentManagementOncallRotation | The on-call rotation. |
### OncallScheduleCreatePayload
Autogenerated return type of OncallScheduleCreate.
......
# frozen_string_literal: true
module IncidentManagement
class OncallRotationsFinder
def initialize(current_user, project, schedule, params = {})
@current_user = current_user
@project = project
@schedule = schedule
@params = params
end
def execute
return IncidentManagement::OncallRotation.none unless schedule && allowed?
collection = schedule.rotations
collection = by_id(collection)
collection
end
private
attr_reader :current_user, :schedule, :project, :params
def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end
def by_id(collection)
return collection unless params[:id]
collection.id_in(params[:id])
end
end
end
......@@ -58,6 +58,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Create
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Destroy
prepend(Types::DeprecatedMutations)
end
......
......@@ -19,6 +19,20 @@ module Mutations
errors: result.errors
}
end
def find_object(project_path:, schedule_iid:, **args)
project = Project.find_by_full_path(project_path)
return unless project
schedule = ::IncidentManagement::OncallSchedulesFinder.new(current_user, project, iid: schedule_iid).execute.first
return unless schedule
args = args.merge(id: args[:id].model_id)
::IncidentManagement::OncallRotationsFinder.new(current_user, project, schedule, args).execute.first
end
end
end
end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallRotation
class Destroy < Base
graphql_name 'OncallRotationDestroy'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to remove the on-call schedule from.'
argument :schedule_iid, GraphQL::STRING_TYPE,
required: true,
description: 'The IID of the on-call schedule to the on-call rotation belongs to.'
argument :id, Types::GlobalIDType[::IncidentManagement::OncallRotation],
required: true,
description: 'The ID of the on-call rotation to remove.'
def resolve(project_path:, schedule_iid:, id:)
oncall_rotation = authorized_find!(project_path: project_path, schedule_iid: schedule_iid, id: id)
response ::IncidentManagement::OncallRotations::DestroyService.new(
oncall_rotation,
current_user
).execute
end
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module OncallRotations
class DestroyService
# @param oncall_schedule [IncidentManagement::OncallRotation]
# @param user [User]
def initialize(oncall_rotation, user)
@oncall_rotation = oncall_rotation
@user = user
@project = oncall_rotation.project
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
if oncall_rotation.destroy
success
else
error(oncall_rotation.errors.full_messages.to_sentence)
end
end
private
attr_reader :oncall_rotation, :user, :project
def allowed?
user&.can?(:admin_incident_management_oncall_schedule, project)
end
def available?
::Gitlab::IncidentManagement.oncall_schedules_available?(project)
end
def error(message)
ServiceResponse.error(message: message)
end
def success
ServiceResponse.success(payload: { oncall_rotation: oncall_rotation })
end
def error_no_permissions
error(_('You have insufficient permissions to remove an on-call rotation from this project'))
end
def error_no_license
error(_('Your license does not support on-call rotations'))
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallRotationsFinder 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, project: project) }
let_it_be(:schedule_rotation_1) { create(:incident_management_oncall_rotation, schedule: oncall_schedule) }
let_it_be(:schedule_rotation_2) { create(:incident_management_oncall_rotation, schedule: oncall_schedule) }
let_it_be(:other_schedule_rotation) { create(:incident_management_oncall_rotation, schedule: another_oncall_schedule) }
let(:params) { {} }
describe '#execute' do
subject(:execute) { described_class.new(current_user, project, oncall_schedule, params).execute }
context 'when feature is available' do
before do
stub_licensed_features(oncall_schedules: true)
end
context 'when user has permissions' do
before_all do
project.add_maintainer(current_user)
end
it 'returns project on-call rotations' do
is_expected.to contain_exactly(schedule_rotation_1, schedule_rotation_2)
end
context 'when id given' do
let(:params) { { id: schedule_rotation_1.id } }
it 'returns an on-call rotation for id' do
is_expected.to contain_exactly(schedule_rotation_1)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to eq(IncidentManagement::OncallRotation.none) }
end
context 'schedule is nil' do
let(:oncall_schedule) { nil }
it { is_expected.to eq(IncidentManagement::OncallRotation.none) }
end
end
context 'when user has no permissions' do
it { is_expected.to eq(IncidentManagement::OncallRotation.none) }
end
end
context 'when feature is not available' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to eq(IncidentManagement::OncallRotation.none) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallRotation::Destroy do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:rotation) { create(:incident_management_oncall_rotation, schedule: schedule) }
let(:args) do
{
project_path: project.full_path,
schedule_iid: schedule.iid,
id: rotation.to_global_id
}
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
context 'user has access to project' do
before do
stub_licensed_features(oncall_schedules: true)
end
before_all do
project.add_maintainer(current_user)
end
context 'when OncallRotation::DestroyService responds with success' do
it 'returns the on-call rotation with no errors' do
expect(resolve).to match(
oncall_rotation: rotation,
errors: be_empty
)
end
it 'removes the rotation' do
resolve
expect { rotation.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when OncallRotations::DestroyService responds with an error' do
before do
allow_next_instance_of(::IncidentManagement::OncallRotations::DestroyService) do |service|
allow(service).to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_rotation: nil }, message: 'An error occurred'))
end
end
it 'returns errors' do
expect(resolve).to eq(
oncall_rotation: nil,
errors: ['An error occurred']
)
end
end
describe 'error cases' do
context 'project path incorrect' do
before do
args[:project_path] = "something/incorrect"
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'license disabled' do
before do
stub_licensed_features(oncall_schedules: false)
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
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
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallRotations::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!(:oncall_rotation) { create(:incident_management_oncall_rotation, schedule: oncall_schedule) }
let(:current_user) { user_with_permissions }
let(:params) { {} }
let(:service) { described_class.new(oncall_rotation, 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 rotation from this project'
end
context 'when the current_user does not have permissions to remove on-call rotations' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to remove an on-call rotation 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 rotations'
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 rotations'
end
context 'when an error occurs during removal' do
before do
allow(oncall_rotation).to receive(:destroy).and_return(false)
oncall_rotation.errors.add(:name, 'cannot be removed')
end
it_behaves_like 'error response', 'Name cannot be removed'
end
it 'successfully deletes and returns the rotation' do
expect(execute).to be_success
oncall_rotation_result = execute.payload[:oncall_rotation]
expect(oncall_rotation_result).to be_a(::IncidentManagement::OncallRotation)
expect(oncall_rotation_result.name).to eq(oncall_rotation.name)
expect { oncall_rotation_result.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
......@@ -32552,6 +32552,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 rotation from this project"
msgstr ""
msgid "You have insufficient permissions to remove an on-call schedule from this project"
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