Commit 599047fe authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add OncallScheduleCreate GraphQL mutation

Add a GraphQL mutation to create on-call schedules for a project
parent 85f370f9
......@@ -13950,6 +13950,7 @@ type Mutation {
"""
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
......@@ -14526,6 +14527,56 @@ Identifier of Noteable
"""
scalar NoteableID
"""
Autogenerated input type of OncallScheduleCreate
"""
input OncallScheduleCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The description of the on-call schedule
"""
description: String
"""
The name of the on-call schedule
"""
name: String!
"""
The project to create the on-call schedule in
"""
projectPath: ID!
"""
The timezone of the on-call schedule
"""
timezone: String!
}
"""
Autogenerated return type of OncallScheduleCreate
"""
type OncallScheduleCreatePayload {
"""
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
"""
......
......@@ -40443,6 +40443,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallScheduleCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "OncallScheduleCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "OncallScheduleCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipelineCancel",
"description": null,
......@@ -43026,6 +43053,146 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OncallScheduleCreateInput",
"description": "Autogenerated input type of OncallScheduleCreate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to create the on-call schedule in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "The name of the on-call schedule",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "description",
"description": "The description of the on-call schedule",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "timezone",
"description": "The timezone of the on-call schedule",
"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": "OncallScheduleCreatePayload",
"description": "Autogenerated return type of OncallScheduleCreate",
"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",
......@@ -2218,6 +2218,16 @@ Autogenerated return type of NamespaceIncreaseStorageTemporarily.
| `repositionNote` | Boolean! | Indicates the user can perform `reposition_note` on this resource |
| `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource |
### OncallScheduleCreatePayload
Autogenerated return type of OncallScheduleCreate.
| 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.
......
......@@ -48,6 +48,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
prepend(Types::DeprecatedMutations)
end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class Create < OncallScheduleBase
include ResolvesProject
graphql_name 'OncallScheduleCreate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to create the on-call schedule in'
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the on-call schedule'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'The description of the on-call schedule'
argument :timezone, GraphQL::STRING_TYPE,
required: true,
description: 'The timezone of the on-call schedule'
def resolve(args)
project = authorized_find!(full_path: args[:project_path])
response ::IncidentManagement::OncallSchedules::CreateService.new(
project,
current_user,
args.slice(:name, :description, :timezone)
).execute
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class OncallScheduleBase < BaseMutation
field :oncall_schedule,
::Types::IncidentManagement::OncallScheduleType,
null: true,
description: 'The on-call schedule'
authorize :modify_incident_management_oncall_schedule
private
def response(result)
{
oncall_schedule: result.payload[:oncall_schedule],
errors: result.errors
}
end
end
end
end
end
......@@ -10,7 +10,6 @@ module Resolvers
def resolve(**args)
return [] unless Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
project.incident_management_oncall_schedules
end
end
......
......@@ -14,7 +14,7 @@ module IncidentManagement
has_internal_id :iid, scope: :project
validates :name, presence: true, length: { maximum: NAME_LENGTH }
validates :name, presence: true, uniqueness: { scope: :project }, length: { maximum: NAME_LENGTH }
validates :description, length: { maximum: DESCRIPTION_LENGTH }
validates :timezone, presence: true, inclusion: { in: :timezones }
......
......@@ -242,6 +242,7 @@ module EE
enable :modify_merge_request_author_setting
enable :modify_merge_request_committer_setting
enable :read_incident_management_oncall_schedule
enable :modify_incident_management_oncall_schedule
end
rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy
......
# frozen_string_literal: true
module IncidentManagement
module OncallSchedules
class CreateService
# @param project [Project]
# @param user [User]
# @param params [Hash]
def initialize(project, user, params)
@project = project
@user = user
@params = params
end
def execute
return error_no_permissions unless allowed?
oncall_schedule = project.incident_management_oncall_schedules.create(params)
return error_in_create(oncall_schedule) unless oncall_schedule.persisted?
success(oncall_schedule)
end
private
attr_reader :project, :user, :params
def allowed?
user&.can?(:modify_incident_management_oncall_schedule, project)
end
def error(message)
ServiceResponse.error(message: message)
end
def success(oncall_schedule)
ServiceResponse.success(payload: { oncall_schedule: oncall_schedule })
end
def error_no_permissions
error(_('You have insufficient permissions to create an on-call schedule for this project'))
end
def error_in_create(oncall_schedule)
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallSchedule::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:args) do
{
project_path: project.full_path,
name: 'On-call schedule',
description: 'On-call schedule description',
timezone: 'Europe/Berlin'
}
end
specify { expect(described_class).to require_graphql_authorizations(:modify_incident_management_oncall_schedule) }
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::CreateService responds with success' do
it 'returns the on-call schedule with no errors' do
expect(resolve).to eq(
oncall_schedule: ::IncidentManagement::OncallSchedule.last,
errors: []
)
end
end
context 'when OncallSchedules::CreateService responds with an error' do
before do
allow_any_instance_of(::IncidentManagement::OncallSchedules::CreateService)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_schedule: nil }, message: 'An on-call schedule already exists'))
end
it 'returns errors' do
expect(resolve).to eq(
oncall_schedule: nil,
errors: ['An on-call schedule already exists']
)
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 'Creating a new on-call schedule' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:variables) do
{
project_path: project.full_path,
name: 'New on-call schedule',
description: 'on-call schedule description',
timezone: 'Europe/Berlin'
}
end
let(:mutation) do
graphql_mutation(:oncall_schedule_create, variables) do
<<~QL
clientMutationId
errors
oncallSchedule {
iid
name
description
timezone
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_schedule_create) }
before do
project.add_maintainer(current_user)
end
it 'create a new on-call schedule' do
post_graphql_mutation(mutation, current_user: current_user)
new_oncall_schedule = ::IncidentManagement::OncallSchedule.last!
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' => new_oncall_schedule.iid.to_s,
'name' => 'New on-call schedule',
'description' => 'on-call schedule description',
'timezone' => 'Europe/Berlin'
)
end
%i[project_path name timezone].each do |argument|
context "without required argument #{argument}" do
before do
variables.delete(argument)
end
it_behaves_like 'an invalid argument to the mutation', argument_name: argument
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedules::CreateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_reload(:project) { create(:project) }
let(:current_user) { user_with_permissions }
let(:params) { { name: 'On-call schedule', description: 'On-call schedule description', timezone: 'Europe/Berlin' } }
let(:service) { described_class.new(project, current_user, params) }
before do
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 create an on-call schedule for this project'
end
context 'when the current_user does not have permissions to create on-call schedules' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to create an on-call schedule for this project'
end
context 'when an on-call schedule already exists' do
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project, name: 'On-call schedule') }
it_behaves_like 'error response', 'Name has already been taken'
end
context 'with valid params' do
it 'successfully creates an on-call schedule' do
expect(execute).to be_success
oncall_schedule = execute.payload[:oncall_schedule]
expect(oncall_schedule).to be_a(::IncidentManagement::OncallSchedule)
expect(oncall_schedule.name).to eq('On-call schedule')
expect(oncall_schedule.description).to eq('On-call schedule description')
expect(oncall_schedule.timezone).to eq('Europe/Berlin')
end
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