Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-07 21:09:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-07 21:09:19 +0300
commit3290d46655f07d7ae3dca788d6df9f326972ffd8 (patch)
tree0d24713e1592cdd3583257f14a52d46a22539ed1 /spec
parentc6b3ec3f56fa32a0e0ed3de0d0878d25f1adaddf (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/metrics/dashboard/annotations.rb15
-rw-r--r--spec/features/issues/user_sorts_issue_comments_spec.rb45
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/init_utils.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js212
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js30
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js16
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js128
-rw-r--r--spec/javascripts/monitoring/components/dashboard_resize_spec.js2
-rw-r--r--spec/models/clusters/cluster_spec.rb1
-rw-r--r--spec/models/environment_spec.rb1
-rw-r--r--spec/models/metrics/dashboard/annotation_spec.rb53
-rw-r--r--spec/policies/metrics/dashboard/annotation_policy_spec.rb73
-rw-r--r--spec/policies/project_policy_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/annotations/create_service_spec.rb160
-rw-r--r--spec/services/metrics/dashboard/annotations/delete_service_spec.rb93
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb4
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