Commit d23d61db authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Bob Van Landuyt

Add alert assignee table, model, API support

Adds table and model for AlertManagement::AlertAssignee, which
will enable the association of users with alerts. This will
aide in triaging incoming alerts for a project.

This commit also adds the ability to read assignees for alerts
to the GraphQL API.
parent 4cba8a1f
......@@ -15,11 +15,13 @@ import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
......@@ -47,7 +49,9 @@ export default {
GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
},
mixins: [glFeatureFlagsMixin()],
props: {
alertId: {
type: String,
......@@ -159,6 +163,9 @@ export default {
const { category, action } = trackAlertsDetailsViewsOptions;
Tracking.event(category, action);
},
alertRefresh() {
this.$apollo.queries.alert.refetch();
},
},
};
</script>
......@@ -287,6 +294,13 @@ export default {
</div>
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div>
</div>
<template v-if="glFeatures.alertAssignee">
<div v-if="alert.notes" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
<system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
</ul>
</div>
</template>
</gl-tab>
<gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle">
<gl-table
......@@ -309,6 +323,7 @@ export default {
:project-path="projectPath"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar"
@alert-sidebar-error="handleAlertSidebarError"
/>
......
......@@ -53,6 +53,7 @@ export default {
v-if="glFeatures.alertAssignee"
:project-path="projectPath"
:alert="alert"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
/>
......
......@@ -141,6 +141,7 @@ export default {
})
.then(() => {
this.hideDropdown();
this.$emit('alert-refresh');
})
.catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
......
<script>
import NoteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '~/lib/utils/common_utils';
export default {
components: {
NoteHeader,
},
props: {
note: {
type: Object,
required: true,
},
},
computed: {
noteAnchorId() {
return `note_${this.note?.id?.split('/').pop()}`;
},
iconHtml() {
return spriteIcon('user');
},
},
};
</script>
<template>
<li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.createdAt" :note-id="note.id">
<span v-html="note.bodyHtml"></span>
</note-header>
</div>
</div>
</div>
</li>
</template>
#import "~/graphql_shared/fragments/author.fragment.graphql"
fragment AlertNote on Note {
id
author {
id
state
...Author
}
body
bodyHtml
createdAt
discussion {
id
}
}
#import "./list_item.fragment.graphql"
#import "./alert_note.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem
......@@ -8,4 +9,9 @@ fragment AlertDetailItem on AlertManagementAlert {
description
updatedAt
details
notes {
nodes {
...AlertNote
}
}
}
#import "../fragments/detailItem.fragment.graphql"
#import "../fragments/detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
......
......@@ -6,6 +6,8 @@ module Types
graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management"
implements(Types::Notes::NoteableType)
authorize :read_alert_management_alert
field :iid,
......
......@@ -19,6 +19,8 @@ module Types
Types::SnippetType
when ::DesignManagement::Design
Types::DesignManagement::DesignType
when ::AlertManagement::Alert
Types::AlertManagement::AlertType
else
raise "Unknown GraphQL type for #{object}"
end
......
......@@ -8,6 +8,7 @@ module AlertManagement
include AtomicInternalId
include ShaAttribute
include Sortable
include Noteable
include Gitlab::SQL::Pattern
STATUSES = {
......@@ -30,6 +31,9 @@ module AlertManagement
has_many :alert_assignees, inverse_of: :alert
has_many :assignees, through: :alert_assignees
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
sha_attribute :fingerprint
......
# frozen_string_literal: true
module AlertManagement
class AlertUserMention < UserMention
belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert'
belongs_to :note
end
end
......@@ -274,6 +274,10 @@ class Note < ApplicationRecord
noteable_type == "Snippet"
end
def for_alert_mangement_alert?
noteable_type == 'AlertManagement::Alert'
end
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
......@@ -396,7 +400,13 @@ class Note < ApplicationRecord
end
def noteable_ability_name
for_snippet? ? 'snippet' : noteable_type.demodulize.underscore
if for_snippet?
'snippet'
elsif for_alert_mangement_alert?
'alert_management_alert'
else
noteable_type.demodulize.underscore
end
end
def can_be_discussion_note?
......
......@@ -19,9 +19,11 @@ module AlertManagement
return error_no_updates if params.empty?
filter_assignees
old_assignees = alert.assignees.to_a
if alert.update(params)
assign_todo
process_assignement(old_assignees)
success
else
error(alert.errors.full_messages.to_sentence)
......@@ -32,29 +34,10 @@ module AlertManagement
attr_reader :alert, :current_user, :params
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def allowed?
current_user.can?(:update_alert_management_alert, alert)
end
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def todo_service
strong_memoize(:todo_service) do
TodoService.new
......@@ -76,6 +59,35 @@ module AlertManagement
def error_no_updates
error(_('Please provide attributes to update'))
end
# ----- Assignee-related behavior ------
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def process_assignement(old_assignees)
assign_todo
add_assignee_system_note(old_assignees)
end
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def add_assignee_system_note(old_assignees)
SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees)
end
end
end
end
---
title: Add system note when assigning user to alert
merge_request: 33217
author:
type: added
# frozen_string_literal: true
class CreateAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :alert_management_alert_user_mentions do |t|
t.references :alert_management_alert, type: :bigint, index: false, null: false, foreign_key: { on_delete: :cascade }
t.bigint :note_id, null: true
t.integer :mentioned_users_ids, array: true
t.integer :mentioned_projects_ids, array: true
t.integer :mentioned_groups_ids, array: true
end
add_index :alert_management_alert_user_mentions, [:note_id], where: 'note_id IS NOT NULL', unique: true, name: 'index_alert_user_mentions_on_note_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id], where: 'note_id IS NULL', unique: true, name: 'index_alert_user_mentions_on_alert_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id, :note_id], unique: true, name: 'index_alert_user_mentions_on_alert_id_and_note_id'
end
end
# frozen_string_literal: true
class AddForeignKeyToAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :alert_management_alert_user_mentions, :notes, column: :note_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :alert_management_alert_user_mentions, column: :note_id
end
end
end
......@@ -37,6 +37,24 @@ CREATE SEQUENCE public.alert_management_alert_assignees_id_seq
ALTER SEQUENCE public.alert_management_alert_assignees_id_seq OWNED BY public.alert_management_alert_assignees.id;
CREATE TABLE public.alert_management_alert_user_mentions (
id bigint NOT NULL,
alert_management_alert_id bigint NOT NULL,
note_id bigint,
mentioned_users_ids integer[],
mentioned_projects_ids integer[],
mentioned_groups_ids integer[]
);
CREATE SEQUENCE public.alert_management_alert_user_mentions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.alert_management_alert_user_mentions_id_seq OWNED BY public.alert_management_alert_user_mentions.id;
CREATE TABLE public.alert_management_alerts (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -7401,6 +7419,8 @@ ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_assignees_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alert_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alerts ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alerts_id_seq'::regclass);
ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
......@@ -8045,6 +8065,9 @@ ALTER TABLE ONLY public.abuse_reports
ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT alert_management_alert_user_mentions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
......@@ -9194,6 +9217,12 @@ CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id) WHERE (note_id IS NULL);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id_and_note_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id, note_id);
CREATE UNIQUE INDEX index_alert_user_mentions_on_note_id ON public.alert_management_alert_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id);
CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id);
......@@ -12391,6 +12420,9 @@ ALTER TABLE ONLY public.design_user_mentions
ALTER TABLE ONLY public.clusters_kubernetes_namespaces
ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_8e48eca0fe FOREIGN KEY (alert_management_alert_id) REFERENCES public.alert_management_alerts(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_daily_statistics
ADD CONSTRAINT fk_rails_8e549b272d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -12769,6 +12801,9 @@ ALTER TABLE ONLY public.merge_request_blocks
ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_e9eb8dc025 FOREIGN KEY (protected_branch_id) REFERENCES public.protected_branches(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.ci_daily_report_results
ADD CONSTRAINT fk_rails_ebc2931b90 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -13873,6 +13908,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200527151413
20200527152116
20200527152657
20200527170649
20200527211000
20200528054112
20200528123703
......@@ -13886,6 +13922,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200604145731
20200604174544
20200604174558
20200605003204
20200608072931
20200608075553
20200609002841
......
......@@ -168,7 +168,7 @@ type AdminSidekiqQueuesDeleteJobsPayload {
"""
Describes an alert from the project's Alert Management
"""
type AlertManagementAlert {
type AlertManagementAlert implements Noteable {
"""
Assignees of the alert
"""
......@@ -209,6 +209,31 @@ type AlertManagementAlert {
"""
details: JSON
"""
All discussions on this noteable
"""
discussions(
"""
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
): DiscussionConnection!
"""
Timestamp the alert ended
"""
......@@ -239,6 +264,31 @@ type AlertManagementAlert {
"""
monitoringTool: String
"""
All notes on this noteable
"""
notes(
"""
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
): NoteConnection!
"""
Service the alert came from
"""
......
......@@ -577,6 +577,63 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "discussions",
"description": "All discussions on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiscussionConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endedAt",
"description": "Timestamp the alert ended",
......@@ -673,6 +730,63 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "notes",
"description": "All notes on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "NoteConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "service",
"description": "Service the alert came from",
......@@ -760,7 +874,11 @@
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
......@@ -23585,6 +23703,11 @@
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Design",
......@@ -17,6 +17,7 @@ FactoryBot.define do
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :note_on_design, traits: [:on_design]
factory :note_on_alert, traits: [:on_alert]
factory :system_note, traits: [:system]
factory :discussion_note, class: 'DiscussionNote'
......@@ -145,6 +146,10 @@ FactoryBot.define do
end
end
trait :on_alert do
noteable { association(:alert_management_alert, project: project) }
end
trait :resolved do
resolved_at { Time.now }
resolved_by { association(:user) }
......
import { shallowMount } from '@vue/test-utils';
import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[1];
describe('Alert Details System Note', () => {
let wrapper;
function mountComponent({ stubs = {} } = {}) {
wrapper = shallowMount(SystemNote, {
propsData: {
note: { ...mockAlert.notes.nodes[0] },
},
stubs,
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('System notes', () => {
beforeEach(() => {
mountComponent({});
});
it('renders the correct system note', () => {
expect(wrapper.find('.note-wrapper').attributes('id')).toBe('note_1628');
});
});
});
......@@ -8,7 +8,8 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED",
"assignees": { "nodes": [] }
"assignees": { "nodes": [] },
"notes": { "nodes": [] }
},
{
"iid": "1527543",
......@@ -18,7 +19,23 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED",
"assignees": { "nodes": [{ "username": "root" }] }
"assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1628",
"author": {
"id": "gid://gitlab/User/1",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
},
{
"iid": "1527544",
......@@ -28,6 +45,22 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED",
"assignees": { "nodes": [{ "username": "root" }] }
"assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1629",
"author": {
"id": "gid://gitlab/User/2",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
}
]
......@@ -25,6 +25,8 @@ describe GitlabSchema.types['AlertManagementAlert'] do
created_at
updated_at
assignees
notes
discussions
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
......@@ -17,6 +17,7 @@ describe Types::Notes::NoteableType do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType)
end
end
end
......@@ -7,6 +7,8 @@ describe AlertManagement::Alert do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
it { is_expected.to have_many(:notes) }
it { is_expected.to have_many(:user_mentions) }
end
describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertUserMention do
describe 'associations' do
it { is_expected.to belong_to(:alert_management_alert) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
......@@ -829,6 +829,10 @@ describe Note do
it 'returns commit for a commit note' do
expect(build(:note_on_commit).noteable_ability_name).to eq('commit')
end
it 'returns alert_management_alert for an alert note' do
expect(build(:note_on_alert).noteable_ability_name).to eq('alert_management_alert')
end
end
describe '#cache_markdown_field' do
......
......@@ -10,6 +10,8 @@ describe 'getting Alert Management Alerts' do
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
let_it_be(:system_note) { create(:note_on_alert, noteable: triggered_alert, project: project) }
let(:params) { {} }
let(:fields) do
......@@ -75,6 +77,8 @@ describe 'getting Alert Management Alerts' do
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
expect(first_alert['notes']['nodes'].first).to include('id' => system_note.to_global_id.to_s)
expect(second_alert).to include(
'iid' => resolved_alert.iid.to_s,
'issueIid' => nil,
......
......@@ -40,6 +40,7 @@ describe AlertManagement::Alerts::UpdateService do
let(:params) { { title: nil } }
it 'results in an error' do
expect { response }.not_to change { alert.reload.notes.count }
expect(response).to be_error
expect(response.message).to eq("Title can't be blank")
end
......@@ -72,6 +73,14 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success
end
it 'creates a system note for the assignment' do
expect { response }.to change { alert.reload.notes.count }.by(1)
end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
......@@ -80,10 +89,6 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success
end
end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment