423 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			423 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
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);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |