summaryrefslogtreecommitdiff
path: root/spec/frontend/monitoring/alert_widget_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/monitoring/alert_widget_spec.js')
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js422
1 files changed, 422 insertions, 0 deletions
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
new file mode 100644
index 00000000000..f0355dfa01b
--- /dev/null
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -0,0 +1,422 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+
+const mockReadAlert = jest.fn();
+const mockCreateAlert = jest.fn();
+const mockUpdateAlert = jest.fn();
+const mockDeleteAlert = jest.fn();
+
+jest.mock('~/flash');
+jest.mock(
+ '~/monitoring/services/alerts_service',
+ () =>
+ function AlertsServiceMock() {
+ return {
+ readAlert: mockReadAlert,
+ createAlert: mockCreateAlert,
+ updateAlert: mockUpdateAlert,
+ deleteAlert: mockDeleteAlert,
+ };
+ },
+);
+
+describe('AlertWidget', () => {
+ let wrapper;
+
+ const nonFiringAlertResult = [
+ {
+ values: [[0, 1], [1, 42], [2, 41]],
+ },
+ ];
+ const firingAlertResult = [
+ {
+ values: [[0, 42], [1, 43], [2, 44]],
+ },
+ ];
+ const metricId = '5';
+ const alertPath = 'my/alert.json';
+
+ const relevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: nonFiringAlertResult,
+ },
+ ];
+
+ const firingRelevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: firingAlertResult,
+ },
+ ];
+
+ const defaultProps = {
+ alertsEndpoint: '',
+ relevantQueries,
+ alertsToManage: {},
+ modalId: 'alert-modal-1',
+ };
+
+ const propsWithAlert = {
+ relevantQueries,
+ };
+
+ const propsWithAlertData = {
+ relevantQueries,
+ alertsToManage: {
+ [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
+ },
+ };
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(AlertWidget, {
+ stubs: { GlTooltip, GlSprintf },
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ });
+ };
+ const hasLoadingIcon = () => wrapper.contains(GlLoadingIcon);
+ const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
+ const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
+ const findCurrentSettingsText = () =>
+ wrapper
+ .find({ ref: 'alertCurrentSetting' })
+ .text()
+ .replace(/\s\s+/g, ' ');
+ const findBadge = () => wrapper.find(GlBadge);
+ const findTooltip = () => wrapper.find(GlTooltip);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays a loading spinner and disables form when fetching alerts', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent(defaultProps);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(true);
+ expect(findWidgetForm().props('disabled')).toBe(true);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(false);
+ expect(findWidgetForm().props('disabled')).toBe(false);
+ });
+ });
+
+ it('does not render loading spinner if showLoadingState is false', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent({
+ ...defaultProps,
+ showLoadingState: false,
+ });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ it('displays an error message when fetch fails', () => {
+ mockReadAlert.mockRejectedValue();
+ createComponent(propsWithAlert);
+ expect(hasLoadingIcon()).toBe(true);
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(hasLoadingIcon()).toBe(false);
+ });
+ });
+
+ describe('Alert not firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...relevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied');
+ });
+ });
+ });
+
+ describe('Alert firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
+ });
+ });
+
+ it('should display tooltip with thresholds summary', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(
+ findTooltip()
+ .text()
+ .replace(/\s\s+/g, ' '),
+ ).toEqual('Firing: alert-label > 42');
+ });
+ });
+ });
+
+ it('creates an alert with an appropriate handler', () => {
+ const alertParams = {
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ };
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const fakeAlertPath = 'foo/bar';
+ mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
+ createComponent({
+ alertsToManage: {
+ [fakeAlertPath]: {
+ alert_path: fakeAlertPath,
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('create', alertParams);
+
+ expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
+ });
+
+ it('updates an alert with an appropriate handler', () => {
+ const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
+ const newAlertParams = { operator: '==', threshold: 12 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
+ createComponent({
+ ...propsWithAlertData,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '==',
+ threshold: 12,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('update', {
+ alert: alertPath,
+ ...newAlertParams,
+ prometheus_metric_id: '5',
+ });
+
+ expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
+ });
+
+ it('deletes an alert with an appropriate handler', () => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockResolvedValue({});
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+
+ describe('when delete fails', () => {
+ beforeEach(() => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockRejectedValue();
+
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows error message', () => {
+ expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
+ });
+
+ it('dismisses error message on cancel', () => {
+ findWidgetForm().vm.$emit('cancel');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+ });
+});