Commit 6d313255 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262847-create-on-call-schedules' into 'master'

Create On-call schedules via GraphQL

See merge request gitlab-org/gitlab!47407
parents 04fabe2a b7cf298e
......@@ -15,7 +15,8 @@ module Enums
operations_user_lists: 7,
alert_management_alerts: 8,
sprints: 9, # iterations
design_management_designs: 10
design_management_designs: 10,
incident_management_oncall_schedules: 11
}
end
end
......
---
title: Create `incident_management_oncall_schedules` table
merge_request: 47407
author:
type: added
# frozen_string_literal: true
class CreateIncidentManagementOncallSchedules < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
unless table_exists?(:incident_management_oncall_schedules)
create_table :incident_management_oncall_schedules do |t|
t.timestamps_with_timezone
t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
t.integer :iid, null: false
t.text :name, null: false
t.text :description
t.text :timezone
t.index %w(project_id iid), name: 'index_im_oncall_schedules_on_project_id_and_iid', unique: true, using: :btree
end
end
end
add_text_limit :incident_management_oncall_schedules, :name, 200
add_text_limit :incident_management_oncall_schedules, :description, 1000
add_text_limit :incident_management_oncall_schedules, :timezone, 100
end
def down
with_lock_retries do
drop_table :incident_management_oncall_schedules
end
end
end
0efb2dcfc65da007a67a15857d0a283dad301650f999a4227aa54ea00dca24bf
\ No newline at end of file
......@@ -12858,6 +12858,29 @@ CREATE SEQUENCE import_failures_id_seq
ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
CREATE TABLE incident_management_oncall_schedules (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
iid integer NOT NULL,
name text NOT NULL,
description text,
timezone text,
CONSTRAINT check_7ed1fd5aa7 CHECK ((char_length(description) <= 1000)),
CONSTRAINT check_cc77cbb103 CHECK ((char_length(timezone) <= 100)),
CONSTRAINT check_e6ef43a664 CHECK ((char_length(name) <= 200))
);
CREATE SEQUENCE incident_management_oncall_schedules_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_oncall_schedules_id_seq OWNED BY incident_management_oncall_schedules.id;
CREATE TABLE index_statuses (
id integer NOT NULL,
project_id integer NOT NULL,
......@@ -18031,6 +18054,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_schedules ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_schedules_id_seq'::regclass);
ALTER TABLE ONLY index_statuses ALTER COLUMN id SET DEFAULT nextval('index_statuses_id_seq'::regclass);
ALTER TABLE ONLY insights ALTER COLUMN id SET DEFAULT nextval('insights_id_seq'::regclass);
......@@ -19215,6 +19240,9 @@ ALTER TABLE ONLY import_export_uploads
ALTER TABLE ONLY import_failures
ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_schedules
ADD CONSTRAINT incident_management_oncall_schedules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY index_statuses
ADD CONSTRAINT index_statuses_pkey PRIMARY KEY (id);
......@@ -21099,6 +21127,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam
CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id);
CREATE UNIQUE INDEX index_im_oncall_schedules_on_project_id_and_iid ON incident_management_oncall_schedules USING btree (project_id, iid);
CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL);
CREATE INDEX index_import_export_uploads_on_project_id ON import_export_uploads USING btree (project_id);
......@@ -21115,6 +21145,8 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL);
CREATE INDEX index_incident_management_oncall_schedules_on_project_id ON incident_management_oncall_schedules USING btree (project_id);
CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id);
CREATE INDEX index_insights_on_namespace_id ON insights USING btree (namespace_id);
......@@ -23587,6 +23619,9 @@ ALTER TABLE ONLY open_project_tracker_data
ALTER TABLE ONLY gpg_signatures
ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES gpg_key_subkeys(id) ON DELETE SET NULL;
ALTER TABLE ONLY incident_management_oncall_schedules
ADD CONSTRAINT fk_rails_19e83fdd65 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_user_mentions
ADD CONSTRAINT fk_rails_1a41c485cd FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
......
......@@ -10134,6 +10134,66 @@ An ISO 8601-encoded date
"""
scalar ISO8601Date
"""
Describes an incident management on-call schedule
"""
type IncidentManagementOncallSchedule {
"""
Description of the on-call schedule
"""
description: String
"""
Internal ID of the on-call schedule
"""
iid: ID!
"""
Name of the on-call schedule
"""
name: String!
"""
Time zone of the on-call schedule
"""
timezone: String!
}
"""
The connection type for IncidentManagementOncallSchedule.
"""
type IncidentManagementOncallScheduleConnection {
"""
A list of edges.
"""
edges: [IncidentManagementOncallScheduleEdge]
"""
A list of nodes.
"""
nodes: [IncidentManagementOncallSchedule]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type IncidentManagementOncallScheduleEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: IncidentManagementOncallSchedule
}
type InstanceSecurityDashboard {
"""
Projects selected in Instance Security Dashboard
......@@ -13890,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
......@@ -14466,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
"""
......@@ -15537,6 +15648,31 @@ type Project {
"""
importStatus: String
"""
Incident Management On-call schedules of the project
"""
incidentManagementOncallSchedules(
"""
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
): IncidentManagementOncallScheduleConnection
"""
A single issue of the project
"""
......
......@@ -1563,6 +1563,17 @@ Autogenerated return type of HttpIntegrationUpdate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `integration` | AlertManagementHttpIntegration | The HTTP integration |
### IncidentManagementOncallSchedule
Describes an incident management on-call schedule.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `description` | String | Description of the on-call schedule |
| `iid` | ID! | Internal ID of the on-call schedule |
| `name` | String! | Name of the on-call schedule |
| `timezone` | String! | Time zone of the on-call schedule |
### InstanceSecurityDashboard
| Field | Type | Description |
......@@ -2207,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.
......@@ -2351,6 +2372,7 @@ Autogenerated return type of PipelineRetry.
| `httpUrlToRepo` | String | URL to connect to the project via HTTPS |
| `id` | ID! | ID of the project |
| `importStatus` | String | Status of import background job of the project |
| `incidentManagementOncallSchedules` | IncidentManagementOncallScheduleConnection | Incident Management On-call schedules of the project |
| `issue` | Issue | A single issue of the project |
| `issueStatusCounts` | IssueStatusCountsType | Counts of issues by status for the project |
| `issues` | IssueConnection | Issues of the project |
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedulesFinder
def initialize(current_user, project, params = {})
@current_user = current_user
@project = project
@params = params
end
def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed?
project.incident_management_oncall_schedules
end
private
attr_reader :current_user, :project, :params
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end
end
end
......@@ -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
......
......@@ -148,6 +148,12 @@ module EE
description: 'Code coverage summary associated with the project',
resolver: ::Resolvers::Ci::CodeCoverageSummaryResolver
field :incident_management_oncall_schedules,
::Types::IncidentManagement::OncallScheduleType.connection_type,
null: true,
description: 'Incident Management On-call schedules of the project',
resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver
def self.sast_ci_configuration(project)
::Security::CiConfiguration::SastParserService.new(project).configuration
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 :admin_incident_management_oncall_schedule
private
def response(result)
{
oncall_schedule: result.payload[:oncall_schedule],
errors: result.errors
}
end
end
end
end
end
# frozen_string_literal: true
module Resolvers
module IncidentManagement
class OncallScheduleResolver < BaseResolver
alias_method :project, :synchronized_object
type Types::IncidentManagement::OncallScheduleType.connection_type, null: true
def resolve(**args)
::IncidentManagement::OncallSchedulesFinder.new(context[:current_user], project).execute
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class OncallScheduleType < BaseObject
graphql_name 'IncidentManagementOncallSchedule'
description 'Describes an incident management on-call schedule'
authorize :read_incident_management_oncall_schedule
field :iid,
GraphQL::ID_TYPE,
null: false,
description: 'Internal ID of the on-call schedule'
field :name,
GraphQL::STRING_TYPE,
null: false,
description: 'Name of the on-call schedule'
field :description,
GraphQL::STRING_TYPE,
null: true,
description: 'Description of the on-call schedule'
field :timezone,
GraphQL::STRING_TYPE,
null: false,
description: 'Time zone of the on-call schedule'
end
end
end
......@@ -98,6 +98,8 @@ module EE
has_many :sourced_pipelines, class_name: 'Ci::Sources::Project', foreign_key: :source_project_id
has_many :incident_management_oncall_schedules, class_name: 'IncidentManagement::OncallSchedule', inverse_of: :project
scope :with_shared_runners_limit_enabled, -> do
if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost?
with_shared_runners
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedule < ApplicationRecord
self.table_name = 'incident_management_oncall_schedules'
include IidRoutes
include AtomicInternalId
NAME_LENGTH = 200
DESCRIPTION_LENGTH = 1000
belongs_to :project, inverse_of: :incident_management_oncall_schedules
has_internal_id :iid, scope: :project
validates :name, presence: true, uniqueness: { scope: :project }, length: { maximum: NAME_LENGTH }
validates :description, length: { maximum: DESCRIPTION_LENGTH }
validates :timezone, presence: true, inclusion: { in: :timezones }
private
def timezones
@timezones ||= ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier }
end
end
end
......@@ -127,6 +127,7 @@ class License < ApplicationRecord
unprotection_restrictions
ci_project_subscriptions
incident_timeline_view
oncall_schedules
]
EEP_FEATURES.freeze
......
......@@ -178,6 +178,7 @@ module EE
enable :read_deploy_board
enable :admin_epic_issue
enable :read_group_timelogs
enable :read_incident_management_oncall_schedule
end
rule { can?(:developer_access) }.policy do
......@@ -241,6 +242,7 @@ module EE
enable :modify_auto_fix_setting
enable :modify_merge_request_author_setting
enable :modify_merge_request_committer_setting
enable :admin_incident_management_oncall_schedule
end
rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedulePolicy < ::BasePolicy
delegate { @subject.project }
end
end
# 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_license unless available?
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?(: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(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_no_license
error(_('Your license does not support on-call schedules'))
end
def error_in_create(oncall_schedule)
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
end
end
---
name: oncall_schedules_mvc
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47407
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/283914
milestone: '13.7'
type: development
group: group::monitor
default_enabled: false
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_oncall_schedule, class: 'IncidentManagement::OncallSchedule' do
project
name { 'Default On-call Schedule' }
description { 'On-call description' }
timezone { 'Europe/Berlin' }
end
end
# frozen_string_literal: true
require 'spec_helper'
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) }
describe '#execute' do
subject(:execute) { described_class.new(current_user, project).execute }
context 'when feature is available' do
before do
stub_licensed_features(oncall_schedules: true)
end
context 'when user has permissions' do
before do
project.add_maintainer(current_user)
end
it 'returns project on-call schedules' do
is_expected.to contain_exactly(oncall_schedule)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
end
end
context 'when user has no permissions' do
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
end
end
context 'when feature is not avaiable' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
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(:admin_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
stub_licensed_features(oncall_schedules: true)
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 Resolvers::IncidentManagement::OncallScheduleResolver do
include GraphqlHelpers
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) }
subject { sync(resolve_oncall_schedules) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
specify do
expect(described_class).to have_nullable_graphql_type(Types::IncidentManagement::OncallScheduleType.connection_type)
end
it 'returns on-call schedules' do
is_expected.to contain_exactly(oncall_schedule)
end
private
def resolve_oncall_schedules(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['IncidentManagementOncallSchedule'] do
specify { expect(described_class.graphql_name).to eq('IncidentManagementOncallSchedule') }
specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_oncall_schedule) }
it 'exposes the expected fields' do
expected_fields = %i[
iid
name
description
timezone
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedule do
let_it_be(:project) { create(:project) }
describe '.associations' do
it { is_expected.to belong_to(:project) }
end
describe '.validations' do
let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } }
subject { build(:incident_management_oncall_schedule, project: project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(200) }
it { is_expected.to validate_length_of(:description).is_at_most(1000) }
it { is_expected.to validate_presence_of(:timezone) }
it { is_expected.to validate_inclusion_of(:timezone).in_array(timezones) }
context 'when the oncall schedule with the same name exists' do
before do
create(:incident_management_oncall_schedule, project: project)
end
it 'has validation errors' do
expect(subject).to be_invalid
expect(subject.errors.full_messages.to_sentence).to eq('Name has already been taken')
end
end
end
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:incident_management_oncall_schedule) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :incident_management_oncall_schedules }
end
end
......@@ -51,6 +51,8 @@ RSpec.describe Project do
it { is_expected.to have_many(:project_aliases) }
it { is_expected.to have_many(:approval_rules) }
it { is_expected.to have_many(:incident_management_oncall_schedules).class_name('IncidentManagement::OncallSchedule') }
describe 'approval_rules association' do
let_it_be(:rule, reload: true) { create(:approval_project_rule) }
let(:project) { rule.project }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedulePolicy do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
subject(:policy) { described_class.new(user, oncall_schedule) }
describe 'rules' do
it { is_expected.to be_disallowed :read_incident_management_oncall_schedule }
context 'when maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed :read_incident_management_oncall_schedule }
end
end
end
......@@ -1342,6 +1342,58 @@ RSpec.describe ProjectPolicy do
end
end
describe 'Incident Management on-call schedules' do
using RSpec::Parameterized::TableSyntax
context ':read_incident_management_oncall_schedule' do
let(:policy) { :read_incident_management_oncall_schedule }
where(:role, :admin_mode, :allowed) do
:guest | nil | false
:reporter | nil | true
:developer | nil | true
:maintainer | nil | true
:owner | nil | true
:admin | false | false
:admin | true | true
end
before do
enable_admin_mode!(current_user) if admin_mode
end
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
context ':admin_incident_management_oncall_schedule' do
let(:policy) { :admin_incident_management_oncall_schedule }
where(:role, :admin_mode, :allowed) do
:guest | nil | false
:reporter | nil | false
:developer | nil | false
:maintainer | nil | true
:owner | nil | true
:admin | false | false
:admin | true | true
end
before do
enable_admin_mode!(current_user) if admin_mode
end
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
end
context 'when project is readonly because the storage usage limit has been exceeded on the root namespace' do
let(:current_user) { owner }
let(:abilities) do
......
# 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
stub_licensed_features(oncall_schedules: true)
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 'getting Incident Management on-call schedules' do
include GraphqlHelpers
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('IncidentManagementOncallSchedule')}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('incidentManagementOncallSchedules', {}, fields)
)
end
let(:oncall_schedules) { graphql_data.dig('project', 'incidentManagementOncallSchedules', 'nodes') }
before do
stub_licensed_features(oncall_schedules: true)
end
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_nil }
end
context 'with project permissions' do
before do
project.add_maintainer(current_user)
end
context 'with unavailable feature' do
before do
stub_licensed_features(oncall_schedules: false)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_empty }
end
context 'without on-call schedules' do
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_empty }
end
context 'with on-call schedules' do
let!(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:last_oncall_schedule) { oncall_schedules.last }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns the correct properties of the on-call schedule' do
expect(last_oncall_schedule).to include(
'iid' => oncall_schedule.iid.to_s,
'name' => oncall_schedule.name,
'description' => oncall_schedule.description,
'timezone' => oncall_schedule.timezone
)
end
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_refind(: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
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 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 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 already exists' do
let!(: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
......@@ -31265,6 +31265,9 @@ msgstr ""
msgid "You have insufficient permissions to create an HTTP integration for this project"
msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration"
msgstr ""
......@@ -31631,6 +31634,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr ""
msgid "Your license does not support on-call schedules"
msgstr ""
msgid "Your license is valid from"
msgstr ""
......
......@@ -554,6 +554,7 @@ project:
- terraform_states
- alert_management_http_integrations
- exported_protected_branches
- incident_management_oncall_schedules
award_emoji:
- awardable
- user
......
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