diff options
Diffstat (limited to 'spec/frontend/vue_shared/alert_details')
10 files changed, 1223 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js new file mode 100644 index 00000000000..dd9a7be6268 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -0,0 +1,361 @@ +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { joinPaths } from '~/lib/utils/url_utility'; +import Tracking from '~/tracking'; +import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue'; +import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue'; +import { SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants'; +import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql'; +import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; +import mockAlerts from './mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; +const environmentName = 'Production'; +const environmentPath = '/fake/path'; + +describe('AlertDetails', () => { + let environmentData = { name: environmentName, path: environmentPath }; + let mock; + let wrapper; + const projectPath = 'root/alerts'; + const projectIssuesPath = 'root/alerts/-/issues'; + const projectId = '1'; + const $router = { replace: jest.fn() }; + + function mountComponent({ + data, + loading = false, + mountMethod = shallowMount, + provide = {}, + stubs = {}, + } = {}) { + wrapper = extendedWrapper( + mountMethod(AlertDetails, { + provide: { + alertId: 'alertId', + projectPath, + projectIssuesPath, + projectId, + ...provide, + }, + data() { + return { + alert: { + ...mockAlert, + environment: environmentData, + }, + sidebarStatus: false, + ...data, + }; + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + sidebarStatus: {}, + }, + }, + $router, + $route: { params: {} }, + }, + stubs: { + ...stubs, + AlertSummaryRow, + }, + }), + ); + } + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + mock.restore(); + }); + + const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn'); + const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn'); + const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError'); + const findEnvironmentName = () => wrapper.findByTestId('environmentName'); + const findEnvironmentPath = () => wrapper.findByTestId('environmentPath'); + const findDetailsTable = () => wrapper.find(AlertDetailsTable); + const findMetricsTab = () => wrapper.findByTestId('metrics'); + + describe('Alert details', () => { + describe('when alert is null', () => { + beforeEach(() => { + mountComponent({ data: { alert: null } }); + }); + + it('shows an empty state', () => { + expect(wrapper.findByTestId('alertDetailsTabs').exists()).toBe(false); + }); + }); + + describe('when alert is present', () => { + beforeEach(() => { + mountComponent({ data: { alert: mockAlert } }); + }); + + it('renders a tab with overview information', () => { + expect(wrapper.findByTestId('overview').exists()).toBe(true); + }); + + it('renders a tab with an activity feed', () => { + expect(wrapper.findByTestId('activity').exists()).toBe(true); + }); + + it('renders severity', () => { + expect(wrapper.findByTestId('severity').text()).toBe(SEVERITY_LEVELS[mockAlert.severity]); + }); + + it('renders a title', () => { + expect(wrapper.findByTestId('title').text()).toBe(mockAlert.title); + }); + + it('renders a start time', () => { + expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true); + expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt); + }); + }); + + describe('individual alert fields', () => { + describe.each` + field | data | isShown + ${'eventCount'} | ${1} | ${true} + ${'eventCount'} | ${undefined} | ${false} + ${'monitoringTool'} | ${'New Relic'} | ${true} + ${'monitoringTool'} | ${undefined} | ${false} + ${'service'} | ${'Prometheus'} | ${true} + ${'service'} | ${undefined} | ${false} + ${'runbook'} | ${undefined} | ${false} + ${'runbook'} | ${'run.com'} | ${true} + `(`$desc`, ({ field, data, isShown }) => { + beforeEach(() => { + mountComponent({ data: { alert: { ...mockAlert, [field]: data } } }); + }); + + it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => { + const element = wrapper.findByTestId(field); + if (isShown) { + expect(element.text()).toContain(data.toString()); + } else { + expect(wrapper.findByTestId(field).exists()).toBe(false); + } + }); + }); + }); + + describe('environment fields', () => { + it('should show the environment name with a link to the path', () => { + mountComponent(); + const path = findEnvironmentPath(); + + expect(findEnvironmentName().exists()).toBe(false); + expect(path.text()).toBe(environmentName); + expect(path.attributes('href')).toBe(environmentPath); + }); + + it('should only show the environment name if the path is not provided', () => { + environmentData = { name: environmentName, path: null }; + mountComponent(); + + expect(findEnvironmentPath().exists()).toBe(false); + expect(findEnvironmentName().text()).toBe(environmentName); + }); + }); + + describe('Threat Monitoring details', () => { + it('should not render the metrics tab', () => { + mountComponent({ + data: { alert: mockAlert, provide: { isThreatMonitoringPage: true } }, + }); + expect(findMetricsTab().exists()).toBe(false); + }); + }); + + describe('Create incident from alert', () => { + it('should display "View incident" button that links the incident page when incident exists', () => { + const issueIid = '3'; + mountComponent({ + data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false }, + }); + + expect(findViewIncidentBtn().exists()).toBe(true); + expect(findViewIncidentBtn().attributes('href')).toBe( + joinPaths(projectIssuesPath, issueIid), + ); + expect(findCreateIncidentBtn().exists()).toBe(false); + }); + + it('should display "Create incident" button when incident doesn\'t exist yet', () => { + const issueIid = null; + mountComponent({ + mountMethod: mount, + data: { alert: { ...mockAlert, issueIid } }, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(findViewIncidentBtn().exists()).toBe(false); + expect(findCreateIncidentBtn().exists()).toBe(true); + }); + }); + + it('calls `$apollo.mutate` with `createIssueQuery`', () => { + const issueIid = '10'; + mountComponent({ + mountMethod: mount, + data: { alert: { ...mockAlert } }, + }); + + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { createAlertIssue: { issue: { iid: issueIid } } } }); + findCreateIncidentBtn().trigger('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: createIssueMutation, + variables: { + iid: mockAlert.iid, + projectPath, + }, + }); + }); + + it('shows error alert when incident creation fails ', async () => { + const errorMsg = 'Something went wrong'; + mountComponent({ + mountMethod: mount, + data: { alert: { ...mockAlert, alertIid: 1 } }, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); + findCreateIncidentBtn().trigger('click'); + + await waitForPromises(); + expect(findIncidentCreationAlert().text()).toBe(errorMsg); + }); + }); + + describe('View full alert details', () => { + beforeEach(() => { + mountComponent({ data: { alert: mockAlert } }); + }); + + it('should display a table of raw alert details data', () => { + expect(findDetailsTable().exists()).toBe(true); + }); + }); + + describe('loading state', () => { + beforeEach(() => { + mountComponent({ loading: true }); + }); + + it('displays a loading state when loading', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('error state', () => { + it('displays a error state correctly', () => { + mountComponent({ data: { errored: true } }); + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + + it('renders html-errors correctly', () => { + mountComponent({ + data: { errored: true, sidebarErrorMessage: '<span data-testid="htmlError" />' }, + }); + expect(wrapper.findByTestId('htmlError').exists()).toBe(true); + }); + + it('does not display an error when dismissed', () => { + mountComponent({ data: { errored: true, isErrorDismissed: true } }); + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); + }); + + describe('header', () => { + const findHeader = () => wrapper.findByTestId('alert-header'); + const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } }; + + describe('individual header fields', () => { + describe.each` + createdAt | monitoringTool | result + ${'2020-04-17T23:18:14.996Z'} | ${null} | ${'Alert Reported now'} + ${'2020-04-17T23:18:14.996Z'} | ${'Datadog'} | ${'Alert Reported now by Datadog'} + `( + `When createdAt=$createdAt, monitoringTool=$monitoringTool`, + ({ createdAt, monitoringTool, result }) => { + beforeEach(() => { + mountComponent({ + data: { alert: { ...mockAlert, createdAt, monitoringTool } }, + mountMethod: mount, + stubs, + }); + }); + + it('header text is shown correctly', () => { + expect(findHeader().text()).toBe(result); + }); + }, + ); + }); + }); + + describe('tab navigation', () => { + beforeEach(() => { + mountComponent({ data: { alert: mockAlert } }); + }); + + it.each` + index | tabId + ${0} | ${'overview'} + ${1} | ${'metrics'} + ${2} | ${'activity'} + `('will navigate to the correct tab via $tabId', ({ index, tabId }) => { + wrapper.setData({ currentTabIndex: index }); + expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } }); + }); + }); + }); + + describe('Snowplow tracking', () => { + const mountOptions = { + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alert: mockAlert }, + loading: false, + }; + + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + }); + + it('should not track alert details page views when the tracking options do not exist', () => { + mountComponent(mountOptions); + expect(Tracking.event).not.toHaveBeenCalled(); + }); + + it('should track alert details page views when the tracking options exist', () => { + const trackAlertsDetailsViewsOptions = { + category: 'Alert Management', + action: 'view_alert_details', + }; + mountComponent({ ...mountOptions, provide: { trackAlertsDetailsViewsOptions } }); + const { category, action } = trackAlertsDetailsViewsOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js new file mode 100644 index 00000000000..87ad5e36564 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js @@ -0,0 +1,112 @@ +import { mount } from '@vue/test-utils'; +import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; +import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue'; +import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql'; +import mockAlerts from './mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar To Do', () => { + let wrapper; + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(SidebarTodo, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]'); + + describe('updating the alert to do', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertTodo: { + errors: [], + alert: {}, + }, + }, + }; + + describe('adding a todo', () => { + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('renders a button for adding a To-Do', async () => { + await wrapper.vm.$nextTick(); + + expect(findToDoButton().text()).toBe('Add a to do'); + }); + + it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + + findToDoButton().trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: createAlertTodoMutation, + variables: { + iid: '1527542', + projectPath: 'projectPath', + }, + }); + }); + }); + + describe('removing a todo', () => { + beforeEach(() => { + mountComponent({ + data: { alert: { ...mockAlert, todos: { nodes: [{ id: '1234' }] } } }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('renders a Mark As Done button when todo is present', async () => { + await wrapper.vm.$nextTick(); + + expect(findToDoButton().text()).toBe('Mark as done'); + }); + + it('calls `$apollo.mutate` with `todoMarkDoneMutation` mutation and variables containing `id`', async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + + findToDoButton().trigger('click'); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: todoMarkDoneMutation, + update: expect.anything(), + variables: { + id: '1234', + }, + }); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js new file mode 100644 index 00000000000..b5a61a4adc1 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js @@ -0,0 +1,62 @@ +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; +import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue'; + +jest.mock('~/monitoring/stores', () => ({ + monitoringDashboard: {}, +})); + +jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({ + render(h) { + return h('div'); + }, +})); + +describe('Alert Metrics', () => { + let wrapper; + const mock = new MockAdapter(axios); + + function mountComponent({ props } = {}) { + wrapper = shallowMount(AlertMetrics, { + propsData: { + ...props, + }, + }); + } + + const findChart = () => wrapper.find(MetricEmbed); + const findEmptyState = () => wrapper.find({ ref: 'emptyState' }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + afterAll(() => { + mock.restore(); + }); + + describe('Empty state', () => { + it('should display a message when metrics dashboard url is not provided ', () => { + mountComponent(); + expect(findChart().exists()).toBe(false); + expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload."); + }); + }); + + describe('Chart', () => { + it('should be rendered when dashboard url is provided', async () => { + mountComponent({ props: { dashboardUrl: 'metrics.url' } }); + + await waitForPromises(); + await wrapper.vm.$nextTick(); + + expect(findEmptyState().exists()).toBe(false); + expect(findChart().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js new file mode 100644 index 00000000000..a866fc13539 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js @@ -0,0 +1,166 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql'; +import Tracking from '~/tracking'; +import AlertManagementStatus from '~/vue_shared/alert_details/components/alert_status.vue'; +import mockAlerts from './mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('AlertManagementStatus', () => { + let wrapper; + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); + + const selectFirstStatusOption = () => { + findFirstStatusOption().vm.$emit('click'); + + return waitForPromises(); + }; + + function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) { + wrapper = shallowMount(AlertManagementStatus, { + propsData: { + alert: { ...mockAlert }, + projectPath: 'gitlab-org/gitlab', + isSidebar: false, + ...props, + }, + provide, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('updating the alert status', () => { + const iid = '1527542'; + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + iid, + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({}); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findFirstStatusOption().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatusMutation, + variables: { + iid, + status: 'TRIGGERED', + projectPath: 'gitlab-org/gitlab', + }, + }); + }); + + describe('when a request fails', () => { + beforeEach(() => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + }); + + it('emits an error', async () => { + await selectFirstStatusOption(); + + expect(wrapper.emitted('alert-error')[0]).toEqual([ + 'There was an error while updating the status of the alert. Please try again.', + ]); + }); + + it('emits an error when triggered a second time', async () => { + await selectFirstStatusOption(); + await wrapper.vm.$nextTick(); + await selectFirstStatusOption(); + // Should emit two errors [0,1] + expect(wrapper.emitted('alert-error').length > 1).toBe(true); + }); + }); + + it('shows an error when response includes HTML errors', async () => { + const mockUpdatedMutationErrorResult = { + data: { + updateAlertStatus: { + errors: ['<span data-testid="htmlError" />'], + alert: { + iid, + status: 'acknowledged', + }, + }, + }, + }; + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult); + + await selectFirstStatusOption(); + + expect(wrapper.emitted('alert-error').length > 0).toBe(true); + expect(wrapper.emitted('alert-error')[0]).toEqual([ + 'There was an error while updating the status of the alert. <span data-testid="htmlError" />', + ]); + }); + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + }); + + it('should not track alert status updates when the tracking options do not exist', () => { + mountComponent({}); + Tracking.event.mockClear(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); + findFirstStatusOption().vm.$emit('click'); + setImmediate(() => { + expect(Tracking.event).not.toHaveBeenCalled(); + }); + }); + + it('should track alert status updates when the tracking options exist', () => { + const trackAlertStatusUpdateOptions = { + category: 'Alert Management', + action: 'update_alert_status', + label: 'Status', + }; + mountComponent({ provide: { trackAlertStatusUpdateOptions } }); + Tracking.event.mockClear(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); + findFirstStatusOption().vm.$emit('click'); + const status = findFirstStatusOption().text(); + setImmediate(() => { + const { category, action, label } = trackAlertStatusUpdateOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js new file mode 100644 index 00000000000..a2981478954 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js @@ -0,0 +1,40 @@ +import { shallowMount } from '@vue/test-utils'; +import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue'; + +const label = 'a label'; +const value = 'a value'; + +describe('AlertSummaryRow', () => { + let wrapper; + + function mountComponent({ mountMethod = shallowMount, props, defaultSlot } = {}) { + wrapper = mountMethod(AlertSummaryRow, { + propsData: props, + scopedSlots: { + default: defaultSlot, + }, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('Alert Summary Row', () => { + beforeEach(() => { + mountComponent({ + props: { + label, + }, + defaultSlot: `<span class="value">${value}</span>`, + }); + }); + + it('should display a label and a value', () => { + expect(wrapper.text()).toBe(`${label} ${value}`); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/mocks/alerts.json b/spec/frontend/vue_shared/alert_details/mocks/alerts.json new file mode 100644 index 00000000000..5267a4fe50d --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/mocks/alerts.json @@ -0,0 +1,71 @@ +[ + { + "iid": "1527542", + "title": "SyntaxError: Invalid or unexpected token", + "severity": "CRITICAL", + "eventCount": 7, + "createdAt": "2020-04-17T23:18:14.996Z", + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "TRIGGERED", + "assignees": { "nodes": [] }, + "notes": { "nodes": [] }, + "todos": { "nodes": [] } + }, + { + "iid": "1527543", + "title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert", + "severity": "MEDIUM", + "eventCount": 1, + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "ACKNOWLEDGED", + "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] }, + "issueIid": "1", + "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" + }, + "systemNoteIconName": "user" + } + ] + }, + "todos": { "nodes": [] } + }, + { + "iid": "1527544", + "title": "SyntaxError: Invalid or unexpected token", + "severity": "LOW", + "eventCount": 4, + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "RESOLVED", + "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "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" + } + } + ] + }, + "todos": { "nodes": [] } + } +] diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js new file mode 100644 index 00000000000..28646994ed1 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js @@ -0,0 +1,173 @@ +import { GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue'; +import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue'; +import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql'; +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar Assignees', () => { + let wrapper; + let mock; + + function mountComponent({ + data, + users = [], + isDropdownSearching = false, + sidebarCollapsed = true, + loading = false, + stubs = {}, + } = {}) { + wrapper = shallowMount(SidebarAssignees, { + data() { + return { + users, + isDropdownSearching, + }; + }, + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + projectId: '1', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + mock.restore(); + }); + + const findAssigned = () => wrapper.find('[data-testid="assigned-users"]'); + const findUnassigned = () => wrapper.find('[data-testid="unassigned-users"]'); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + alertSetAssignees: { + errors: [], + alert: { + assigneeUsernames: ['root'], + }, + }, + }, + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + const path = '/-/autocomplete/users.json'; + const users = [ + { + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 1, + name: 'User 1', + username: 'root', + }, + { + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 2, + name: 'User 2', + username: 'not-root', + }, + ]; + + mock.onGet(path).replyOnce(200, users); + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + users, + stubs: { + SidebarAssignee, + }, + }); + }); + + it('renders a unassigned option', async () => { + wrapper.setData({ isDropdownSearching: false }); + await wrapper.vm.$nextTick(); + expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned'); + }); + + it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + wrapper.setData({ isDropdownSearching: false }); + + await wrapper.vm.$nextTick(); + wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: AlertSetAssignees, + variables: { + iid: '1527542', + assigneeUsernames: ['root'], + projectPath: 'projectPath', + }, + }); + }); + + it('emits an error when request contains error messages', () => { + wrapper.setData({ isDropdownSearching: false }); + const errorMutationResult = { + data: { + alertSetAssignees: { + errors: ['There was a problem for sure.'], + alert: {}, + }, + }, + }; + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); + return wrapper.vm + .$nextTick() + .then(() => { + const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); + SideBarAssigneeItem.vm.$emit('update-alert-assignees'); + }) + .then(() => { + expect(wrapper.emitted('alert-error')).toBeDefined(); + }); + }); + + it('stops updating and cancels loading when the request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + wrapper.vm.updateAlertAssignees('root'); + expect(findUnassigned().text()).toBe('assign yourself'); + }); + + it('shows a user avatar, username and full name when a user is set', () => { + mountComponent({ + data: { alert: mockAlerts[1] }, + sidebarCollapsed: false, + loading: false, + stubs: { + SidebarAssignee, + }, + }); + + expect(findAssigned().find('img').attributes('src')).toBe('/url'); + expect(findAssigned().find('.dropdown-menu-user-full-name').text()).toBe('root'); + expect(findAssigned().find('.dropdown-menu-user-username').text()).toBe('@root'); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js new file mode 100644 index 00000000000..70cf2597963 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js @@ -0,0 +1,95 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue'; +import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue'; +import SidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue'; +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar', () => { + let wrapper; + let mock; + + function mountComponent({ + mountMethod = shallowMount, + stubs = {}, + alert = {}, + provide = {}, + } = {}) { + wrapper = mountMethod(AlertSidebar, { + data() { + return { + sidebarStatus: false, + }; + }, + propsData: { + alert, + }, + provide: { + projectPath: 'projectPath', + projectId: '1', + ...provide, + }, + stubs, + mocks: { + $apollo: { + queries: { + sidebarStatus: {}, + }, + }, + }, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + mock.restore(); + }); + + describe('the sidebar renders', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mountComponent(); + }); + + it('open as default', () => { + expect(wrapper.classes('right-sidebar-expanded')).toBe(true); + }); + + it('should render side bar assignee dropdown', () => { + mountComponent({ + mountMethod: mount, + alert: mockAlert, + }); + expect(wrapper.find(SidebarAssignees).exists()).toBe(true); + }); + + it('should render side bar status dropdown', () => { + mountComponent({ + mountMethod: mount, + alert: mockAlert, + }); + expect(wrapper.find(SidebarStatus).exists()).toBe(true); + }); + }); + + describe('the sidebar renders for threat monitoring', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mountComponent(); + }); + + it('should not render side bar status dropdown', () => { + mountComponent({ + mountMethod: mount, + alert: mockAlert, + provide: { isThreatMonitoringPage: true }, + }); + expect(wrapper.find(SidebarStatus).exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js new file mode 100644 index 00000000000..f5b9efb4d98 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js @@ -0,0 +1,103 @@ +import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql'; +import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue'; +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar Status', () => { + let wrapper; + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); + const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findStatusDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]'); + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(AlertSidebarStatus, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Alert Sidebar Dropdown Status', () => { + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('displays status dropdown', () => { + expect(findStatusDropdown().exists()).toBe(true); + }); + + it('displays the dropdown status header', () => { + expect(findStatusDropdownHeader().exists()).toBe(true); + }); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findStatusDropdownItem().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatusMutation, + variables: { + iid: '1527542', + status: 'TRIGGERED', + projectPath: 'projectPath', + }, + }); + }); + + it('stops updating when the request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findStatusDropdownItem().vm.$emit('click'); + expect(findStatusLoadingIcon().exists()).toBe(false); + expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js new file mode 100644 index 00000000000..a5a9fb55737 --- /dev/null +++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js @@ -0,0 +1,40 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SystemNote from '~/vue_shared/alert_details/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(); + wrapper = null; + } + }); + + describe('System notes', () => { + beforeEach(() => { + mountComponent({}); + }); + + it('renders the correct system note', () => { + const noteId = wrapper.find('.note-wrapper').attributes('id'); + const iconName = wrapper.find(GlIcon).attributes('name'); + + expect(noteId).toBe('note_1628'); + expect(iconName).toBe(mockAlert.notes.nodes[0].systemNoteIconName); + }); + }); +}); |