Commit e18bf545 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add GraphQL query to retrieve timeline events

Changelog: added
EE: true
parent dffaa093
...@@ -7741,6 +7741,29 @@ The edge type for [`TestSuiteSummary`](#testsuitesummary). ...@@ -7741,6 +7741,29 @@ The edge type for [`TestSuiteSummary`](#testsuitesummary).
| <a id="testsuitesummaryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="testsuitesummaryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="testsuitesummaryedgenode"></a>`node` | [`TestSuiteSummary`](#testsuitesummary) | The item at the end of the edge. | | <a id="testsuitesummaryedgenode"></a>`node` | [`TestSuiteSummary`](#testsuitesummary) | The item at the end of the edge. |
#### `TimelineEventTypeConnection`
The connection type for [`TimelineEventType`](#timelineeventtype).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="timelineeventtypeconnectionedges"></a>`edges` | [`[TimelineEventTypeEdge]`](#timelineeventtypeedge) | A list of edges. |
| <a id="timelineeventtypeconnectionnodes"></a>`nodes` | [`[TimelineEventType]`](#timelineeventtype) | A list of nodes. |
| <a id="timelineeventtypeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `TimelineEventTypeEdge`
The edge type for [`TimelineEventType`](#timelineeventtype).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="timelineeventtypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="timelineeventtypeedgenode"></a>`node` | [`TimelineEventType`](#timelineeventtype) | The item at the end of the edge. |
#### `TimelogConnection` #### `TimelogConnection`
The connection type for [`Timelog`](#timelog). The connection type for [`Timelog`](#timelog).
...@@ -13295,6 +13318,35 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -13295,6 +13318,35 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectincidentmanagementoncallschedulesiids"></a>`iids` | [`[ID!]`](#id) | IIDs of on-call schedules. | | <a id="projectincidentmanagementoncallschedulesiids"></a>`iids` | [`[ID!]`](#id) | IIDs of on-call schedules. |
##### `Project.incidentManagementTimelineEvent`
Incident Management Timeline event associated with the incident.
Returns [`TimelineEventType`](#timelineeventtype).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectincidentmanagementtimelineeventid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event. |
| <a id="projectincidentmanagementtimelineeventincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | ID of the incident. |
##### `Project.incidentManagementTimelineEvents`
Incident Management Timeline events associated with the incident.
Returns [`TimelineEventTypeConnection`](#timelineeventtypeconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectincidentmanagementtimelineeventsincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | ID of the incident. |
##### `Project.issue` ##### `Project.issue`
A single issue of the project. A single issue of the project.
...@@ -14999,6 +15051,27 @@ Represents a historically accurate report about the timebox. ...@@ -14999,6 +15051,27 @@ Represents a historically accurate report about the timebox.
| <a id="timeboxreportburnuptimeseries"></a>`burnupTimeSeries` | [`[BurnupChartDailyTotals!]`](#burnupchartdailytotals) | Daily scope and completed totals for burnup charts. | | <a id="timeboxreportburnuptimeseries"></a>`burnupTimeSeries` | [`[BurnupChartDailyTotals!]`](#burnupchartdailytotals) | Daily scope and completed totals for burnup charts. |
| <a id="timeboxreportstats"></a>`stats` | [`TimeReportStats`](#timereportstats) | Represents the time report stats for the timebox. | | <a id="timeboxreportstats"></a>`stats` | [`TimeReportStats`](#timereportstats) | Represents the time report stats for the timebox. |
### `TimelineEventType`
Describes an incident management timeline event.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="timelineeventtypeaction"></a>`action` | [`String!`](#string) | Indicates the timeline event icon. |
| <a id="timelineeventtypeauthor"></a>`author` | [`UserCore`](#usercore) | User that created the timeline event. |
| <a id="timelineeventtypecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp when the event created. |
| <a id="timelineeventtypeeditable"></a>`editable` | [`Boolean!`](#boolean) | Indicates the timeline event is editable. |
| <a id="timelineeventtypeid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event. |
| <a id="timelineeventtypeincident"></a>`incident` | [`Issue!`](#issue) | Incident of the timeline event. |
| <a id="timelineeventtypenote"></a>`note` | [`String`](#string) | Text note of the timeline event. |
| <a id="timelineeventtypenotehtml"></a>`noteHtml` | [`String`](#string) | HTML note of the timeline event. |
| <a id="timelineeventtypeoccurredat"></a>`occurredAt` | [`Time!`](#time) | Timestamp when the event occurred. |
| <a id="timelineeventtypepromotedfromnote"></a>`promotedFromNote` | [`Note`](#note) | Note from which the timeline event was created. |
| <a id="timelineeventtypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp when the event updated. |
| <a id="timelineeventtypeupdatedbyuser"></a>`updatedByUser` | [`UserCore`](#usercore) | User that updated the timeline event. |
### `Timelog` ### `Timelog`
#### Fields #### Fields
...@@ -17691,6 +17764,12 @@ A `IncidentManagementOncallRotationID` is a global ID. It is encoded as a string ...@@ -17691,6 +17764,12 @@ A `IncidentManagementOncallRotationID` is a global ID. It is encoded as a string
An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManagement::OncallRotation/1"`. An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManagement::OncallRotation/1"`.
### `IncidentManagementTimelineEventID`
A `IncidentManagementTimelineEventID` is a global ID. It is encoded as a string.
An example `IncidentManagementTimelineEventID` is: `"gid://gitlab/IncidentManagement::TimelineEvent/1"`.
### `Int` ### `Int`
Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
# frozen_string_literal: true
module IncidentManagement
class TimelineEventsFinder
def initialize(user, incident, params = {})
@user = user
@incident = incident
@params = params
end
def execute
return ::IncidentManagement::TimelineEvent.none unless allowed?
collection = incident.incident_management_timeline_events
collection = by_id(collection)
sort(collection)
end
private
attr_reader :user, :incident, :params
def allowed?
Ability.allowed?(user, :read_incident_management_timeline_event, incident)
end
def by_id(collection)
return collection unless params[:id]
collection.id_in(params[:id])
end
def sort(collection)
collection.order_occurred_at_asc
end
end
end
...@@ -139,6 +139,19 @@ module EE ...@@ -139,6 +139,19 @@ module EE
description: 'Incident Management escalation policy of the project.', description: 'Incident Management escalation policy of the project.',
resolver: ::Resolvers::IncidentManagement::EscalationPoliciesResolver.single resolver: ::Resolvers::IncidentManagement::EscalationPoliciesResolver.single
field :incident_management_timeline_events,
::Types::IncidentManagement::TimelineEventType.connection_type,
null: true,
description: 'Incident Management Timeline events associated with the incident.',
extras: [:lookahead],
resolver: ::Resolvers::IncidentManagement::TimelineEventsResolver
field :incident_management_timeline_event,
::Types::IncidentManagement::TimelineEventType,
null: true,
description: 'Incident Management Timeline event associated with the incident.',
resolver: ::Resolvers::IncidentManagement::TimelineEventsResolver.single
field :api_fuzzing_ci_configuration, field :api_fuzzing_ci_configuration,
::Types::AppSec::Fuzzing::API::CiConfigurationType, ::Types::AppSec::Fuzzing::API::CiConfigurationType,
null: true, null: true,
......
# frozen_string_literal: true
module Resolvers
module IncidentManagement
class TimelineEventsResolver < BaseResolver
include LooksAhead
alias_method :project, :object
type ::Types::IncidentManagement::TimelineEventType.connection_type, null: true
argument :incident_id,
::Types::GlobalIDType[::Issue],
required: true,
description: 'ID of the incident.'
when_single do
argument :id,
::Types::GlobalIDType[::IncidentManagement::TimelineEvent],
required: true,
description: 'ID of the timeline event.',
prepare: ->(id, ctx) { id.model_id }
end
def resolve(**args)
incident = args[:incident_id].find
apply_lookahead(::IncidentManagement::TimelineEventsFinder.new(current_user, incident, args).execute)
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class TimelineEventType < BaseObject
graphql_name 'TimelineEventType'
description 'Describes an incident management timeline event'
authorize :read_incident_management_timeline_event
field :id,
Types::GlobalIDType[::IncidentManagement::TimelineEvent],
null: false,
description: 'ID of the timeline event.'
field :author,
Types::UserType,
null: true,
description: 'User that created the timeline event.'
field :updated_by_user,
Types::UserType,
null: true,
description: 'User that updated the timeline event.'
field :incident,
Types::IssueType,
null: false,
description: 'Incident of the timeline event.'
field :note,
GraphQL::Types::String,
null: true,
description: 'Text note of the timeline event.'
field :note_html,
GraphQL::Types::String,
null: true,
description: 'HTML note of the timeline event.'
field :promoted_from_note,
Types::Notes::NoteType,
null: true,
description: 'Note from which the timeline event was created.'
field :editable,
GraphQL::Types::Boolean,
null: false,
description: 'Indicates the timeline event is editable.'
field :action,
GraphQL::Types::String,
null: false,
description: 'Indicates the timeline event icon.'
field :occurred_at,
Types::TimeType,
null: false,
description: 'Timestamp when the event occurred.'
field :created_at,
Types::TimeType,
null: false,
description: 'Timestamp when the event created.'
field :updated_at,
Types::TimeType,
null: false,
description: 'Timestamp when the event updated.'
end
end
end
...@@ -73,6 +73,7 @@ module EE ...@@ -73,6 +73,7 @@ module EE
has_many :feature_flags, through: :feature_flag_issues, class_name: '::Operations::FeatureFlag' has_many :feature_flags, through: :feature_flag_issues, class_name: '::Operations::FeatureFlag'
has_many :pending_escalations, class_name: 'IncidentManagement::PendingEscalations::Issue', foreign_key: :issue_id, inverse_of: :issue has_many :pending_escalations, class_name: 'IncidentManagement::PendingEscalations::Issue', foreign_key: :issue_id, inverse_of: :issue
has_many :incident_management_timeline_events, class_name: 'IncidentManagement::TimelineEvent', foreign_key: :issue_id, inverse_of: :incident
validates :weight, allow_nil: true, numericality: { greater_than_or_equal_to: 0 } validates :weight, allow_nil: true, numericality: { greater_than_or_equal_to: 0 }
validate :validate_confidential_epic validate :validate_confidential_epic
......
...@@ -6,12 +6,14 @@ module IncidentManagement ...@@ -6,12 +6,14 @@ module IncidentManagement
belongs_to :project belongs_to :project
belongs_to :author, class_name: 'User', foreign_key: :author_id belongs_to :author, class_name: 'User', foreign_key: :author_id
belongs_to :incident, class_name: 'Issue', foreign_key: :issue_id belongs_to :incident, class_name: 'Issue', foreign_key: :issue_id, inverse_of: :incident_management_timeline_events
belongs_to :updated_by_user, class_name: 'User', foreign_key: :updated_by_user_id belongs_to :updated_by_user, class_name: 'User', foreign_key: :updated_by_user_id
belongs_to :promoted_from_note, class_name: 'Note', foreign_key: :promoted_from_note_id belongs_to :promoted_from_note, class_name: 'Note', foreign_key: :promoted_from_note_id
validates :project, :incident, :occurred_at, presence: true validates :project, :incident, :occurred_at, presence: true
validates :action, presence: true, length: { maximum: 128 } validates :action, presence: true, length: { maximum: 128 }
validates :note, :note_html, presence: true, length: { maximum: 10_000 } validates :note, :note_html, presence: true, length: { maximum: 10_000 }
scope :order_occurred_at_asc, -> { reorder(occurred_at: :asc) }
end end
end end
...@@ -168,6 +168,7 @@ class License < ApplicationRecord ...@@ -168,6 +168,7 @@ class License < ApplicationRecord
group_level_compliance_dashboard group_level_compliance_dashboard
group_level_devops_adoption group_level_devops_adoption
incident_management incident_management
incident_timeline_events
inline_codequality inline_codequality
insights insights
instance_level_devops_adoption instance_level_devops_adoption
......
...@@ -9,10 +9,19 @@ module EE ...@@ -9,10 +9,19 @@ module EE
@user && @subject.author_id == @user.id @user && @subject.author_id == @user.id
end end
with_scope :subject
condition(:timeline_events_available) do
::Gitlab::IncidentManagement.timeline_events_available?(@subject.project)
end
rule { can?(:read_issue) }.policy do rule { can?(:read_issue) }.policy do
enable :read_issuable_metric_image enable :read_issuable_metric_image
end end
rule { can?(:read_issue) & timeline_events_available }.policy do
enable :read_incident_management_timeline_event
end
rule { can?(:create_issue) & can?(:update_issue) }.policy do rule { can?(:create_issue) & can?(:update_issue) }.policy do
enable :upload_issuable_metric_image enable :upload_issuable_metric_image
end end
......
# frozen_string_literal: true
module IncidentManagement
class TimelineEventPolicy < ::BasePolicy
delegate { @subject.incident }
end
end
...@@ -3,11 +3,15 @@ ...@@ -3,11 +3,15 @@
module Gitlab module Gitlab
module IncidentManagement module IncidentManagement
def self.oncall_schedules_available?(project) def self.oncall_schedules_available?(project)
project.feature_available?(:oncall_schedules) project.licensed_feature_available?(:oncall_schedules)
end end
def self.escalation_policies_available?(project) def self.escalation_policies_available?(project)
oncall_schedules_available?(project) && project.feature_available?(:escalation_policies) oncall_schedules_available?(project) && project.licensed_feature_available?(:escalation_policies)
end
def self.timeline_events_available?(project)
project.licensed_feature_available?(:incident_timeline_events)
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::TimelineEventsFinder do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:another_incident) { create(:incident, project: project) }
let_it_be(:timeline_event1) do
create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: Time.current)
end
let_it_be(:timeline_event2) do
create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: 1.minute.ago)
end
let_it_be(:timeline_event_of_another_incident) { create(:incident_management_timeline_event, project: project, incident: another_incident) }
let(:params) { {} }
describe '#execute' do
subject(:execute) { described_class.new(user, incident, params).execute }
context 'when feature is available' do
before do
stub_licensed_features(incident_timeline_events: true)
end
context 'when user has permissions' do
before do
project.add_guest(user)
end
it 'returns timeline events' do
is_expected.to eq([timeline_event2, timeline_event1])
end
context 'when filtering by ID' do
let(:params) { { id: timeline_event1 } }
it 'returns only matched timeline event' do
is_expected.to contain_exactly(timeline_event1)
end
end
context 'when incident is nil' do
let_it_be(:incident) { nil }
it { is_expected.to eq(IncidentManagement::TimelineEvent.none) }
end
end
context 'when user has no permissions' do
it { is_expected.to eq(IncidentManagement::TimelineEvent.none) }
end
end
context 'when feature is not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it { is_expected.to eq(IncidentManagement::TimelineEvent.none) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::IncidentManagement::TimelineEventsResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:first_timeline_event) { create(:incident_management_timeline_event, project: project, incident: incident) }
let_it_be(:second_timeline_event) { create(:incident_management_timeline_event, project: project, incident: incident) }
let(:args) { { incident_id: incident.to_global_id } }
let(:resolver) { described_class }
subject(:resolved_timeline_events) { sync(resolve_timeline_events(args, current_user: current_user).to_a) }
before do
stub_licensed_features(incident_timeline_events: true)
project.add_guest(current_user)
end
specify do
expect(resolver).to have_nullable_graphql_type(Types::IncidentManagement::TimelineEventType.connection_type)
end
it 'returns timeline events', :aggregate_failures do
expect(resolved_timeline_events.length).to eq(2)
expect(resolved_timeline_events.first).to be_a(::IncidentManagement::TimelineEvent)
end
context 'when user does not have permissions' do
let(:non_member) { create(:user) }
subject(:resolved_timeline_events) { sync(resolve_timeline_events(args, current_user: non_member).to_a) }
before do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
end
it 'returns no timeline events' do
expect(resolved_timeline_events.length).to eq(0)
end
end
context 'when resolving a single item' do
let(:resolver) { described_class.single }
subject(:resolved_timeline_event) { sync(resolve_timeline_events(args, current_user: current_user)) }
context 'when id given' do
let(:args) { { incident_id: incident.to_global_id, id: first_timeline_event.to_global_id } }
it 'returns the timeline event' do
expect(resolved_timeline_event).to eq(first_timeline_event)
end
end
end
private
def resolve_timeline_events(args = {}, context = { current_user: current_user })
resolve(resolver, obj: incident, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['TimelineEventType'] do
specify { expect(described_class.graphql_name).to eq('TimelineEventType') }
specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_timeline_event) }
it 'exposes the expected fields' do
expected_fields = %i[
id
author
updated_by_user
incident
note
note_html
promoted_from_note
editable
action
occurred_at
created_at
updated_at
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
...@@ -21,7 +21,8 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -21,7 +21,8 @@ RSpec.describe GitlabSchema.types['Project'] do
vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day
security_dashboard_path iterations iteration_cadences repository_size_excess actual_repository_size_limit security_dashboard_path iterations iteration_cadences repository_size_excess actual_repository_size_limit
code_coverage_summary api_fuzzing_ci_configuration corpuses path_locks incident_management_escalation_policies code_coverage_summary api_fuzzing_ci_configuration corpuses path_locks incident_management_escalation_policies
incident_management_escalation_policy scan_execution_policies network_policies incident_management_escalation_policy scan_execution_policies network_policies incident_management_timeline_events
incident_management_timeline_event
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -48,4 +48,22 @@ RSpec.describe Gitlab::IncidentManagement do ...@@ -48,4 +48,22 @@ RSpec.describe Gitlab::IncidentManagement do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
end end
describe '.timeline_events_available?' do
subject { described_class.timeline_events_available?(project) }
before do
stub_licensed_features(incident_timeline_events: true)
end
it { is_expected.to be_truthy }
context 'when timeline events not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it { is_expected.to be_falsey }
end
end
end end
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IncidentManagement::TimelineEvent do RSpec.describe IncidentManagement::TimelineEvent do
let_it_be(:timeline_event) { create(:incident_management_timeline_event) } let_it_be(:project) { create(:project) }
let_it_be(:timeline_event) { create(:incident_management_timeline_event, project: project) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
...@@ -26,4 +27,15 @@ RSpec.describe IncidentManagement::TimelineEvent do ...@@ -26,4 +27,15 @@ RSpec.describe IncidentManagement::TimelineEvent do
it { is_expected.to validate_presence_of(:action) } it { is_expected.to validate_presence_of(:action) }
it { is_expected.to validate_length_of(:action).is_at_most(128) } it { is_expected.to validate_length_of(:action).is_at_most(128) }
end end
describe '.order_occurred_at_asc' do
let_it_be(:occurred_3mins_ago) { create(:incident_management_timeline_event, project: project, occurred_at: 3.minutes.ago) }
let_it_be(:occurred_2mins_ago) { create(:incident_management_timeline_event, project: project, occurred_at: 2.minutes.ago) }
subject(:order) { described_class.order_occurred_at_asc }
it 'sorts timeline events by occurred_at' do
is_expected.to eq([occurred_3mins_ago, occurred_2mins_ago, timeline_event])
end
end
end end
...@@ -15,6 +15,7 @@ RSpec.describe Issue do ...@@ -15,6 +15,7 @@ RSpec.describe Issue do
it { is_expected.to have_one(:issuable_sla) } it { is_expected.to have_one(:issuable_sla) }
it { is_expected.to have_many(:metric_images) } it { is_expected.to have_many(:metric_images) }
it { is_expected.to have_many(:pending_escalations) } it { is_expected.to have_many(:pending_escalations) }
it { is_expected.to have_many(:incident_management_timeline_events) }
it { is_expected.to have_one(:requirement) } it { is_expected.to have_one(:requirement) }
it { is_expected.to have_many(:test_reports) } it { is_expected.to have_many(:test_reports) }
......
...@@ -13,6 +13,8 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -13,6 +13,8 @@ RSpec.describe IssuablePolicy, models: true do
before do before do
project.add_guest(guest) project.add_guest(guest)
project.add_reporter(reporter) project.add_reporter(reporter)
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(true)
end end
def permissions(user, issue) def permissions(user, issue)
...@@ -40,6 +42,22 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -40,6 +42,22 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image)
end end
context 'Timeline events' do
it 'allows non-members to read time line events' do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
end
context 'when timeline events are not available' do
before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
end
it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
end
end
end
end end
context 'in a private project' do context 'in a private project' do
...@@ -61,6 +79,26 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -61,6 +79,26 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image)
end end
context 'Timeline events' do
it 'disallows non-members from reading timeline events' do
expect(permissions(non_member, issue)).to be_disallowed(:read_incident_management_timeline_event)
end
it 'allows guests to read time line events' do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
end
context 'when timeline events are not available' do
before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
end
it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
end
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting incident timeline events' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:updated_by_user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:another_incident) { create(:incident, project: project) }
let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) }
let_it_be(:timeline_event) do
create(
:incident_management_timeline_event,
incident: incident,
project: project,
updated_by_user: updated_by_user,
promoted_from_note: promoted_from_note
)
end
let_it_be(:second_timeline_event) { create(:incident_management_timeline_event, incident: incident, project: project) }
let_it_be(:another_timeline_event) { create(:incident_management_timeline_event, incident: another_incident, project: project) }
let(:params) { { incident_id: incident.to_global_id.to_s } }
let(:timeline_event_fields) do
<<~QUERY
nodes {
id
author { id username }
updatedByUser { id username }
incident { id title }
note
noteHtml
promotedFromNote { id body }
editable
action
occurredAt
createdAt
updatedAt
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('incidentManagementTimelineEvents', params, timeline_event_fields)
)
end
let(:timeline_events) do
graphql_data.dig('project', 'incidentManagementTimelineEvents', 'nodes')
end
before do
stub_licensed_features(incident_timeline_events: true)
project.add_guest(current_user)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns the correct number of timeline events' do
expect(timeline_events.count).to eq(2)
end
it 'returns the correct properties of the incident timeline events' do
expect(timeline_events.first).to include(
'author' => {
'id' => timeline_event.author.to_global_id.to_s,
'username' => timeline_event.author.username
},
'updatedByUser' => {
'id' => updated_by_user.to_global_id.to_s,
'username' => updated_by_user.username
},
'incident' => {
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
'note' => timeline_event.note,
'noteHtml' => timeline_event.note_html,
'promotedFromNote' => {
'id' => promoted_from_note.to_global_id.to_s,
'body' => promoted_from_note.note
},
'editable' => false,
'action' => timeline_event.action,
'occurredAt' => timeline_event.occurred_at.iso8601,
'createdAt' => timeline_event.created_at.iso8601,
'updatedAt' => timeline_event.updated_at.iso8601
)
end
context 'when filtering by id' do
let(:params) { { incident_id: incident.to_global_id.to_s, id: timeline_event.to_global_id.to_s } }
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('incidentManagementTimelineEvent', params, 'id occurredAt')
)
end
it_behaves_like 'a working graphql query'
it 'returns a single timeline event', :aggregate_failures do
single_timeline_event = graphql_data.dig('project', 'incidentManagementTimelineEvent')
expect(single_timeline_event).to include(
'id' => timeline_event.to_global_id.to_s,
'occurredAt' => timeline_event.occurred_at.iso8601
)
end
end
end
...@@ -58,6 +58,7 @@ issues: ...@@ -58,6 +58,7 @@ issues:
- test_reports - test_reports
- requirement - requirement
- incident_management_issuable_escalation_status - incident_management_issuable_escalation_status
- incident_management_timeline_events
- pending_escalations - pending_escalations
- customer_relations_contacts - customer_relations_contacts
- issue_customer_relations_contacts - issue_customer_relations_contacts
......
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