Commit eb314867 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262849-update-oncall-schedules-graphql' into 'master'

Add update on-call schedules GraphQL mutation

See merge request gitlab-org/gitlab!48292
parents 45a450aa 92da3222
...@@ -14144,6 +14144,7 @@ type Mutation { ...@@ -14144,6 +14144,7 @@ type Mutation {
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
...@@ -14838,6 +14839,61 @@ type OncallScheduleDestroyPayload { ...@@ -14838,6 +14839,61 @@ type OncallScheduleDestroyPayload {
oncallSchedule: IncidentManagementOncallSchedule oncallSchedule: IncidentManagementOncallSchedule
} }
"""
Autogenerated input type of OncallScheduleUpdate
"""
input OncallScheduleUpdateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The description of the on-call schedule
"""
description: String
"""
The on-call schedule internal ID to update
"""
iid: String!
"""
The name of the on-call schedule
"""
name: String
"""
The project to update the on-call schedule in
"""
projectPath: ID!
"""
The timezone of the on-call schedule
"""
timezone: String
}
"""
Autogenerated return type of OncallScheduleUpdate
"""
type OncallScheduleUpdatePayload {
"""
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
""" """
......
...@@ -41158,6 +41158,33 @@ ...@@ -41158,6 +41158,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "oncallScheduleUpdate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "OncallScheduleUpdateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "OncallScheduleUpdatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "pipelineCancel", "name": "pipelineCancel",
"description": null, "description": null,
...@@ -44104,6 +44131,152 @@ ...@@ -44104,6 +44131,152 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "OncallScheduleUpdateInput",
"description": "Autogenerated input type of OncallScheduleUpdate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to update the on-call schedule in",
"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 update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "The name of the on-call schedule",
"type": {
"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": "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": "OncallScheduleUpdatePayload",
"description": "Autogenerated return type of OncallScheduleUpdate",
"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",
...@@ -2272,6 +2272,16 @@ Autogenerated return type of OncallScheduleDestroy. ...@@ -2272,6 +2272,16 @@ Autogenerated return type of OncallScheduleDestroy.
| `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 |
### OncallScheduleUpdatePayload
Autogenerated return type of OncallScheduleUpdate.
| 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.
......
...@@ -9,7 +9,7 @@ module IncidentManagement ...@@ -9,7 +9,7 @@ module IncidentManagement
end end
def execute def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed? return IncidentManagement::OncallSchedule.none unless allowed?
collection = project.incident_management_oncall_schedules collection = project.incident_management_oncall_schedules
collection = by_iid(collection) collection = by_iid(collection)
...@@ -21,11 +21,6 @@ module IncidentManagement ...@@ -21,11 +21,6 @@ module IncidentManagement
attr_reader :current_user, :project, :params attr_reader :current_user, :project, :params
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
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
......
...@@ -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::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class Update < OncallScheduleBase
graphql_name 'OncallScheduleUpdate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to update the on-call schedule in'
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: 'The on-call schedule internal ID to update'
argument :name, GraphQL::STRING_TYPE,
required: false,
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: false,
description: 'The timezone of the on-call schedule'
def resolve(args)
oncall_schedule = authorized_find!(project_path: args[:project_path], iid: args[:iid])
response ::IncidentManagement::OncallSchedules::UpdateService.new(
oncall_schedule,
current_user,
args.slice(:name, :description, :timezone)
).execute
end
end
end
end
end
...@@ -155,8 +155,7 @@ module EE ...@@ -155,8 +155,7 @@ module EE
with_scope :subject with_scope :subject
condition(:oncall_schedules_available) do condition(:oncall_schedules_available) do
::Feature.enabled?(:oncall_schedules_mvc, @subject) && ::Gitlab::IncidentManagement.oncall_schedules_available?(@subject)
@subject.feature_available?(:oncall_schedules)
end end
rule { visual_review_bot }.policy do rule { visual_review_bot }.policy do
......
...@@ -31,8 +31,7 @@ module IncidentManagement ...@@ -31,8 +31,7 @@ module IncidentManagement
end end
def available? def available?
Feature.enabled?(:oncall_schedules_mvc, project) && ::Gitlab::IncidentManagement.oncall_schedules_available?(project)
project.feature_available?(:oncall_schedules)
end end
def error(message) def error(message)
......
...@@ -31,8 +31,7 @@ module IncidentManagement ...@@ -31,8 +31,7 @@ module IncidentManagement
end end
def available? def available?
Feature.enabled?(:oncall_schedules_mvc, project) && ::Gitlab::IncidentManagement.oncall_schedules_available?(project)
project.feature_available?(:oncall_schedules)
end end
def error(message) def error(message)
......
# frozen_string_literal: true
module IncidentManagement
module OncallSchedules
class UpdateService
# @param oncall_schedule [IncidentManagement::OncallSchedule]
# @param user [User]
# @param params [Hash]
def initialize(oncall_schedule, user, params)
@oncall_schedule = oncall_schedule
@user = user
@params = params
@project = oncall_schedule.project
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
if oncall_schedule.update(params)
success(oncall_schedule)
else
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
private
attr_reader :oncall_schedule, :user, :params, :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(oncall_schedule)
ServiceResponse.success(payload: { oncall_schedule: oncall_schedule })
end
def error_no_permissions
error(_('You have insufficient permissions to update an on-call schedule for this project'))
end
def error_no_license
error(_('Your license does not support on-call schedules'))
end
end
end
end
# frozen_string_literal: true
module Gitlab
module IncidentManagement
def self.oncall_schedules_available?(project)
::Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallSchedule::Update do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) do
{
project_path: project.full_path,
iid: oncall_schedule.iid.to_s,
name: 'Updated name',
description: 'Updated description',
timezone: 'America/New_York'
}
end
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::UpdateService 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::UpdateService responds with an error' do
before do
allow_any_instance_of(::IncidentManagement::OncallSchedules::UpdateService)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_schedule: nil }, message: 'Name has already been taken'))
end
it 'returns errors' do
expect(resolve).to eq(
oncall_schedule: nil,
errors: ['Name has already been taken']
)
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 Gitlab::IncidentManagement do
let_it_be_with_refind(:project) { create(:project) }
describe '.oncall_schedules_available?' do
subject { described_class.oncall_schedules_available?(project) }
before do
stub_licensed_features(oncall_schedules: true)
stub_feature_flags(oncall_schedules_mvc: project)
end
it { is_expected.to be_truthy }
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to be_falsey }
end
context 'when there is no license' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to be_falsey }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Updating 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) do
{
project_path: project.full_path,
iid: oncall_schedule.iid.to_s,
name: 'Updated name',
description: 'Updated description',
timezone: 'America/New_York'
}
end
let(:mutation) do
graphql_mutation(:oncall_schedule_update, variables) do
<<~QL
clientMutationId
errors
oncallSchedule {
iid
name
description
timezone
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_schedule_update) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user)
end
it 'updates 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' => variables[:name],
'description' => variables[:description],
'timezone' => variables[:timezone]
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedules::UpdateService 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_it_be_with_reload(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:current_user) { user_with_permissions }
let(:params) { { name: 'Updated name', description: 'Updated description', timezone: 'America/New_York' } }
let(:service) { described_class.new(oncall_schedule, current_user, params) }
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 update an on-call schedule for this project'
end
context 'when the current_user does not have permissions to update on-call schedules' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to update an on-call schedule for 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 on-call schedule witht the same name already exists' do
before do
create(:incident_management_oncall_schedule, project: project, name: params[:name])
end
it_behaves_like 'error response', 'Name has already been taken'
end
context 'with valid params' do
it 'successfully creates an on-call schedule' do
response = execute
payload = response.payload
oncall_schedule.reload
expect(response).to be_success
expect(payload[:oncall_schedule]).to eq(oncall_schedule)
expect(oncall_schedule).to be_a(::IncidentManagement::OncallSchedule)
expect(oncall_schedule.name).to eq(params[:name])
expect(oncall_schedule.description).to eq(params[:description])
expect(oncall_schedule.timezone).to eq(params[:timezone])
end
end
end
end
...@@ -31432,6 +31432,9 @@ msgstr "" ...@@ -31432,6 +31432,9 @@ msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration" msgid "You have insufficient permissions to remove this HTTP integration"
msgstr "" msgstr ""
msgid "You have insufficient permissions to update an on-call schedule for this project"
msgstr ""
msgid "You have insufficient permissions to update this HTTP integration" msgid "You have insufficient permissions to update 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