actions_spec.js 23.8 KB
Newer Older
1
import MockAdapter from 'axios-mock-adapter';
2
import testAction from 'helpers/vuex_action_helper';
3
import Tracking from '~/tracking';
4
import axios from '~/lib/utils/axios_utils';
5
import statusCodes from '~/lib/utils/http_status';
6
import * as commonUtils from '~/lib/utils/common_utils';
7
import createFlash from '~/flash';
8
import { defaultTimeRange } from '~/vue_shared/constants';
9
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
10

11 12 13 14 15 16 17
import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
  fetchDashboard,
  receiveMetricsDashboardSuccess,
  fetchDeploymentsData,
  fetchEnvironmentsData,
18
  fetchDashboardData,
19
  fetchAnnotations,
20
  fetchPrometheusMetric,
21
  setInitialState,
22
  filterEnvironments,
23
  setGettingStartedEmptyState,
24
  duplicateSystemDashboard,
25
} from '~/monitoring/stores/actions';
26 27 28 29 30
import {
  gqClient,
  parseEnvironmentsResponse,
  parseAnnotationsResponse,
} from '~/monitoring/stores/utils';
31
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
32
import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
33 34 35 36
import storeState from '~/monitoring/stores/state';
import {
  deploymentData,
  environmentData,
37
  annotationsData,
38
  dashboardGitResponse,
39
  mockDashboardsErrorResponse,
40
} from '../mock_data';
41 42 43 44 45
import {
  metricsDashboardResponse,
  metricsDashboardViewModel,
  metricsDashboardPanelCount,
} from '../fixture_data';
46

47
jest.mock('~/flash');
48

49 50 51 52 53 54 55 56
const resetStore = str => {
  str.replaceState({
    showEmptyState: true,
    emptyState: 'loading',
    groups: [],
  });
};

57
describe('Monitoring store actions', () => {
58 59
  const { convertObjectPropsToCamelCase } = commonUtils;

60
  let mock;
61

62 63 64
  beforeEach(() => {
    mock = new MockAdapter(axios);

65
    jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
66 67 68 69 70 71
      const q = new Promise((resolve, reject) => {
        const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
        const next = () => callback(next, stop);
        // Define a timeout based on a mock timer
        setTimeout(() => {
          callback(next, stop);
72
        });
73 74 75 76
      });
      // Run all resolved promises in chain
      jest.runOnlyPendingTimers();
      return q;
77 78
    });
  });
79 80
  afterEach(() => {
    resetStore(store);
81 82
    mock.reset();

83
    commonUtils.backOff.mockReset();
84
    createFlash.mockReset();
85
  });
86

87
  describe('fetchDeploymentsData', () => {
88
    it('dispatches receiveDeploymentsDataSuccess on success', () => {
89 90 91 92 93
      const { state } = store;
      state.deploymentsEndpoint = '/success';
      mock.onGet(state.deploymentsEndpoint).reply(200, {
        deployments: deploymentData,
      });
94 95 96 97

      return testAction(
        fetchDeploymentsData,
        null,
98
        state,
99 100 101
        [],
        [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }],
      );
102
    });
103
    it('dispatches receiveDeploymentsDataFailure on error', () => {
104 105 106
      const { state } = store;
      state.deploymentsEndpoint = '/error';
      mock.onGet(state.deploymentsEndpoint).reply(500);
107 108 109 110

      return testAction(
        fetchDeploymentsData,
        null,
111
        state,
112 113 114 115 116 117
        [],
        [{ type: 'receiveDeploymentsDataFailure' }],
        () => {
          expect(createFlash).toHaveBeenCalled();
        },
      );
118 119
    });
  });
120

121
  describe('fetchEnvironmentsData', () => {
122 123 124 125 126 127 128 129
    const { state } = store;
    state.projectPath = 'gitlab-org/gitlab-test';

    afterEach(() => {
      resetStore(store);
    });

    it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
130 131 132 133 134
      jest.spyOn(gqClient, 'mutate').mockReturnValue({
        data: {
          project: {
            data: {
              environments: [],
135 136
            },
          },
137 138
        },
      });
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156

      return testAction(
        filterEnvironments,
        {},
        state,
        [
          {
            type: 'SET_ENVIRONMENTS_FILTER',
            payload: {},
          },
        ],
        [
          {
            type: 'fetchEnvironmentsData',
          },
        ],
      );
    });
157

158 159 160 161 162 163 164 165
    it('fetch environments data call takes in search param', () => {
      const mockMutate = jest.spyOn(gqClient, 'mutate');
      const searchTerm = 'Something';
      const mutationVariables = {
        mutation: getEnvironments,
        variables: {
          projectPath: state.projectPath,
          search: searchTerm,
166
          states: [ENVIRONMENT_AVAILABLE_STATE],
167 168 169
        },
      };
      state.environmentsSearchTerm = searchTerm;
170
      mockMutate.mockResolvedValue({});
171

172 173 174
      return testAction(
        fetchEnvironmentsData,
        null,
175
        state,
176
        [],
177 178 179 180
        [
          { type: 'requestEnvironmentsData' },
          { type: 'receiveEnvironmentsDataSuccess', payload: [] },
        ],
181 182 183 184
        () => {
          expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
        },
      );
185 186
    });

187
    it('dispatches receiveEnvironmentsDataSuccess on success', () => {
188 189 190 191 192
      jest.spyOn(gqClient, 'mutate').mockResolvedValue({
        data: {
          project: {
            data: {
              environments: environmentData,
193 194
            },
          },
195 196
        },
      });
197

198 199 200
      return testAction(
        fetchEnvironmentsData,
        null,
201
        state,
202 203 204 205 206 207 208 209 210
        [],
        [
          { type: 'requestEnvironmentsData' },
          {
            type: 'receiveEnvironmentsDataSuccess',
            payload: parseEnvironmentsResponse(environmentData, state.projectPath),
          },
        ],
      );
211
    });
212

213
    it('dispatches receiveEnvironmentsDataFailure on error', () => {
214
      jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
215

216 217 218
      return testAction(
        fetchEnvironmentsData,
        null,
219
        state,
220 221 222
        [],
        [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
      );
223 224
    });
  });
225

226 227
  describe('fetchAnnotations', () => {
    const { state } = store;
228 229 230 231
    state.timeRange = {
      start: '2020-04-15T12:54:32.137Z',
      end: '2020-08-15T12:54:32.137Z',
    };
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    state.projectPath = 'gitlab-org/gitlab-test';
    state.currentEnvironmentName = 'production';
    state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';

    afterEach(() => {
      resetStore(store);
    });

    it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
      const mockMutate = jest.spyOn(gqClient, 'mutate');
      const mutationVariables = {
        mutation: getAnnotations,
        variables: {
          projectPath: state.projectPath,
          environmentName: state.currentEnvironmentName,
247 248
          dashboardPath: state.currentDashboard,
          startingFrom: state.timeRange.start,
249 250
        },
      };
251
      const parsedResponse = parseAnnotationsResponse(annotationsData);
252 253 254 255

      mockMutate.mockResolvedValue({
        data: {
          project: {
256 257 258 259 260 261 262 263 264 265
            environments: {
              nodes: [
                {
                  metricsDashboard: {
                    annotations: {
                      nodes: parsedResponse,
                    },
                  },
                },
              ],
266 267 268 269 270 271 272 273 274 275
            },
          },
        },
      });

      return testAction(
        fetchAnnotations,
        null,
        state,
        [],
276
        [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
277 278 279 280 281 282 283 284 285 286 287 288 289
        () => {
          expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
        },
      );
    });

    it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
      const mockMutate = jest.spyOn(gqClient, 'mutate');
      const mutationVariables = {
        mutation: getAnnotations,
        variables: {
          projectPath: state.projectPath,
          environmentName: state.currentEnvironmentName,
290 291
          dashboardPath: state.currentDashboard,
          startingFrom: state.timeRange.start,
292 293 294 295 296 297 298 299 300 301
        },
      };

      mockMutate.mockRejectedValue({});

      return testAction(
        fetchAnnotations,
        null,
        state,
        [],
302
        [{ type: 'receiveAnnotationsFailure' }],
303 304 305 306 307 308 309
        () => {
          expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
        },
      );
    });
  });

310
  describe('Set initial state', () => {
311 312 313 314
    let mockedState;
    beforeEach(() => {
      mockedState = storeState();
    });
315
    it('should commit SET_INITIAL_STATE mutation', done => {
316
      testAction(
317
        setInitialState,
318 319 320 321 322 323 324
        {
          metricsEndpoint: 'additional_metrics.json',
          deploymentsEndpoint: 'deployments.json',
        },
        mockedState,
        [
          {
325
            type: types.SET_INITIAL_STATE,
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
            payload: {
              metricsEndpoint: 'additional_metrics.json',
              deploymentsEndpoint: 'deployments.json',
            },
          },
        ],
        [],
        done,
      );
    });
  });
  describe('Set empty states', () => {
    let mockedState;
    beforeEach(() => {
      mockedState = storeState();
    });
    it('should commit SET_METRICS_ENDPOINT mutation', done => {
      testAction(
        setGettingStartedEmptyState,
        null,
        mockedState,
        [
          {
            type: types.SET_GETTING_STARTED_EMPTY_STATE,
          },
        ],
        [],
        done,
      );
    });
  });
  describe('fetchDashboard', () => {
    let dispatch;
    let state;
360
    let commit;
361 362 363
    const response = metricsDashboardResponse;
    beforeEach(() => {
      dispatch = jest.fn();
364
      commit = jest.fn();
365 366 367
      state = storeState();
      state.dashboardEndpoint = '/dashboard';
    });
368 369

    it('on success, dispatches receive and success actions', () => {
370
      document.body.dataset.page = 'projects:environments:metrics';
371
      mock.onGet(state.dashboardEndpoint).reply(200, response);
372 373 374 375 376 377 378 379 380 381 382 383 384 385

      return testAction(
        fetchDashboard,
        null,
        state,
        [],
        [
          { type: 'requestMetricsDashboard' },
          {
            type: 'receiveMetricsDashboardSuccess',
            payload: { response },
          },
        ],
      );
386
    });
387 388 389 390 391 392

    describe('on failure', () => {
      let result;
      beforeEach(() => {
        const params = {};
        result = () => {
393 394
          mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
          return fetchDashboard({ state, commit, dispatch }, params);
395 396 397
        };
      });

398
      it('dispatches a failure', done => {
399 400
        result()
          .then(() => {
401 402 403 404
            expect(commit).toHaveBeenCalledWith(
              types.SET_ALL_DASHBOARDS,
              mockDashboardsErrorResponse.all_dashboards,
            );
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
            expect(dispatch).toHaveBeenCalledWith(
              'receiveMetricsDashboardFailure',
              new Error('Request failed with status code 500'),
            );
            expect(createFlash).toHaveBeenCalled();
            done();
          })
          .catch(done.fail);
      });

      it('dispatches a failure action when a message is returned', done => {
        result()
          .then(() => {
            expect(dispatch).toHaveBeenCalledWith(
              'receiveMetricsDashboardFailure',
              new Error('Request failed with status code 500'),
            );
422 423 424
            expect(createFlash).toHaveBeenCalledWith(
              expect.stringContaining(mockDashboardsErrorResponse.message),
            );
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
            done();
          })
          .catch(done.fail);
      });

      it('does not show a flash error when showErrorBanner is disabled', done => {
        state.showErrorBanner = false;

        result()
          .then(() => {
            expect(dispatch).toHaveBeenCalledWith(
              'receiveMetricsDashboardFailure',
              new Error('Request failed with status code 500'),
            );
            expect(createFlash).not.toHaveBeenCalled();
            done();
          })
          .catch(done.fail);
      });
444 445 446 447 448 449
    });
  });
  describe('receiveMetricsDashboardSuccess', () => {
    let commit;
    let dispatch;
    let state;
450

451 452 453 454 455
    beforeEach(() => {
      commit = jest.fn();
      dispatch = jest.fn();
      state = storeState();
    });
456 457

    it('stores groups', () => {
458
      const response = metricsDashboardResponse;
459
      receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
460
      expect(commit).toHaveBeenCalledWith(
461
        types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
462

463
        metricsDashboardResponse.dashboard,
464
      );
465
      expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
    });
    it('sets the dashboards loaded from the repository', () => {
      const params = {};
      const response = metricsDashboardResponse;
      response.all_dashboards = dashboardGitResponse;
      receiveMetricsDashboardSuccess(
        {
          state,
          commit,
          dispatch,
        },
        {
          response,
          params,
        },
      );
      expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
    });
  });
485
  describe('fetchDashboardData', () => {
486 487
    let commit;
    let dispatch;
488 489
    let state;

490
    beforeEach(() => {
491
      jest.spyOn(Tracking, 'event');
492 493
      commit = jest.fn();
      dispatch = jest.fn();
494
      state = storeState();
495 496

      state.timeRange = defaultTimeRange;
497
    });
498

499
    it('commits empty state when state.groups is empty', done => {
500 501 502
      const getters = {
        metricsWithData: () => [],
      };
503
      fetchDashboardData({ state, commit, dispatch, getters })
504
        .then(() => {
505 506 507 508 509 510 511 512 513
          expect(Tracking.event).toHaveBeenCalledWith(
            document.body.dataset.page,
            'dashboard_fetch',
            {
              label: 'custom_metrics_dashboard',
              property: 'count',
              value: 0,
            },
          );
514 515 516
          expect(dispatch).toHaveBeenCalledTimes(1);
          expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');

517
          expect(createFlash).not.toHaveBeenCalled();
518 519 520 521 522
          done();
        })
        .catch(done.fail);
    });
    it('dispatches fetchPrometheusMetric for each panel query', done => {
523 524 525 526 527
      state.dashboard.panelGroups = convertObjectPropsToCamelCase(
        metricsDashboardResponse.dashboard.panel_groups,
      );

      const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
528 529 530 531
      const getters = {
        metricsWithData: () => [metric.id],
      };

532
      fetchDashboardData({ state, commit, dispatch, getters })
533 534 535
        .then(() => {
          expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
            metric,
536 537 538 539 540
            defaultQueryParams: {
              start_time: expect.any(String),
              end_time: expect.any(String),
              step: expect.any(Number),
            },
541 542
          });

543 544 545 546 547 548 549 550 551
          expect(Tracking.event).toHaveBeenCalledWith(
            document.body.dataset.page,
            'dashboard_fetch',
            {
              label: 'custom_metrics_dashboard',
              property: 'count',
              value: 1,
            },
          );
552 553 554 555 556 557 558 559

          done();
        })
        .catch(done.fail);
      done();
    });

    it('dispatches fetchPrometheusMetric for each panel query, handles an error', done => {
560 561
      state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups;
      const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
562

563
      dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
564
      // Mock having one out of four metrics failing
565 566 567
      dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
      dispatch.mockResolvedValue();

568
      fetchDashboardData({ state, commit, dispatch })
569
        .then(() => {
570
          expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 1); // plus 1 for deployments
571
          expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
572 573
          expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
            metric,
574 575 576 577 578
            defaultQueryParams: {
              start_time: expect.any(String),
              end_time: expect.any(String),
              step: expect.any(Number),
            },
579
          });
580 581 582

          expect(createFlash).toHaveBeenCalledTimes(1);

583 584 585 586 587 588 589
          done();
        })
        .catch(done.fail);
      done();
    });
  });
  describe('fetchPrometheusMetric', () => {
590
    const defaultQueryParams = {
591 592
      start_time: '2019-08-06T12:40:02.184Z',
      end_time: '2019-08-06T20:40:02.184Z',
593
      step: 60,
594 595 596 597
    };
    let metric;
    let state;
    let data;
598
    let prometheusEndpointPath;
599 600 601

    beforeEach(() => {
      state = storeState();
602 603 604
      [metric] = metricsDashboardViewModel.panelGroups[0].panels[0].metrics;

      prometheusEndpointPath = metric.prometheusEndpointPath;
605 606 607 608 609

      data = {
        metricId: metric.metricId,
        result: [1582065167.353, 5, 1582065599.353],
      };
610 611 612
    });

    it('commits result', done => {
613
      mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
614

615 616
      testAction(
        fetchPrometheusMetric,
617
        { metric, defaultQueryParams },
618 619 620 621 622
        state,
        [
          {
            type: types.REQUEST_METRIC_RESULT,
            payload: {
623
              metricId: metric.metricId,
624 625 626 627 628
            },
          },
          {
            type: types.RECEIVE_METRIC_RESULT_SUCCESS,
            payload: {
629
              metricId: metric.metricId,
630 631 632 633 634 635
              result: data.result,
            },
          },
        ],
        [],
        () => {
636
          expect(mock.history.get).toHaveLength(1);
637
          done();
638 639
        },
      ).catch(done.fail);
640
    });
641

642 643 644 645 646 647 648 649
    describe('without metric defined step', () => {
      const expectedParams = {
        start_time: '2019-08-06T12:40:02.184Z',
        end_time: '2019-08-06T20:40:02.184Z',
        step: 60,
      };

      it('uses calculated step', done => {
650
        mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
651 652 653

        testAction(
          fetchPrometheusMetric,
654
          { metric, defaultQueryParams },
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
          state,
          [
            {
              type: types.REQUEST_METRIC_RESULT,
              payload: {
                metricId: metric.metricId,
              },
            },
            {
              type: types.RECEIVE_METRIC_RESULT_SUCCESS,
              payload: {
                metricId: metric.metricId,
                result: data.result,
              },
            },
          ],
          [],
          () => {
            expect(mock.history.get[0].params).toEqual(expectedParams);
            done();
          },
        ).catch(done.fail);
      });
    });

    describe('with metric defined step', () => {
      beforeEach(() => {
        metric.step = 7;
      });

      const expectedParams = {
        start_time: '2019-08-06T12:40:02.184Z',
        end_time: '2019-08-06T20:40:02.184Z',
        step: 7,
      };

      it('uses metric step', done => {
692
        mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
693 694 695

        testAction(
          fetchPrometheusMetric,
696
          { metric, defaultQueryParams },
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
          state,
          [
            {
              type: types.REQUEST_METRIC_RESULT,
              payload: {
                metricId: metric.metricId,
              },
            },
            {
              type: types.RECEIVE_METRIC_RESULT_SUCCESS,
              payload: {
                metricId: metric.metricId,
                result: data.result,
              },
            },
          ],
          [],
          () => {
            expect(mock.history.get[0].params).toEqual(expectedParams);
            done();
          },
        ).catch(done.fail);
      });
    });

722 723
    it('commits result, when waiting for results', done => {
      // Mock multiple attempts while the cache is filling up
724 725 726 727
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt
728

729 730
      testAction(
        fetchPrometheusMetric,
731
        { metric, defaultQueryParams },
732 733 734 735 736
        state,
        [
          {
            type: types.REQUEST_METRIC_RESULT,
            payload: {
737
              metricId: metric.metricId,
738 739 740 741 742
            },
          },
          {
            type: types.RECEIVE_METRIC_RESULT_SUCCESS,
            payload: {
743
              metricId: metric.metricId,
744 745 746 747 748 749
              result: data.result,
            },
          },
        ],
        [],
        () => {
750 751
          expect(mock.history.get).toHaveLength(4);
          done();
752 753
        },
      ).catch(done.fail);
754 755 756 757
    });

    it('commits failure, when waiting for results and getting a server error', done => {
      // Mock multiple attempts while the cache is filling up and fails
758 759 760 761
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
      mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt
762

763 764 765 766
      const error = new Error('Request failed with status code 500');

      testAction(
        fetchPrometheusMetric,
767
        { metric, defaultQueryParams },
768 769 770 771 772
        state,
        [
          {
            type: types.REQUEST_METRIC_RESULT,
            payload: {
773
              metricId: metric.metricId,
774 775 776
            },
          },
          {
777
            type: types.RECEIVE_METRIC_RESULT_FAILURE,
778
            payload: {
779
              metricId: metric.metricId,
780 781 782 783 784 785 786 787 788 789
              error,
            },
          },
        ],
        [],
      ).catch(e => {
        expect(mock.history.get).toHaveLength(4);
        expect(e).toEqual(error);
        done();
      });
790
    });
791
  });
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872

  describe('duplicateSystemDashboard', () => {
    let state;

    beforeEach(() => {
      state = storeState();
      state.dashboardsEndpoint = '/dashboards.json';
    });

    it('Succesful POST request resolves', done => {
      mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
        dashboard: dashboardGitResponse[1],
      });

      testAction(duplicateSystemDashboard, {}, state, [], [])
        .then(() => {
          expect(mock.history.post).toHaveLength(1);
          done();
        })
        .catch(done.fail);
    });

    it('Succesful POST request resolves to a dashboard', done => {
      const mockCreatedDashboard = dashboardGitResponse[1];

      const params = {
        dashboard: 'my-dashboard',
        fileName: 'file-name.yml',
        branch: 'my-new-branch',
        commitMessage: 'A new commit message',
      };

      const expectedPayload = JSON.stringify({
        dashboard: 'my-dashboard',
        file_name: 'file-name.yml',
        branch: 'my-new-branch',
        commit_message: 'A new commit message',
      });

      mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
        dashboard: mockCreatedDashboard,
      });

      testAction(duplicateSystemDashboard, params, state, [], [])
        .then(result => {
          expect(mock.history.post).toHaveLength(1);
          expect(mock.history.post[0].data).toEqual(expectedPayload);
          expect(result).toEqual(mockCreatedDashboard);

          done();
        })
        .catch(done.fail);
    });

    it('Failed POST request throws an error', done => {
      mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST);

      testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
        expect(mock.history.post).toHaveLength(1);
        expect(err).toEqual(expect.any(String));

        done();
      });
    });

    it('Failed POST request throws an error with a description', done => {
      const backendErrorMsg = 'This file already exists!';

      mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST, {
        error: backendErrorMsg,
      });

      testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
        expect(mock.history.post).toHaveLength(1);
        expect(err).toEqual(expect.any(String));
        expect(err).toEqual(expect.stringContaining(backendErrorMsg));

        done();
      });
    });
  });
873
});