diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-07 18:09:19 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-07 18:09:19 +0000 |
commit | 3290d46655f07d7ae3dca788d6df9f326972ffd8 (patch) | |
tree | 0d24713e1592cdd3583257f14a52d46a22539ed1 /spec | |
parent | c6b3ec3f56fa32a0e0ed3de0d0878d25f1adaddf (diff) | |
download | gitlab-ce-3290d46655f07d7ae3dca788d6df9f326972ffd8.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
19 files changed, 719 insertions, 142 deletions
diff --git a/spec/factories/metrics/dashboard/annotations.rb b/spec/factories/metrics/dashboard/annotations.rb new file mode 100644 index 00000000000..2e5c373918e --- /dev/null +++ b/spec/factories/metrics/dashboard/annotations.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :metrics_dashboard_annotation, class: '::Metrics::Dashboard::Annotation' do + description { "Dashbaord annoation description" } + dashboard_path { "custom_dashbaord.yml" } + starting_at { Time.current } + environment + + trait :with_cluster do + cluster + environment { nil } + end + end +end diff --git a/spec/features/issues/user_sorts_issue_comments_spec.rb b/spec/features/issues/user_sorts_issue_comments_spec.rb new file mode 100644 index 00000000000..e1c0acc32f1 --- /dev/null +++ b/spec/features/issues/user_sorts_issue_comments_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Comment sort direction' do + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:comment_1) { create(:note_on_issue, noteable: issue, project: project, note: 'written first') } + let_it_be(:comment_2) { create(:note_on_issue, noteable: issue, project: project, note: 'written second') } + + context 'on issue page', :js do + before do + visit project_issue_path(project, issue) + end + + it 'saves sort order' do + # open dropdown, and select 'Newest first' + page.within('.issuable-details') do + click_button('Oldest first') + click_button('Newest first') + end + + expect(first_comment).to have_content(comment_2.note) + expect(last_comment).to have_content(comment_1.note) + + visit project_issue_path(project, issue) + wait_for_requests + + expect(first_comment).to have_content(comment_2.note) + expect(last_comment).to have_content(comment_1.note) + end + end + + def all_comments + all('.timeline > .note.timeline-entry') + end + + def first_comment + all_comments.first + end + + def last_comment + all_comments.last + end +end diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 84b74ef659e..f2478a583dc 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -65,7 +65,7 @@ describe('Time series component', () => { store = createStore(); store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, metricsDashboardPayload, ); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index f2c3b199481..f0b510a01f4 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -202,7 +202,7 @@ describe('Dashboard', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, metricsDashboardPayload, ); wrapper.vm.$store.commit( diff --git a/spec/frontend/monitoring/init_utils.js b/spec/frontend/monitoring/init_utils.js index 36c654ba7b3..55b6199fdfc 100644 --- a/spec/frontend/monitoring/init_utils.js +++ b/spec/frontend/monitoring/init_utils.js @@ -32,7 +32,7 @@ export const propsData = { export const setupComponentStore = wrapper => { wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, metricsDashboardPayload, ); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 9f0b4d16fc1..7c559aed2c5 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -5,13 +5,13 @@ import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import * as commonUtils from '~/lib/utils/common_utils'; import createFlash from '~/flash'; +import { defaultTimeRange } from '~/vue_shared/constants'; import store from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; import { fetchDashboard, receiveMetricsDashboardSuccess, - receiveMetricsDashboardFailure, fetchDeploymentsData, fetchEnvironmentsData, fetchPrometheusMetrics, @@ -77,42 +77,40 @@ describe('Monitoring store actions', () => { }); describe('fetchDeploymentsData', () => { - it('commits RECEIVE_DEPLOYMENTS_DATA_SUCCESS on error', done => { - const dispatch = jest.fn(); + it('dispatches receiveDeploymentsDataSuccess on success', () => { const { state } = store; state.deploymentsEndpoint = '/success'; mock.onGet(state.deploymentsEndpoint).reply(200, { deployments: deploymentData, }); - fetchDeploymentsData({ + + return testAction( + fetchDeploymentsData, + null, state, - dispatch, - }) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataSuccess', deploymentData); - done(); - }) - .catch(done.fail); + [], + [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }], + ); }); - it('commits RECEIVE_DEPLOYMENTS_DATA_FAILURE on error', done => { - const dispatch = jest.fn(); + it('dispatches receiveDeploymentsDataFailure on error', () => { const { state } = store; state.deploymentsEndpoint = '/error'; mock.onGet(state.deploymentsEndpoint).reply(500); - fetchDeploymentsData({ + + return testAction( + fetchDeploymentsData, + null, state, - dispatch, - }) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataFailure'); - done(); - }) - .catch(done.fail); + [], + [{ type: 'receiveDeploymentsDataFailure' }], + () => { + expect(createFlash).toHaveBeenCalled(); + }, + ); }); }); describe('fetchEnvironmentsData', () => { - const dispatch = jest.fn(); const { state } = store; state.projectPath = 'gitlab-org/gitlab-test'; @@ -164,15 +162,19 @@ describe('Monitoring store actions', () => { state.environmentsSearchTerm = searchTerm; mockMutate.mockReturnValue(Promise.resolve()); - return fetchEnvironmentsData({ + return testAction( + fetchEnvironmentsData, + null, state, - dispatch, - }).then(() => { - expect(mockMutate).toHaveBeenCalledWith(mutationVariables); - }); + [], + [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }], + () => { + expect(mockMutate).toHaveBeenCalledWith(mutationVariables); + }, + ); }); - it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on success', () => { + it('dispatches receiveEnvironmentsDataSuccess on success', () => { jest.spyOn(gqClient, 'mutate').mockReturnValue( Promise.resolve({ data: { @@ -185,26 +187,31 @@ describe('Monitoring store actions', () => { }), ); - return fetchEnvironmentsData({ + return testAction( + fetchEnvironmentsData, + null, state, - dispatch, - }).then(() => { - expect(dispatch).toHaveBeenCalledWith( - 'receiveEnvironmentsDataSuccess', - parseEnvironmentsResponse(environmentData, state.projectPath), - ); - }); + [], + [ + { type: 'requestEnvironmentsData' }, + { + type: 'receiveEnvironmentsDataSuccess', + payload: parseEnvironmentsResponse(environmentData, state.projectPath), + }, + ], + ); }); - it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', () => { + it('dispatches receiveEnvironmentsDataFailure on error', () => { jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject()); - return fetchEnvironmentsData({ + return testAction( + fetchEnvironmentsData, + null, state, - dispatch, - }).then(() => { - expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataFailure'); - }); + [], + [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }], + ); }); }); @@ -266,27 +273,24 @@ describe('Monitoring store actions', () => { state = storeState(); state.dashboardEndpoint = '/dashboard'; }); - it('on success, dispatches receive and success actions', done => { - const params = {}; + + it('on success, dispatches receive and success actions', () => { document.body.dataset.page = 'projects:environments:metrics'; mock.onGet(state.dashboardEndpoint).reply(200, response); - fetchDashboard( - { - state, - commit, - dispatch, - }, - params, - ) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('requestMetricsDashboard'); - expect(dispatch).toHaveBeenCalledWith('receiveMetricsDashboardSuccess', { - response, - params, - }); - done(); - }) - .catch(done.fail); + + return testAction( + fetchDashboard, + null, + state, + [], + [ + { type: 'requestMetricsDashboard' }, + { + type: 'receiveMetricsDashboardSuccess', + payload: { response }, + }, + ], + ); }); describe('on failure', () => { @@ -299,7 +303,7 @@ describe('Monitoring store actions', () => { }; }); - it('dispatches a failure action', done => { + it('dispatches a failure', done => { result() .then(() => { expect(commit).toHaveBeenCalledWith( @@ -351,31 +355,22 @@ describe('Monitoring store actions', () => { let commit; let dispatch; let state; + beforeEach(() => { commit = jest.fn(); dispatch = jest.fn(); state = storeState(); }); - it('stores groups ', () => { - const params = {}; + + it('stores groups', () => { const response = metricsDashboardResponse; - receiveMetricsDashboardSuccess( - { - state, - commit, - dispatch, - }, - { - response, - params, - }, - ); + receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response }); expect(commit).toHaveBeenCalledWith( - types.RECEIVE_METRICS_DATA_SUCCESS, + types.RECEIVE_METRICS_DASHBOARD_SUCCESS, metricsDashboardResponse.dashboard, ); - expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params); + expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics'); }); it('sets the dashboards loaded from the repository', () => { const params = {}; @@ -395,29 +390,7 @@ describe('Monitoring store actions', () => { expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse); }); }); - describe('receiveMetricsDashboardFailure', () => { - let commit; - beforeEach(() => { - commit = jest.fn(); - }); - it('commits failure action', () => { - receiveMetricsDashboardFailure({ - commit, - }); - expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, undefined); - }); - it('commits failure action with error', () => { - receiveMetricsDashboardFailure( - { - commit, - }, - 'uh-oh', - ); - expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, 'uh-oh'); - }); - }); describe('fetchPrometheusMetrics', () => { - const params = {}; let commit; let dispatch; let state; @@ -427,13 +400,15 @@ describe('Monitoring store actions', () => { commit = jest.fn(); dispatch = jest.fn(); state = storeState(); + + state.timeRange = defaultTimeRange; }); it('commits empty state when state.groups is empty', done => { const getters = { metricsWithData: () => [], }; - fetchPrometheusMetrics({ state, commit, dispatch, getters }, params) + fetchPrometheusMetrics({ state, commit, dispatch, getters }) .then(() => { expect(Tracking.event).toHaveBeenCalledWith( document.body.dataset.page, @@ -444,7 +419,9 @@ describe('Monitoring store actions', () => { value: 0, }, ); - expect(dispatch).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); + expect(createFlash).not.toHaveBeenCalled(); done(); }) @@ -460,11 +437,15 @@ describe('Monitoring store actions', () => { metricsWithData: () => [metric.id], }; - fetchPrometheusMetrics({ state, commit, dispatch, getters }, params) + fetchPrometheusMetrics({ state, commit, dispatch, getters }) .then(() => { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, - params, + defaultQueryParams: { + start_time: expect.any(String), + end_time: expect.any(String), + step: expect.any(Number), + }, }); expect(Tracking.event).toHaveBeenCalledWith( @@ -487,16 +468,22 @@ describe('Monitoring store actions', () => { state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups; const metric = state.dashboard.panelGroups[0].panels[0].metrics[0]; + dispatch.mockResolvedValueOnce(); // fetchDeploymentsData // Mock having one out of four metrics failing dispatch.mockRejectedValueOnce(new Error('Error fetching this metric')); dispatch.mockResolvedValue(); - fetchPrometheusMetrics({ state, commit, dispatch }, params) + fetchPrometheusMetrics({ state, commit, dispatch }) .then(() => { - expect(dispatch).toHaveBeenCalledTimes(9); // one per metric + expect(dispatch).toHaveBeenCalledTimes(10); // one per metric plus 1 for deployments + expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, - params, + defaultQueryParams: { + start_time: expect.any(String), + end_time: expect.any(String), + step: expect.any(Number), + }, }); expect(createFlash).toHaveBeenCalledTimes(1); @@ -508,9 +495,10 @@ describe('Monitoring store actions', () => { }); }); describe('fetchPrometheusMetric', () => { - const params = { + const defaultQueryParams = { start_time: '2019-08-06T12:40:02.184Z', end_time: '2019-08-06T20:40:02.184Z', + step: 60, }; let metric; let state; @@ -532,7 +520,7 @@ describe('Monitoring store actions', () => { testAction( fetchPrometheusMetric, - { metric, params }, + { metric, defaultQueryParams }, state, [ { @@ -569,7 +557,7 @@ describe('Monitoring store actions', () => { testAction( fetchPrometheusMetric, - { metric, params }, + { metric, defaultQueryParams }, state, [ { @@ -611,7 +599,7 @@ describe('Monitoring store actions', () => { testAction( fetchPrometheusMetric, - { metric, params }, + { metric, defaultQueryParams }, state, [ { @@ -646,7 +634,7 @@ describe('Monitoring store actions', () => { testAction( fetchPrometheusMetric, - { metric, params }, + { metric, defaultQueryParams }, state, [ { @@ -682,7 +670,7 @@ describe('Monitoring store actions', () => { testAction( fetchPrometheusMetric, - { metric, params }, + { metric, defaultQueryParams }, state, [ { diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index bc62ada1034..40341d32cf5 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -51,7 +51,7 @@ describe('Monitoring store Getters', () => { setupState({ dashboard: { panelGroups: [] }, }); - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); groups = state.dashboard.panelGroups; }); @@ -60,21 +60,21 @@ describe('Monitoring store Getters', () => { }); it('on an empty metric with no result, returns NO_DATA', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); expect(getMetricStates()).toEqual([metricStates.NO_DATA]); }); it('on a metric with a result, returns OK', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); expect(getMetricStates()).toEqual([metricStates.OK]); }); it('on a metric with an error, returns an error', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId: groups[0].panels[0].metrics[0].metricId, }); @@ -83,7 +83,7 @@ describe('Monitoring store Getters', () => { }); it('on multiple metrics with results, returns OK', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); @@ -94,7 +94,7 @@ describe('Monitoring store Getters', () => { expect(getMetricStates(state.dashboard.panelGroups[2].key)).toEqual([]); }); it('on multiple metrics errors', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId: groups[0].panels[0].metrics[0].metricId, @@ -113,7 +113,7 @@ describe('Monitoring store Getters', () => { }); it('on multiple metrics with errors', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); // An success in 1 group mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); @@ -175,27 +175,27 @@ describe('Monitoring store Getters', () => { }); it('no loaded metric returns empty', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); expect(metricsWithData()).toEqual([]); }); it('an empty metric, returns empty', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); expect(metricsWithData()).toEqual([]); }); it('a metric with results, it returns a metric', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); expect(metricsWithData()).toEqual([mockedQueryResultFixture.metricId]); }); it('multiple metrics with results, it return multiple metrics', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); @@ -206,7 +206,7 @@ describe('Monitoring store Getters', () => { }); it('multiple metrics with results, it returns metrics filtered by group', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); @@ -291,7 +291,7 @@ describe('Monitoring store Getters', () => { }); it('return no metrics when dashboard is not persisted', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData); metricsSavedToDb = getters.metricsSavedToDb(state); expect(metricsSavedToDb).toEqual([]); @@ -304,7 +304,7 @@ describe('Monitoring store Getters', () => { metric.metric_id = id; - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData); metricsSavedToDb = getters.metricsSavedToDb(state); expect(metricsSavedToDb).toEqual([`${id}_${metric.id}`]); @@ -321,7 +321,7 @@ describe('Monitoring store Getters', () => { metric1.metric_id = id1; metric2.metric_id = id2; - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, mockData); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData); metricsSavedToDb = getters.metricsSavedToDb(state); expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 6f1a81782f3..21a27a443af 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -20,7 +20,7 @@ describe('Monitoring mutations', () => { stateCopy = state(); }); - describe('RECEIVE_METRICS_DATA_SUCCESS', () => { + describe('RECEIVE_METRICS_DASHBOARD_SUCCESS', () => { let payload; const getGroups = () => stateCopy.dashboard.panelGroups; @@ -29,7 +29,7 @@ describe('Monitoring mutations', () => { payload = metricsDashboardPayload; }); it('adds a key to the group', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload); const groups = getGroups(); expect(groups[0].key).toBe('system-metrics-kubernetes-0'); @@ -37,7 +37,7 @@ describe('Monitoring mutations', () => { expect(groups[2].key).toBe('response-metrics-nginx-ingress-2'); }); it('normalizes values', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload); const expectedLabel = 'Pod average (MB)'; const { label, queryRange } = getGroups()[0].panels[2].metrics[0]; @@ -45,7 +45,7 @@ describe('Monitoring mutations', () => { expect(queryRange.length).toBeGreaterThan(0); }); it('contains six groups, with panels with a metric each', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload); const groups = getGroups(); @@ -61,7 +61,7 @@ describe('Monitoring mutations', () => { expect(groups[1].panels[0].metrics).toHaveLength(1); }); it('assigns metrics a metric id', () => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload); const groups = getGroups(); @@ -195,7 +195,7 @@ describe('Monitoring mutations', () => { describe('REQUEST_METRIC_RESULT', () => { beforeEach(() => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard); }); it('stores a loading state on a metric', () => { expect(stateCopy.showEmptyState).toBe(true); @@ -218,7 +218,7 @@ describe('Monitoring mutations', () => { describe('RECEIVE_METRIC_RESULT_SUCCESS', () => { beforeEach(() => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard); }); it('clears empty state', () => { expect(stateCopy.showEmptyState).toBe(true); @@ -251,7 +251,7 @@ describe('Monitoring mutations', () => { describe('RECEIVE_METRIC_RESULT_FAILURE', () => { beforeEach(() => { - mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard); + mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard); }); it('maintains the loading state when a metric fails', () => { expect(stateCopy.showEmptyState).toBe(true); diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js index 724c77eee3d..575f1057db2 100644 --- a/spec/frontend/notes/components/sort_discussion_spec.js +++ b/spec/frontend/notes/components/sort_discussion_spec.js @@ -1,6 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import SortDiscussion from '~/notes/components/sort_discussion.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import createStore from '~/notes/stores'; import { ASC, DESC } from '~/notes/constants'; import Tracking from '~/tracking'; @@ -21,6 +22,8 @@ describe('Sort Discussion component', () => { }); }; + const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + beforeEach(() => { store = createStore(); jest.spyOn(Tracking, 'event'); @@ -31,6 +34,22 @@ describe('Sort Discussion component', () => { wrapper = null; }); + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('has local storage sync', () => { + expect(findLocalStorageSync().exists()).toBe(true); + }); + + it('calls setDiscussionSortDirection when update is emitted', () => { + findLocalStorageSync().vm.$emit('input', ASC); + + expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC); + }); + }); + describe('when asc', () => { describe('when the dropdown is clicked', () => { it('calls the right actions', () => { diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js new file mode 100644 index 00000000000..5470171a21e --- /dev/null +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -0,0 +1,128 @@ +import { shallowMount } from '@vue/test-utils'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +describe('Local Storage Sync', () => { + let wrapper; + + const createComponent = ({ props = {}, slots = {} } = {}) => { + wrapper = shallowMount(LocalStorageSync, { + propsData: props, + slots, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + localStorage.clear(); + }); + + it('is a renderless component', () => { + const html = '<div class="test-slot"></div>'; + createComponent({ + props: { + storageKey: 'key', + }, + slots: { + default: html, + }, + }); + + expect(wrapper.html()).toBe(html); + }); + + describe('localStorage empty', () => { + const storageKey = 'issue_list_order'; + + it('does not emit input event', () => { + createComponent({ + props: { + storageKey, + value: 'ascending', + }, + }); + + expect(wrapper.emitted('input')).toBeFalsy(); + }); + + it('saves updated value to localStorage', () => { + createComponent({ + props: { + storageKey, + value: 'ascending', + }, + }); + + const newValue = 'descending'; + wrapper.setProps({ + value: newValue, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(localStorage.getItem(storageKey)).toBe(newValue); + }); + }); + + it('does not save default value', () => { + const value = 'ascending'; + + createComponent({ + props: { + storageKey, + value, + }, + }); + + expect(localStorage.getItem(storageKey)).toBe(null); + }); + }); + + describe('localStorage has saved value', () => { + const storageKey = 'issue_list_order_by'; + const savedValue = 'last_updated'; + + beforeEach(() => { + localStorage.setItem(storageKey, savedValue); + }); + + it('emits input event with saved value', () => { + createComponent({ + props: { + storageKey, + value: 'ascending', + }, + }); + + expect(wrapper.emitted('input')[0][0]).toBe(savedValue); + }); + + it('does not overwrite localStorage with prop value', () => { + createComponent({ + props: { + storageKey, + value: 'created', + }, + }); + + expect(localStorage.getItem(storageKey)).toBe(savedValue); + }); + + it('updating the value updates localStorage', () => { + createComponent({ + props: { + storageKey, + value: 'created', + }, + }); + + const newValue = 'last_updated'; + wrapper.setProps({ + value: newValue, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(localStorage.getItem(storageKey)).toBe(newValue); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/components/dashboard_resize_spec.js b/spec/javascripts/monitoring/components/dashboard_resize_spec.js index 6a35069ccff..6455346e890 100644 --- a/spec/javascripts/monitoring/components/dashboard_resize_spec.js +++ b/spec/javascripts/monitoring/components/dashboard_resize_spec.js @@ -39,7 +39,7 @@ const propsData = { function setupComponentStore(component) { // Load 2 panel groups component.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, metricsDashboardPayload, ); diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f6c19ccc9d3..8685838fdde 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -27,6 +27,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do it { is_expected.to have_many(:kubernetes_namespaces) } it { is_expected.to have_one(:cluster_project) } it { is_expected.to have_many(:deployment_clusters) } + it { is_expected.to have_many(:metrics_dashboard_annotations) } it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 896203d8669..d0305d878e3 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -17,6 +17,7 @@ describe Environment, :use_clean_rails_memory_store_caching do it { is_expected.to belong_to(:project).required } it { is_expected.to have_many(:deployments) } + it { is_expected.to have_many(:metrics_dashboard_annotations) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb new file mode 100644 index 00000000000..ed3bef37a7c --- /dev/null +++ b/spec/models/metrics/dashboard/annotation_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Metrics::Dashboard::Annotation do + describe 'associations' do + it { is_expected.to belong_to(:environment).inverse_of(:metrics_dashboard_annotations) } + it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster').inverse_of(:metrics_dashboard_annotations) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_presence_of(:dashboard_path) } + it { is_expected.to validate_presence_of(:starting_at) } + it { is_expected.to validate_length_of(:dashboard_path).is_at_most(255) } + it { is_expected.to validate_length_of(:panel_xid).is_at_most(255) } + it { is_expected.to validate_length_of(:description).is_at_most(255) } + + context 'orphaned annotation' do + subject { build(:metrics_dashboard_annotation, environment: nil) } + + it { is_expected.not_to be_valid } + + it 'reports error about both missing relations' do + subject.valid? + + expect(subject.errors.full_messages).to include(/Annotation must belong to a cluster or an environment/) + end + end + + context 'environments annotation' do + subject { build(:metrics_dashboard_annotation) } + + it { is_expected.to be_valid } + end + + context 'clusters annotation' do + subject { build(:metrics_dashboard_annotation, :with_cluster) } + + it { is_expected.to be_valid } + end + + context 'annotation with shared ownership' do + subject { build(:metrics_dashboard_annotation, :with_cluster, environment: build(:environment) ) } + + it 'reports error about both shared ownership' do + subject.valid? + + expect(subject.errors.full_messages).to include(/Annotation can't belong to both a cluster and an environment at the same time/) + end + end + end +end diff --git a/spec/policies/metrics/dashboard/annotation_policy_spec.rb b/spec/policies/metrics/dashboard/annotation_policy_spec.rb new file mode 100644 index 00000000000..4dc5f4cd0b4 --- /dev/null +++ b/spec/policies/metrics/dashboard/annotation_policy_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Metrics::Dashboard::AnnotationPolicy, :models do + shared_examples 'metrics dashboard annotation policy' do + context 'when guest' do + before do + project.add_guest(user) + end + + it { expect(policy).to be_disallowed :read_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :create_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :update_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :delete_metrics_dashboard_annotation } + end + + context 'when reporter' do + before do + project.add_reporter(user) + end + + it { expect(policy).to be_allowed :read_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :create_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :update_metrics_dashboard_annotation } + it { expect(policy).to be_disallowed :delete_metrics_dashboard_annotation } + end + + context 'when developer' do + before do + project.add_developer(user) + end + + it { expect(policy).to be_allowed :read_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :create_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :update_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :delete_metrics_dashboard_annotation } + end + + context 'when maintainer' do + before do + project.add_maintainer(user) + end + + it { expect(policy).to be_allowed :read_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :create_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :update_metrics_dashboard_annotation } + it { expect(policy).to be_allowed :delete_metrics_dashboard_annotation } + end + end + + describe 'rules' do + context 'environments annotation' do + let(:annotation) { create(:metrics_dashboard_annotation, environment: environment) } + let(:environment) { create(:environment) } + let!(:project) { environment.project } + let(:user) { create(:user) } + let(:policy) { described_class.new(user, annotation) } + + it_behaves_like 'metrics dashboard annotation policy' + end + + context 'cluster annotation' do + let(:annotation) { create(:metrics_dashboard_annotation, environment: nil, cluster: cluster) } + let(:cluster) { create(:cluster, :project) } + let(:project) { cluster.project } + let(:user) { create(:user) } + let(:policy) { described_class.new(user, annotation) } + + it_behaves_like 'metrics dashboard annotation policy' + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index cce84c4f357..d098369e124 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -28,7 +28,7 @@ describe ProjectPolicy do download_code fork_project create_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue + read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation ] end @@ -43,6 +43,7 @@ describe ProjectPolicy do update_pipeline create_merge_request_from create_wiki push_code resolve_note create_container_image update_container_image destroy_container_image create_environment update_environment create_deployment update_deployment create_release update_release + create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation ] end diff --git a/spec/services/metrics/dashboard/annotations/create_service_spec.rb b/spec/services/metrics/dashboard/annotations/create_service_spec.rb new file mode 100644 index 00000000000..7dabca3c860 --- /dev/null +++ b/spec/services/metrics/dashboard/annotations/create_service_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Metrics::Dashboard::Annotations::CreateService do + let_it_be(:user) { create(:user) } + let(:description) { 'test annotation' } + let(:dashboard_path) { 'config/prometheus/common_metrics.yml' } + let(:starting_at) { 15.minutes.ago } + let(:ending_at) { nil } + let(:service_instance) { described_class.new(user, annotation_params) } + let(:annotation_params) do + { + environment: environment, + cluster: cluster, + description: description, + dashboard_path: dashboard_path, + starting_at: starting_at, + ending_at: ending_at + } + end + + shared_examples 'executed annotation creation' do + it 'returns success response', :aggregate_failures do + annotation = instance_double(::Metrics::Dashboard::Annotation) + allow(::Metrics::Dashboard::Annotation).to receive(:new).and_return(annotation) + allow(annotation).to receive(:save).and_return(true) + + response = service_instance.execute + + expect(response[:status]).to be :success + expect(response[:annotation]).to be annotation + end + + it 'creates annotation', :aggregate_failures do + annotation = instance_double(::Metrics::Dashboard::Annotation) + + expect(::Metrics::Dashboard::Annotation) + .to receive(:new).with(annotation_params).and_return(annotation) + expect(annotation).to receive(:save).and_return(true) + + service_instance.execute + end + end + + shared_examples 'prevented annotation creation' do |message| + it 'returns error response', :aggregate_failures do + response = service_instance.execute + + expect(response[:status]).to be :error + expect(response[:message]).to eql message + end + + it 'does not change db state' do + expect(::Metrics::Dashboard::Annotation).not_to receive(:new) + + service_instance.execute + end + end + + shared_examples 'annotation creation failure' do + it 'returns error response', :aggregate_failures do + annotation = instance_double(::Metrics::Dashboard::Annotation) + + expect(annotation).to receive(:errors).and_return('Model validation error') + expect(::Metrics::Dashboard::Annotation) + .to receive(:new).with(annotation_params).and_return(annotation) + expect(annotation).to receive(:save).and_return(false) + + response = service_instance.execute + + expect(response[:status]).to be :error + expect(response[:message]).to eql 'Model validation error' + end + end + + describe '.execute' do + context 'with environment' do + let(:environment) { create(:environment) } + let(:cluster) { nil } + + context 'with anonymous user' do + it_behaves_like 'prevented annotation creation', 'You are not authorized to create annotation for selected environment' + end + + context 'with maintainer user' do + before do + environment.project.add_maintainer(user) + end + + it_behaves_like 'executed annotation creation' + end + end + + context 'with cluster' do + let(:environment) { nil } + + context 'with anonymous user' do + let(:cluster) { create(:cluster, :project) } + + it_behaves_like 'prevented annotation creation', 'You are not authorized to create annotation for selected cluster' + end + + context 'with maintainer user' do + let(:cluster) { create(:cluster, :project) } + + before do + cluster.project.add_maintainer(user) + end + + it_behaves_like 'executed annotation creation' + end + + context 'with owner user' do + let(:cluster) { create(:cluster, :group) } + + before do + cluster.group.add_owner(user) + end + + it_behaves_like 'executed annotation creation' + end + end + + context 'non cluster nor environment is supplied' do + let(:environment) { nil } + let(:cluster) { nil } + + it_behaves_like 'annotation creation failure' + end + + context 'missing dashboard_path' do + let(:cluster) { create(:cluster, :project) } + let(:environment) { nil } + let(:dashboard_path) { nil } + + context 'with maintainer user' do + before do + cluster.project.add_maintainer(user) + end + + it_behaves_like 'annotation creation failure' + end + end + + context 'incorrect dashboard_path' do + let(:cluster) { create(:cluster, :project) } + let(:environment) { nil } + let(:dashboard_path) { 'something_incorrect.yml' } + + context 'with maintainer user' do + before do + cluster.project.add_maintainer(user) + end + + it_behaves_like 'prevented annotation creation', 'Dashboard with requested path can not be found' + end + end + end +end diff --git a/spec/services/metrics/dashboard/annotations/delete_service_spec.rb b/spec/services/metrics/dashboard/annotations/delete_service_spec.rb new file mode 100644 index 00000000000..95825db6902 --- /dev/null +++ b/spec/services/metrics/dashboard/annotations/delete_service_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Metrics::Dashboard::Annotations::DeleteService do + let(:user) { create(:user) } + let(:service_instance) { described_class.new(user, annotation) } + + shared_examples 'executed annotation deletion' do + it 'returns success response', :aggregate_failures do + expect(annotation).to receive(:destroy).and_return(true) + + response = service_instance.execute + + expect(response[:status]).to be :success + end + end + + shared_examples 'prevented annotation deletion' do |message| + it 'returns error response', :aggregate_failures do + response = service_instance.execute + + expect(response[:status]).to be :error + expect(response[:message]).to eql message + end + + it 'does not change db state' do + expect(annotation).not_to receive(:destroy) + + service_instance.execute + end + end + + describe '.execute' do + context 'with specific environment' do + let(:annotation) { create(:metrics_dashboard_annotation, environment: environment) } + let(:environment) { create(:environment) } + + context 'with anonymous user' do + it_behaves_like 'prevented annotation deletion', 'You are not authorized to delete this annotation' + end + + context 'with maintainer user' do + before do + environment.project.add_maintainer(user) + end + + it_behaves_like 'executed annotation deletion' + + context 'annotation failed to delete' do + it 'returns error response', :aggregate_failures do + allow(annotation).to receive(:destroy).and_return(false) + + response = service_instance.execute + + expect(response[:status]).to be :error + expect(response[:message]).to eql 'Annotation has not been deleted' + end + end + end + end + + context 'with specific cluster' do + let(:annotation) { create(:metrics_dashboard_annotation, cluster: cluster, environment: nil) } + + context 'with anonymous user' do + let(:cluster) { create(:cluster, :project) } + + it_behaves_like 'prevented annotation deletion', 'You are not authorized to delete this annotation' + end + + context 'with maintainer user' do + let(:cluster) { create(:cluster, :project) } + + before do + cluster.project.add_maintainer(user) + end + + it_behaves_like 'executed annotation deletion' + end + + context 'with owner user' do + let(:cluster) { create(:cluster, :group) } + + before do + cluster.group.add_owner(user) + end + + it_behaves_like 'executed annotation deletion' + end + end + end +end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index 3a306f80b3c..4f81a71f586 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -18,8 +18,8 @@ RSpec.shared_context 'GroupPolicy context' do ] end let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } - let(:reporter_permissions) { %i[admin_label read_container_image] } - let(:developer_permissions) { [:admin_milestone] } + let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] } + let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } let(:maintainer_permissions) do %i[ create_projects |