Commit 8a081546 authored by charlie ablett's avatar charlie ablett

Merge branch '262857-creation-rotation-graphql' into 'master'

Add GraphQL mutation for adding oncall rotation

See merge request gitlab-org/gitlab!49206
parents fe0d76d1 730d2533
# frozen_string_literal: true
module Types
module DataVisualizationPalette
class ColorEnum < BaseEnum
graphql_name 'DataVisualizationColorEnum'
description 'Color of the data visualization palette'
Enums::DataVisualizationPalette.colors.keys.each do |unit|
value unit.upcase, value: unit, description: "#{unit.to_s.titleize} color"
end
end
end
end
# frozen_string_literal: true
module Types
module DataVisualizationPalette
class WeightEnum < BaseEnum
graphql_name 'DataVisualizationWeightEnum'
description 'Weight of the data visualization palette'
::Enums::DataVisualizationPalette.weights.keys.each do |unit|
value "weight_#{unit}".upcase, value: unit, description: "#{unit.to_s.titleize} weight"
end
end
end
end
---
title: Add GraphQL mutation to create on-call rotations
merge_request: 49206
author:
type: added
...@@ -5925,6 +5925,96 @@ enum DastSiteValidationStrategyEnum { ...@@ -5925,6 +5925,96 @@ enum DastSiteValidationStrategyEnum {
TEXT_FILE TEXT_FILE
} }
"""
Color of the data visualization palette
"""
enum DataVisualizationColorEnum {
"""
Aqua color
"""
AQUA
"""
Blue color
"""
BLUE
"""
Green color
"""
GREEN
"""
Magenta color
"""
MAGENTA
"""
Orange color
"""
ORANGE
}
"""
Weight of the data visualization palette
"""
enum DataVisualizationWeightEnum {
"""
100 weight
"""
WEIGHT_100
"""
200 weight
"""
WEIGHT_200
"""
300 weight
"""
WEIGHT_300
"""
400 weight
"""
WEIGHT_400
"""
50 weight
"""
WEIGHT_50
"""
500 weight
"""
WEIGHT_500
"""
600 weight
"""
WEIGHT_600
"""
700 weight
"""
WEIGHT_700
"""
800 weight
"""
WEIGHT_800
"""
900 weight
"""
WEIGHT_900
"""
950 weight
"""
WEIGHT_950
}
""" """
Date represented in ISO 8601 Date represented in ISO 8601
""" """
...@@ -11095,6 +11185,106 @@ An ISO 8601-encoded date ...@@ -11095,6 +11185,106 @@ An ISO 8601-encoded date
""" """
scalar ISO8601Date scalar ISO8601Date
"""
Identifier of IncidentManagement::OncallParticipant
"""
scalar IncidentManagementOncallParticipantID
"""
Describes an incident management on-call rotation
"""
type IncidentManagementOncallRotation {
"""
ID of the on-call rotation.
"""
id: IncidentManagementOncallRotationID!
"""
Length of the on-call schedule, in the units specified by lengthUnit.
"""
length: Int
"""
Unit of the on-call rotation length.
"""
lengthUnit: OncallRotationUnitEnum
"""
Name of the on-call rotation.
"""
name: String!
"""
Participants of the on-call rotation.
"""
participants(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): OncallParticipantTypeConnection
"""
Start date of the on-call rotation.
"""
startsAt: Time
}
"""
The connection type for IncidentManagementOncallRotation.
"""
type IncidentManagementOncallRotationConnection {
"""
A list of edges.
"""
edges: [IncidentManagementOncallRotationEdge]
"""
A list of nodes.
"""
nodes: [IncidentManagementOncallRotation]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type IncidentManagementOncallRotationEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: IncidentManagementOncallRotation
}
"""
Identifier of IncidentManagement::OncallRotation
"""
scalar IncidentManagementOncallRotationID
""" """
Describes an incident management on-call schedule Describes an incident management on-call schedule
""" """
...@@ -11114,6 +11304,31 @@ type IncidentManagementOncallSchedule { ...@@ -11114,6 +11304,31 @@ type IncidentManagementOncallSchedule {
""" """
name: String! name: String!
"""
On-call rotations for the on-call schedule
"""
rotations(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): IncidentManagementOncallRotationConnection!
""" """
Time zone of the on-call schedule Time zone of the on-call schedule
""" """
...@@ -15044,6 +15259,7 @@ type Mutation { ...@@ -15044,6 +15259,7 @@ type Mutation {
""" """
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallRotationCreate(input: OncallRotationCreateInput!): OncallRotationCreatePayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
...@@ -15654,6 +15870,176 @@ Identifier of Noteable ...@@ -15654,6 +15870,176 @@ Identifier of Noteable
""" """
scalar NoteableID scalar NoteableID
"""
The rotation participant and color palette
"""
type OncallParticipantType {
"""
The color palette to assign to the on-call user. For example "blue".
"""
colorPalette: String
"""
The color weight to assign to for the on-call user, for example "500". Max 4 chars. For easy identification of the user.
"""
colorWeight: String
"""
ID of the on-call participant.
"""
id: IncidentManagementOncallParticipantID!
"""
The user who is participating.
"""
user: User!
}
"""
The connection type for OncallParticipantType.
"""
type OncallParticipantTypeConnection {
"""
A list of edges.
"""
edges: [OncallParticipantTypeEdge]
"""
A list of nodes.
"""
nodes: [OncallParticipantType]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type OncallParticipantTypeEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: OncallParticipantType
}
"""
Autogenerated input type of OncallRotationCreate
"""
input OncallRotationCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The name of the on-call rotation.
"""
name: String!
"""
The usernames of users participating in the on-call rotation.
"""
participants: [OncallUserInputType!]!
"""
The project to create the on-call schedule in.
"""
projectPath: ID!
"""
The rotation length of the on-call rotation.
"""
rotationLength: OncallRotationLengthInputType!
"""
The IID of the on-call schedule to create the on-call rotation in.
"""
scheduleIid: String!
"""
The start date and time of the on-call rotation, in the timezone of the on-call schedule.
"""
startsAt: OncallRotationDateInputType!
}
"""
Autogenerated return type of OncallRotationCreate
"""
type OncallRotationCreatePayload {
"""
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
}
"""
Date input type for on-call rotation
"""
input OncallRotationDateInputType {
"""
The date component of the date in YYYY-MM-DD format.
"""
date: String!
"""
The time component of the date in 24hr HH:MM format.
"""
time: String!
}
"""
The rotation length of the on-call rotation
"""
input OncallRotationLengthInputType {
"""
The rotation length of the on-call rotation.
"""
length: Int!
"""
The unit of the rotation length of the on-call rotation.
"""
unit: OncallRotationUnitEnum!
}
"""
Rotation length unit of an on-call rotation
"""
enum OncallRotationUnitEnum {
"""
Days
"""
DAYS
"""
Hours
"""
HOURS
"""
Weeks
"""
WEEKS
}
""" """
Autogenerated input type of OncallScheduleCreate Autogenerated input type of OncallScheduleCreate
""" """
...@@ -15799,6 +16185,26 @@ type OncallScheduleUpdatePayload { ...@@ -15799,6 +16185,26 @@ type OncallScheduleUpdatePayload {
oncallSchedule: IncidentManagementOncallSchedule oncallSchedule: IncidentManagementOncallSchedule
} }
"""
The rotation user and color palette
"""
input OncallUserInputType {
"""
A value of DataVisualizationColorEnum. The color from the palette to assign to the on-call user.
"""
colorPalette: DataVisualizationColorEnum
"""
A value of DataVisualizationWeightEnum. The color weight to assign to for the on-call user.
"""
colorWeight: DataVisualizationWeightEnum
"""
The username of the user to participate in the on-call rotation, such as `user_one`.
"""
username: String!
}
""" """
Represents a package Represents a package
""" """
......
...@@ -1713,6 +1713,19 @@ Autogenerated return type of HttpIntegrationUpdate. ...@@ -1713,6 +1713,19 @@ Autogenerated return type of HttpIntegrationUpdate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `integration` | AlertManagementHttpIntegration | The HTTP integration | | `integration` | AlertManagementHttpIntegration | The HTTP integration |
### IncidentManagementOncallRotation
Describes an incident management on-call rotation.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `id` | IncidentManagementOncallRotationID! | ID of the on-call rotation. |
| `length` | Int | Length of the on-call schedule, in the units specified by lengthUnit. |
| `lengthUnit` | OncallRotationUnitEnum | Unit of the on-call rotation length. |
| `name` | String! | Name of the on-call rotation. |
| `participants` | OncallParticipantTypeConnection | Participants of the on-call rotation. |
| `startsAt` | Time | Start date of the on-call rotation. |
### IncidentManagementOncallSchedule ### IncidentManagementOncallSchedule
Describes an incident management on-call schedule. Describes an incident management on-call schedule.
...@@ -1722,6 +1735,7 @@ Describes an incident management on-call schedule. ...@@ -1722,6 +1735,7 @@ Describes an incident management on-call schedule.
| `description` | String | Description of the on-call schedule | | `description` | String | Description of the on-call schedule |
| `iid` | ID! | Internal ID of the on-call schedule | | `iid` | ID! | Internal ID of the on-call schedule |
| `name` | String! | Name of the on-call schedule | | `name` | String! | Name of the on-call schedule |
| `rotations` | IncidentManagementOncallRotationConnection! | On-call rotations for the on-call schedule |
| `timezone` | String! | Time zone of the on-call schedule | | `timezone` | String! | Time zone of the on-call schedule |
### InstanceSecurityDashboard ### InstanceSecurityDashboard
...@@ -2379,6 +2393,27 @@ Autogenerated return type of NamespaceIncreaseStorageTemporarily. ...@@ -2379,6 +2393,27 @@ Autogenerated return type of NamespaceIncreaseStorageTemporarily.
| `repositionNote` | Boolean! | Indicates the user can perform `reposition_note` on this resource | | `repositionNote` | Boolean! | Indicates the user can perform `reposition_note` on this resource |
| `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource | | `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource |
### OncallParticipantType
The rotation participant and color palette.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `colorPalette` | String | The color palette to assign to the on-call user. For example "blue". |
| `colorWeight` | String | The color weight to assign to for the on-call user, for example "500". Max 4 chars. For easy identification of the user. |
| `id` | IncidentManagementOncallParticipantID! | ID of the on-call participant. |
| `user` | User! | The user who is participating. |
### OncallRotationCreatePayload
Autogenerated return type of OncallRotationCreate.
| 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 ### OncallScheduleCreatePayload
Autogenerated return type of OncallScheduleCreate. Autogenerated return type of OncallScheduleCreate.
...@@ -4242,6 +4277,36 @@ Status of a container repository. ...@@ -4242,6 +4277,36 @@ Status of a container repository.
| `HEADER` | Header validation | | `HEADER` | Header validation |
| `TEXT_FILE` | Text file validation | | `TEXT_FILE` | Text file validation |
### DataVisualizationColorEnum
Color of the data visualization palette.
| Value | Description |
| ----- | ----------- |
| `AQUA` | Aqua color |
| `BLUE` | Blue color |
| `GREEN` | Green color |
| `MAGENTA` | Magenta color |
| `ORANGE` | Orange color |
### DataVisualizationWeightEnum
Weight of the data visualization palette.
| Value | Description |
| ----- | ----------- |
| `WEIGHT_100` | 100 weight |
| `WEIGHT_200` | 200 weight |
| `WEIGHT_300` | 300 weight |
| `WEIGHT_400` | 400 weight |
| `WEIGHT_50` | 50 weight |
| `WEIGHT_500` | 500 weight |
| `WEIGHT_600` | 600 weight |
| `WEIGHT_700` | 700 weight |
| `WEIGHT_800` | 800 weight |
| `WEIGHT_900` | 900 weight |
| `WEIGHT_950` | 950 weight |
### DesignCollectionCopyState ### DesignCollectionCopyState
Copy state of a DesignCollection. Copy state of a DesignCollection.
...@@ -4577,6 +4642,16 @@ Values for sorting projects. ...@@ -4577,6 +4642,16 @@ Values for sorting projects.
| `SIMILARITY` | Most similar to the search query | | `SIMILARITY` | Most similar to the search query |
| `STORAGE` | Sort by storage size | | `STORAGE` | Sort by storage size |
### OncallRotationUnitEnum
Rotation length unit of an on-call rotation.
| Value | Description |
| ----- | ----------- |
| `DAYS` | Days |
| `HOURS` | Hours |
| `WEEKS` | Weeks |
### PackageTypeEnum ### PackageTypeEnum
| Value | Description | | Value | Description |
......
...@@ -55,6 +55,7 @@ module EE ...@@ -55,6 +55,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Create
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
end end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallRotation
class Base < BaseMutation
field :oncall_rotation,
::Types::IncidentManagement::OncallRotationType,
null: true,
description: 'The on-call rotation.'
authorize :admin_incident_management_oncall_schedule
private
def response(result)
{
oncall_rotation: result.payload[:oncall_rotation],
errors: result.errors
}
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallRotation
class Create < Base
include ResolvesProject
graphql_name 'OncallRotationCreate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to create the on-call schedule in.'
argument :schedule_iid, GraphQL::STRING_TYPE,
required: true,
description: 'The IID of the on-call schedule to create the on-call rotation in.',
as: :iid
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the on-call rotation.'
argument :starts_at, Types::IncidentManagement::OncallRotationDateInputType,
required: true,
description: 'The start date and time of the on-call rotation, in the timezone of the on-call schedule.'
argument :rotation_length, Types::IncidentManagement::OncallRotationLengthInputType,
required: true,
description: 'The rotation length of the on-call rotation.'
argument :participants,
[Types::IncidentManagement::OncallUserInputType],
required: true,
description: 'The usernames of users participating in the on-call rotation.'
MAXIMUM_PARTICIPANTS = 100
def resolve(iid:, project_path:, participants:, **args)
project = Project.find_by_full_path(project_path)
raise_project_not_found unless project
schedule = ::IncidentManagement::OncallSchedulesFinder.new(current_user, project, iid: iid)
.execute
.first
raise_schedule_not_found unless schedule
result = ::IncidentManagement::OncallRotations::CreateService.new(
schedule,
project,
current_user,
create_service_params(schedule, participants, args)
).execute
response(result)
rescue ActiveRecord::RecordInvalid => e
raise Gitlab::Graphql::Errors::ArgumentError, e.message
end
private
def create_service_params(schedule, participants, args)
rotation_length = args[:rotation_length][:length]
rotation_length_unit = args[:rotation_length][:unit]
starts_at = parse_start_time(schedule, args)
args.slice(:name).merge(
length: rotation_length,
length_unit: rotation_length_unit,
starts_at: starts_at,
participants: find_participants(participants)
)
end
def parse_start_time(schedule, args)
args[:starts_at].asctime.in_time_zone(schedule.timezone)
end
def find_participants(user_array)
raise_too_many_users_error if user_array.size > MAXIMUM_PARTICIPANTS
usernames = user_array.map {|h| h[:username] }
raise_duplicate_users_error if usernames.size != usernames.uniq.size
matched_users = UsersFinder.new(current_user, username: usernames).execute.order_by(:username)
raise_user_not_found if matched_users.size != user_array.size
user_array = user_array.sort_by! { |h| h[:username] }
user_array.map.with_index { |param, i| param.to_h.merge(user: matched_users[i]) }
end
def raise_project_not_found
raise Gitlab::Graphql::Errors::ArgumentError, 'The project could not be found'
end
def raise_schedule_not_found
raise Gitlab::Graphql::Errors::ArgumentError, 'The schedule could not be found'
end
def raise_too_many_users_error
raise Gitlab::Graphql::Errors::ArgumentError, "A maximum of #{MAXIMUM_PARTICIPANTS} participants can be added"
end
def raise_duplicate_users_error
raise Gitlab::Graphql::Errors::ArgumentError, "A duplicate username is included in the participant list"
end
def raise_user_not_found
raise Gitlab::Graphql::Errors::ArgumentError, "A provided username couldn't be matched to a user"
end
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
# rubocop: disable Graphql/AuthorizeTypes
class OncallParticipantType < BaseObject
graphql_name 'OncallParticipantType'
description 'The rotation participant and color palette'
field :id,
Types::GlobalIDType[::IncidentManagement::OncallParticipant],
null: false,
description: 'ID of the on-call participant.'
field :user, Types::UserType,
null: false,
description: 'The user who is participating.'
field :color_palette, GraphQL::STRING_TYPE,
null: true,
description: 'The color palette to assign to the on-call user. For example "blue".'
field :color_weight, GraphQL::STRING_TYPE,
null: true,
description: 'The color weight to assign to for the on-call user, for example "500". Max 4 chars. For easy identification of the user.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
# rubocop: disable Graphql/AuthorizeTypes
class OncallRotationDateInputType < BaseInputObject
graphql_name 'OncallRotationDateInputType'
description 'Date input type for on-call rotation'
argument :date, GraphQL::STRING_TYPE,
required: true,
description: 'The date component of the date in YYYY-MM-DD format.'
argument :time, GraphQL::STRING_TYPE,
required: true,
description: 'The time component of the date in 24hr HH:MM format.'
DATE_FORMAT = %r[\d{4}-[0123]\d-\d{2}].freeze
TIME_FORMAT = %r[[012]\d:\d{2}].freeze
def prepare
raise Gitlab::Graphql::Errors::ArgumentError, 'Date given is invalid' unless DATE_FORMAT.match(date)
raise Gitlab::Graphql::Errors::ArgumentError, 'Time given is invalid' unless TIME_FORMAT.match(time)
Time.parse("#{date} #{time}")
rescue ArgumentError, TypeError
raise Gitlab::Graphql::Errors::ArgumentError, 'Date & time is invalid'
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
# rubocop: disable Graphql/AuthorizeTypes
class OncallRotationLengthInputType < BaseInputObject
graphql_name 'OncallRotationLengthInputType'
description 'The rotation length of the on-call rotation'
argument :length, GraphQL::INT_TYPE,
required: true,
description: 'The rotation length of the on-call rotation.'
argument :unit, Types::IncidentManagement::OncallRotationLengthUnitEnum,
required: true,
description: 'The unit of the rotation length of the on-call rotation.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class OncallRotationLengthUnitEnum < BaseEnum
graphql_name 'OncallRotationUnitEnum'
description 'Rotation length unit of an on-call rotation'
::IncidentManagement::OncallRotation.length_units.keys.each do |unit|
value unit.upcase, value: unit, description: "#{unit.titleize}"
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class OncallRotationType < BaseObject
graphql_name 'IncidentManagementOncallRotation'
description 'Describes an incident management on-call rotation'
authorize :read_incident_management_oncall_schedule
field :id,
Types::GlobalIDType[::IncidentManagement::OncallRotation],
null: false,
description: 'ID of the on-call rotation.'
field :name,
GraphQL::STRING_TYPE,
null: false,
description: 'Name of the on-call rotation.'
field :starts_at,
Types::TimeType,
null: true,
description: 'Start date of the on-call rotation.'
field :length,
GraphQL::INT_TYPE,
null: true,
description: 'Length of the on-call schedule, in the units specified by lengthUnit.'
field :length_unit,
Types::IncidentManagement::OncallRotationLengthUnitEnum,
null: true,
description: 'Unit of the on-call rotation length.'
field :participants,
::Types::IncidentManagement::OncallParticipantType.connection_type,
null: true,
description: 'Participants of the on-call rotation.'
end
end
end
...@@ -27,6 +27,11 @@ module Types ...@@ -27,6 +27,11 @@ module Types
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
null: false, null: false,
description: 'Time zone of the on-call schedule' description: 'Time zone of the on-call schedule'
field :rotations,
OncallRotationType.connection_type,
null: false,
description: 'On-call rotations for the on-call schedule'
end end
end end
end end
# frozen_string_literal: true
module Types
module IncidentManagement
# rubocop: disable Graphql/AuthorizeTypes
class OncallUserInputType < BaseInputObject
graphql_name 'OncallUserInputType'
description 'The rotation user and color palette'
argument :username, GraphQL::STRING_TYPE,
required: true,
description: 'The username of the user to participate in the on-call rotation, such as `user_one`.'
argument :color_palette, ::Types::DataVisualizationPalette::ColorEnum,
required: false,
description: 'A value of DataVisualizationColorEnum. The color from the palette to assign to the on-call user.'
argument :color_weight, ::Types::DataVisualizationPalette::WeightEnum,
required: false,
description: 'A value of DataVisualizationWeightEnum. The color weight to assign to for the on-call user.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallRotation::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) do
{
project_path: project.full_path,
name: 'On-call rotation',
schedule_iid: schedule.iid,
starts_at: "2020-01-10 09:00".in_time_zone(schedule.timezone),
rotation_length: {
length: 1,
unit: ::IncidentManagement::OncallRotation.length_units[:days]
},
participants: [
{
username: current_user.username,
color_weight: ::IncidentManagement::OncallParticipant.color_weights['50'],
color_palette: ::IncidentManagement::OncallParticipant.color_palettes[:blue]
}
]
}
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(iid: schedule.iid, project_path: project.full_path, participants: args[:participants], **args) }
context 'user has access to project' do
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
context 'when OncallRotation::CreateService responds with success' do
it 'returns the on-call rotation with no errors' do
expect(resolve).to match(
oncall_rotation: ::IncidentManagement::OncallRotation.last!,
errors: be_empty
)
end
end
context 'when OncallRotations::CreateService responds with an error' do
before do
allow_next_instance_of(::IncidentManagement::OncallRotations::CreateService) do |service|
allow(service).to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_rotation: nil }, message: 'An on-call rotation already exists'))
end
end
it 'returns errors' do
expect(resolve).to eq(
oncall_rotation: nil,
errors: ['An on-call rotation already exists']
)
end
end
describe 'error cases' do
context 'user cannot be found' do
before do
args.merge!(participants: [username: 'unknown'])
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "A provided username couldn't be matched to a user")
end
end
context 'user does not have access to the project' do
before do
other_user = create(:user)
args.merge!(
participants: [
{
username: other_user.username,
color_weight: ::IncidentManagement::OncallParticipant.color_weights['50'],
color_palette: ::IncidentManagement::OncallParticipant.color_palettes[:blue]
}
]
)
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /User does not have access to the project/)
end
end
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::ArgumentError, 'The project could not be found')
end
end
context 'duplicate participants' do
before do
args[:participants] << args[:participants].first
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'A duplicate username is included in the participant list')
end
end
context 'schedule does not exist' do
let(:schedule) { double(iid: non_existing_record_iid, timezone: 'UTC') }
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'The schedule could not be found')
end
end
context 'too many users' do
before do
stub_const('Mutations::IncidentManagement::OncallRotation::Create::MAXIMUM_PARTICIPANTS', 0)
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "A maximum of #{described_class::MAXIMUM_PARTICIPANTS} participants can be added")
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::ArgumentError, 'The schedule could not be found')
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 GitlabSchema.types['OncallParticipantType'] do
specify { expect(described_class.graphql_name).to eq('OncallParticipantType') }
it 'exposes the expected fields' do
expected_fields = %i[
id
user
color_palette
color_weight
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['IncidentManagementOncallRotation'] do
specify { expect(described_class.graphql_name).to eq('IncidentManagementOncallRotation') }
specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_oncall_schedule) }
it 'exposes the expected fields' do
expected_fields = %i[
id
name
starts_at
length
length_unit
participants
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
...@@ -13,6 +13,7 @@ RSpec.describe GitlabSchema.types['IncidentManagementOncallSchedule'] do ...@@ -13,6 +13,7 @@ RSpec.describe GitlabSchema.types['IncidentManagementOncallSchedule'] do
name name
description description
timezone timezone
rotations
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
......
# 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_it_be(:schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) do
{
project_path: project.full_path,
name: 'On-call rotation',
schedule_iid: schedule.iid.to_s,
starts_at: {
date: "2020-09-19",
time: "09:00"
},
rotation_length: {
length: 1,
unit: 'DAYS'
},
participants: [
{
username: current_user.username,
colorWeight: "WEIGHT_500",
colorPalette: "BLUE"
}
]
}
end
let(:mutation) do
graphql_mutation(:oncall_rotation_create, args) do
<<~QL
clientMutationId
errors
oncallRotation {
id
name
startsAt
length
lengthUnit
participants {
nodes {
user {
id
username
}
}
}
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_rotation_create) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
it 'creates a new on-call rotation', :aggregate_failures do
post_graphql_mutation(mutation, current_user: current_user)
new_oncall_rotation = ::IncidentManagement::OncallRotation.last!
oncall_rotation_response = mutation_response['oncallRotation']
expect(response).to have_gitlab_http_status(:success)
expect(oncall_rotation_response.slice(*%w[id name length lengthUnit])).to eq(
'id' => global_id_of(new_oncall_rotation),
'name' => args[:name],
'length' => 1,
'lengthUnit' => 'DAYS'
)
start_time = "#{args[:starts_at][:date]} #{args[:starts_at][:time]}".in_time_zone(schedule.timezone)
expect(Time.parse(oncall_rotation_response['startsAt'])).to eq(start_time)
expect(oncall_rotation_response.dig('participants', 'nodes')).to contain_exactly(
{
'user' => {
'id' => global_id_of(current_user),
'username' => current_user.username
}
}
)
end
%i[project_path schedule_iid name starts_at rotation_length participants].each do |argument|
context "without required argument #{argument}" do
before do
args.delete(argument)
end
it_behaves_like 'an invalid argument to the mutation', argument_name: argument
end
end
context 'time is invalid' do
before do
args[:starts_at][:time] = '999:999'
end
it 'returns the on-call rotation with errors' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).not_to be_empty
error = graphql_errors.first.dig('extensions', 'problems', 0, 'explanation')
expect(error).to match('Time given is invalid')
end
end
context 'date is invalid' do
before do
args[:starts_at][:date] = 'incorrect date'
end
it 'returns the on-call rotation with errors' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).not_to be_empty
error = graphql_errors.first.dig('extensions', 'problems', 0, 'explanation')
expect(error).to match('Date given is invalid')
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