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>2019-10-30 18:14:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-30 18:14:17 +0300
commit3fe9588b1c1c4fb58f8ba8e9c27244fc2fc1c103 (patch)
treed19448d010ff9d58fed14846736ee358fb6b3327 /spec
parentad8eea383406037a207c80421e6e4bfa357f8044 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/group_links_controller_spec.rb114
-rw-r--r--spec/factories/group_group_links.rb9
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js303
-rw-r--r--spec/frontend/monitoring/mock_data.js161
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/javascripts/monitoring/charts/time_series_spec.js42
-rw-r--r--spec/javascripts/monitoring/mock_data.js6
-rw-r--r--spec/javascripts/monitoring/panel_type_spec.js31
-rw-r--r--spec/javascripts/monitoring/utils_spec.js38
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb167
-rw-r--r--spec/models/group_group_link_spec.rb36
-rw-r--r--spec/models/group_spec.rb122
-rw-r--r--spec/requests/api/members_spec.rb12
-rw-r--r--spec/services/groups/group_links/create_service_spec.rb119
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb37
16 files changed, 1157 insertions, 43 deletions
diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb
new file mode 100644
index 00000000000..8f04822fee6
--- /dev/null
+++ b/spec/controllers/groups/group_links_controller_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::GroupLinksController do
+ let(:shared_with_group) { create(:group, :private) }
+ let(:shared_group) { create(:group, :private) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#create' do
+ let(:shared_with_group_id) { shared_with_group.id }
+
+ subject do
+ post(:create,
+ params: { group_id: shared_group,
+ shared_with_group_id: shared_with_group_id,
+ shared_group_access: GroupGroupLink.default_access })
+ end
+
+ context 'when user has correct access to both groups' do
+ let(:group_member) { create(:user) }
+
+ before do
+ shared_with_group.add_developer(user)
+ shared_group.add_owner(user)
+
+ shared_with_group.add_developer(group_member)
+ end
+
+ it 'links group with selected group' do
+ expect { subject }.to change { shared_with_group.shared_groups.include?(shared_group) }.from(false).to(true)
+ end
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ end
+
+ it 'allows access for group member' do
+ expect { subject }.to change { group_member.can?(:read_group, shared_group) }.from(false).to(true)
+ end
+
+ context 'when shared with group id is not present' do
+ let(:shared_with_group_id) { nil }
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ expect(flash[:alert]).to eq('Please select a group.')
+ end
+ end
+
+ context 'when link is not persisted in the database' do
+ before do
+ allow(::Groups::GroupLinks::CreateService).to(
+ receive_message_chain(:new, :execute)
+ .and_return({ status: :error,
+ http_status: 409,
+ message: 'error' }))
+ end
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ expect(flash[:alert]).to eq('error')
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when user does not have access to the group' do
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user does not have admin access to the shared group' do
+ before do
+ shared_with_group.add_developer(user)
+ shared_group.add_developer(user)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/factories/group_group_links.rb b/spec/factories/group_group_links.rb
new file mode 100644
index 00000000000..0711a15b8dd
--- /dev/null
+++ b/spec/factories/group_group_links.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :group_group_link do
+ shared_group { create(:group) }
+ shared_with_group { create(:group) }
+ group_access { GroupMember::DEVELOPER }
+ end
+end
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
new file mode 100644
index 00000000000..6707d0b1fe8
--- /dev/null
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -0,0 +1,303 @@
+import Anomaly from '~/monitoring/components/charts/anomaly.vue';
+
+import { shallowMount } from '@vue/test-utils';
+import { colorValues } from '~/monitoring/constants';
+import {
+ anomalyDeploymentData,
+ mockProjectDir,
+ anomalyMockGraphData,
+ anomalyMockResultValues,
+} from '../../mock_data';
+import { TEST_HOST } from 'helpers/test_constants';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+
+const mockWidgets = 'mockWidgets';
+const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
+
+jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent
+
+const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => {
+ const queries = anomalyMockResultValues[datasetName].map((values, index) => ({
+ ...template.queries[index],
+ result: [
+ {
+ metrics: {},
+ values,
+ },
+ ],
+ }));
+ return { ...template, queries };
+};
+
+describe('Anomaly chart component', () => {
+ let wrapper;
+
+ const setupAnomalyChart = props => {
+ wrapper = shallowMount(Anomaly, {
+ propsData: { ...props },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ });
+ };
+ const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart);
+ const getTimeSeriesProps = () => findTimeSeries().props();
+
+ describe('wrapped monitor-time-series-chart component', () => {
+ const dataSetName = 'noAnomaly';
+ const dataSet = anomalyMockResultValues[dataSetName];
+ const inputThresholds = ['some threshold'];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ thresholds: inputThresholds,
+ projectPath: mockProjectPath,
+ });
+ });
+
+ it('is a Vue instance', () => {
+ expect(findTimeSeries().exists()).toBe(true);
+ expect(findTimeSeries().isVueInstance()).toBe(true);
+ });
+
+ describe('receives props correctly', () => {
+ describe('graph-data', () => {
+ it('receives a single "metric" series', () => {
+ const { graphData } = getTimeSeriesProps();
+ expect(graphData.queries.length).toBe(1);
+ });
+
+ it('receives "metric" with all data', () => {
+ const { graphData } = getTimeSeriesProps();
+ const query = graphData.queries[0];
+ const expectedQuery = makeAnomalyGraphData(dataSetName).queries[0];
+ expect(query).toEqual(expectedQuery);
+ });
+
+ it('receives the "metric" results', () => {
+ const { graphData } = getTimeSeriesProps();
+ const { result } = graphData.queries[0];
+ const { values } = result[0];
+ const [metricDataset] = dataSet;
+ expect(values).toEqual(expect.any(Array));
+
+ values.forEach(([, y], index) => {
+ expect(y).toBeCloseTo(metricDataset[index][1]);
+ });
+ });
+ });
+
+ describe('option', () => {
+ let option;
+ let series;
+
+ beforeEach(() => {
+ ({ option } = getTimeSeriesProps());
+ ({ series } = option);
+ });
+
+ it('contains a boundary band', () => {
+ expect(series).toEqual(expect.any(Array));
+ expect(series.length).toEqual(2); // 1 upper + 1 lower boundaries
+ expect(series[0].stack).toEqual(series[1].stack);
+
+ series.forEach(s => {
+ expect(s.type).toBe('line');
+ expect(s.lineStyle.width).toBe(0);
+ expect(s.lineStyle.color).toMatch(/rgba\(.+\)/);
+ expect(s.lineStyle.color).toMatch(s.color);
+ expect(s.symbol).toEqual('none');
+ });
+ });
+
+ it('upper boundary values are stacked on top of lower boundary', () => {
+ const [lowerSeries, upperSeries] = series;
+ const [, upperDataset, lowerDataset] = dataSet;
+
+ lowerSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(lowerDataset[i][1]);
+ });
+
+ upperSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ });
+ });
+ });
+
+ describe('series-config', () => {
+ let seriesConfig;
+
+ beforeEach(() => {
+ ({ seriesConfig } = getTimeSeriesProps());
+ });
+
+ it('display symbols is enabled', () => {
+ expect(seriesConfig).toEqual(
+ expect.objectContaining({
+ type: 'line',
+ symbol: 'circle',
+ showSymbol: true,
+ symbolSize: expect.any(Function),
+ itemStyle: {
+ color: expect.any(Function),
+ },
+ }),
+ );
+ });
+ it('does not display anomalies', () => {
+ const { symbolSize, itemStyle } = seriesConfig;
+ const [metricDataset] = dataSet;
+
+ metricDataset.forEach((v, dataIndex) => {
+ const size = symbolSize(null, { dataIndex });
+ const color = itemStyle.color({ dataIndex });
+
+ // normal color and small size
+ expect(size).toBeCloseTo(0);
+ expect(color).toBe(colorValues.primaryColor);
+ });
+ });
+
+ it('can format y values (to use in tooltips)', () => {
+ expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
+ expect(parseFloat(wrapper.vm.yValueFormatted(1, 0))).toEqual(dataSet[1][0][1]);
+ expect(parseFloat(wrapper.vm.yValueFormatted(2, 0))).toEqual(dataSet[2][0][1]);
+ });
+ });
+
+ describe('inherited properties', () => {
+ it('"deployment-data" keeps the same value', () => {
+ const { deploymentData } = getTimeSeriesProps();
+ expect(deploymentData).toEqual(anomalyDeploymentData);
+ });
+ it('"thresholds" keeps the same value', () => {
+ const { thresholds } = getTimeSeriesProps();
+ expect(thresholds).toEqual(inputThresholds);
+ });
+ it('"projectPath" keeps the same value', () => {
+ const { projectPath } = getTimeSeriesProps();
+ expect(projectPath).toEqual(mockProjectPath);
+ });
+ });
+ });
+ });
+
+ describe('with no boundary data', () => {
+ const dataSetName = 'noBoundary';
+ const dataSet = anomalyMockResultValues[dataSetName];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('option', () => {
+ let option;
+ let series;
+
+ beforeEach(() => {
+ ({ option } = getTimeSeriesProps());
+ ({ series } = option);
+ });
+
+ it('does not display a boundary band', () => {
+ expect(series).toEqual(expect.any(Array));
+ expect(series.length).toEqual(0); // no boundaries
+ });
+
+ it('can format y values (to use in tooltips)', () => {
+ expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
+ expect(wrapper.vm.yValueFormatted(1, 0)).toBe(''); // missing boundary
+ expect(wrapper.vm.yValueFormatted(2, 0)).toBe(''); // missing boundary
+ });
+ });
+ });
+
+ describe('with one anomaly', () => {
+ const dataSetName = 'oneAnomaly';
+ const dataSet = anomalyMockResultValues[dataSetName];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('series-config', () => {
+ it('displays one anomaly', () => {
+ const { seriesConfig } = getTimeSeriesProps();
+ const { symbolSize, itemStyle } = seriesConfig;
+ const [metricDataset] = dataSet;
+
+ const bigDots = metricDataset.filter((v, dataIndex) => {
+ const size = symbolSize(null, { dataIndex });
+ return size > 0.1;
+ });
+ const redDots = metricDataset.filter((v, dataIndex) => {
+ const color = itemStyle.color({ dataIndex });
+ return color === colorValues.anomalySymbol;
+ });
+
+ expect(bigDots.length).toBe(1);
+ expect(redDots.length).toBe(1);
+ });
+ });
+ });
+
+ describe('with offset', () => {
+ const dataSetName = 'negativeBoundary';
+ const dataSet = anomalyMockResultValues[dataSetName];
+ const expectedOffset = 4; // Lowst point in mock data is -3.70, it gets rounded
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('receives props correctly', () => {
+ describe('graph-data', () => {
+ it('receives a single "metric" series', () => {
+ const { graphData } = getTimeSeriesProps();
+ expect(graphData.queries.length).toBe(1);
+ });
+
+ it('receives "metric" results and applies the offset to them', () => {
+ const { graphData } = getTimeSeriesProps();
+ const { result } = graphData.queries[0];
+ const { values } = result[0];
+ const [metricDataset] = dataSet;
+ expect(values).toEqual(expect.any(Array));
+
+ values.forEach(([, y], index) => {
+ expect(y).toBeCloseTo(metricDataset[index][1] + expectedOffset);
+ });
+ });
+ });
+ });
+
+ describe('option', () => {
+ it('upper boundary values are stacked on top of lower boundary, plus the offset', () => {
+ const { option } = getTimeSeriesProps();
+ const { series } = option;
+ const [lowerSeries, upperSeries] = series;
+ const [, upperDataset, lowerDataset] = dataSet;
+
+ lowerSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(lowerDataset[i][1] + expectedOffset);
+ });
+
+ upperSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
new file mode 100644
index 00000000000..39b4343d0ce
--- /dev/null
+++ b/spec/frontend/monitoring/mock_data.js
@@ -0,0 +1,161 @@
+export const mockProjectDir = '/frontend-fixtures/environments-project';
+
+export const anomalyDeploymentData = [
+ {
+ id: 111,
+ iid: 3,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-08-19T22:00:00.000Z',
+ deployed_at: '2019-08-19T22:01:00.000Z',
+ tag: false,
+ 'last?': true,
+ },
+ {
+ id: 110,
+ iid: 2,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-08-19T23:00:00.000Z',
+ deployed_at: '2019-08-19T23:00:00.000Z',
+ tag: false,
+ 'last?': false,
+ },
+];
+
+export const anomalyMockResultValues = {
+ noAnomaly: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 1.45],
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ['2019-08-19T22:00:00.000Z', 1.48],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ['2019-08-19T22:00:00.000Z', 3.0],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', 0.45],
+ ['2019-08-19T20:00:00.000Z', 0.65],
+ ['2019-08-19T21:00:00.000Z', 0.7],
+ ['2019-08-19T22:00:00.000Z', 0.8],
+ ],
+ ],
+ noBoundary: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 1.45],
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ['2019-08-19T22:00:00.000Z', 1.48],
+ ],
+ [
+ // empty upper boundary
+ ],
+ [
+ // empty lower boundary
+ ],
+ ],
+ oneAnomaly: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', 0.45],
+ ['2019-08-19T20:00:00.000Z', 0.65],
+ ['2019-08-19T21:00:00.000Z', 0.7],
+ ],
+ ],
+ negativeBoundary: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', -1.25],
+ ['2019-08-19T20:00:00.000Z', -2.65],
+ ['2019-08-19T21:00:00.000Z', -3.7], // lowest point
+ ],
+ ],
+};
+
+export const anomalyMockGraphData = {
+ title: 'Requests Per Second Mock Data',
+ type: 'anomaly-chart',
+ weight: 3,
+ metrics: [
+ // Not used
+ ],
+ queries: [
+ {
+ metricId: '90',
+ id: 'metric',
+ query_range: 'MOCK_PROMETHEUS_METRIC_QUERY_RANGE',
+ unit: 'RPS',
+ label: 'Metrics RPS',
+ metric_id: 90,
+ prometheus_endpoint_path: 'MOCK_METRIC_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ {
+ metricId: '91',
+ id: 'upper',
+ query_range: '...',
+ unit: 'RPS',
+ label: 'Upper Limit Metrics RPS',
+ metric_id: 91,
+ prometheus_endpoint_path: 'MOCK_UPPER_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ {
+ metricId: '92',
+ id: 'lower',
+ query_range: '...',
+ unit: 'RPS',
+ label: 'Lower Limit Metrics RPS',
+ metric_id: 92,
+ prometheus_endpoint_path: 'MOCK_LOWER_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index e539c560975..565a46bb957 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -104,7 +104,7 @@ describe('Repository table row component', () => {
if (pushes) {
expect(visitUrl).not.toHaveBeenCalled();
} else {
- expect(visitUrl).toHaveBeenCalledWith('https://test.com');
+ expect(visitUrl).toHaveBeenCalledWith('https://test.com', undefined);
}
});
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js
index 5c718135b90..31ea9ede9de 100644
--- a/spec/javascripts/monitoring/charts/time_series_spec.js
+++ b/spec/javascripts/monitoring/charts/time_series_spec.js
@@ -29,7 +29,6 @@ describe('Time series component', () => {
shallowMount(TimeSeries, {
propsData: {
graphData: { ...graphData, type },
- containerWidth: 0,
deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath,
},
@@ -82,7 +81,7 @@ describe('Time series component', () => {
seriesName: timeSeriesChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
- seriesIndex: 0,
+ dataIndex: 0,
},
],
value: mockDate,
@@ -101,11 +100,15 @@ describe('Time series component', () => {
it('formats tooltip content', () => {
const name = 'Core Usage';
const value = '5.556';
+ const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
expect(seriesLabel.vm.color).toBe('');
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
- expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([
+ { name, value, dataIndex, color: undefined },
+ ]);
+
expect(
shallowWrapperContainsSlotText(
timeSeriesChart.find(GlAreaChart),
@@ -212,6 +215,39 @@ describe('Time series component', () => {
});
describe('chartOptions', () => {
+ describe('are extended by `option`', () => {
+ const mockSeriesName = 'Extra series 1';
+ const mockOption = {
+ option1: 'option1',
+ option2: 'option2',
+ };
+
+ it('arbitrary options', () => {
+ timeSeriesChart.setProps({
+ option: mockOption,
+ });
+
+ expect(timeSeriesChart.vm.chartOptions).toEqual(jasmine.objectContaining(mockOption));
+ });
+
+ it('additional series', () => {
+ timeSeriesChart.setProps({
+ option: {
+ series: [
+ {
+ name: mockSeriesName,
+ },
+ ],
+ },
+ });
+
+ const optionSeries = timeSeriesChart.vm.chartOptions.series;
+
+ expect(optionSeries.length).toEqual(2);
+ expect(optionSeries[0].name).toEqual(mockSeriesName);
+ });
+ });
+
describe('yAxis formatter', () => {
let format;
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 17e7314e214..6f9a2a34ef5 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,3 +1,7 @@
+import { anomalyMockGraphData as importedAnomalyMockGraphData } from '../../frontend/monitoring/mock_data';
+
+export const anomalyMockGraphData = importedAnomalyMockGraphData;
+
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
export const mockProjectPath = '/frontend-fixtures/environments-project';
@@ -975,7 +979,7 @@ export const graphDataPrometheusQuery = {
export const graphDataPrometheusQueryRange = {
title: 'Super Chart A1',
- type: 'area',
+ type: 'area-chart',
weight: 2,
metrics: [
{
diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js
index a2366e74d43..e3781117eaf 100644
--- a/spec/javascripts/monitoring/panel_type_spec.js
+++ b/spec/javascripts/monitoring/panel_type_spec.js
@@ -2,7 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
import { graphDataPrometheusQueryRange } from './mock_data';
+import { anomalyMockGraphData } from '../../frontend/monitoring/mock_data';
import { createStore } from '~/monitoring/stores';
describe('Panel Type component', () => {
@@ -49,17 +51,20 @@ describe('Panel Type component', () => {
describe('when Graph data is available', () => {
const exampleText = 'example_text';
+ const propsData = {
+ clipboardText: exampleText,
+ dashboardWidth,
+ graphData: graphDataPrometheusQueryRange,
+ };
- beforeEach(() => {
+ beforeEach(done => {
store = createStore();
panelType = shallowMount(PanelType, {
- propsData: {
- clipboardText: exampleText,
- dashboardWidth,
- graphData: graphDataPrometheusQueryRange,
- },
+ propsData,
+ sync: false,
store,
});
+ panelType.vm.$nextTick(done);
});
describe('Time Series Chart panel type', () => {
@@ -75,5 +80,19 @@ describe('Panel Type component', () => {
expect(clipboardText()).toBe(exampleText);
});
});
+
+ describe('Anomaly Chart panel type', () => {
+ beforeEach(done => {
+ panelType.setProps({
+ graphData: anomalyMockGraphData,
+ });
+ panelType.vm.$nextTick(done);
+ });
+
+ it('is rendered with an anomaly chart', () => {
+ expect(panelType.find(AnomalyChart).isVueInstance()).toBe(true);
+ expect(panelType.find(AnomalyChart).exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
index 512dd2a0eb3..202b4ec8f2e 100644
--- a/spec/javascripts/monitoring/utils_spec.js
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -7,9 +7,14 @@ import {
stringToISODate,
ISODateToString,
isValidDate,
+ graphDataValidatorForAnomalyValues,
} from '~/monitoring/utils';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
-import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
+import {
+ graphDataPrometheusQuery,
+ graphDataPrometheusQueryRange,
+ anomalyMockGraphData,
+} from './mock_data';
describe('getTimeDiff', () => {
function secondsBetween({ start, end }) {
@@ -307,3 +312,34 @@ describe('isDateTimePickerInputValid', () => {
});
});
});
+
+describe('graphDataValidatorForAnomalyValues', () => {
+ let oneQuery;
+ let threeQueries;
+ let fourQueries;
+ beforeEach(() => {
+ oneQuery = graphDataPrometheusQuery;
+ threeQueries = anomalyMockGraphData;
+
+ const queries = [...threeQueries.queries];
+ queries.push(threeQueries.queries[0]);
+ fourQueries = {
+ ...anomalyMockGraphData,
+ queries,
+ };
+ });
+ /*
+ * Anomaly charts can accept results for exactly 3 queries,
+ */
+ it('validates passes with the right query format', () => {
+ expect(graphDataValidatorForAnomalyValues(threeQueries)).toBe(true);
+ });
+
+ it('validation fails for wrong format, 1 metric', () => {
+ expect(graphDataValidatorForAnomalyValues(oneQuery)).toBe(false);
+ });
+
+ it('validation fails for wrong format, more than 3 metrics', () => {
+ expect(graphDataValidatorForAnomalyValues(fourQueries)).toBe(false);
+ });
+});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3b665d95004..d0e4b92410d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -423,6 +423,7 @@ project:
- alerts_service
- grafana_integration
- remove_source_branch_after_merge
+- deleting_user
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index 82ccb42f8a6..006daa29ea1 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -3,48 +3,55 @@
require 'spec_helper'
describe Gitlab::ProjectAuthorizations do
- let(:group) { create(:group) }
- let!(:owned_project) { create(:project) }
- let!(:other_project) { create(:project) }
- let!(:group_project) { create(:project, namespace: group) }
-
- let(:user) { owned_project.namespace.owner }
-
def map_access_levels(rows)
rows.each_with_object({}) do |row, hash|
hash[row.project_id] = row.access_level
end
end
- before do
- other_project.add_reporter(user)
- group.add_developer(user)
- end
-
- let(:authorizations) do
+ subject(:authorizations) do
described_class.new(user).calculate
end
- it 'returns the correct number of authorizations' do
- expect(authorizations.length).to eq(3)
- end
+ context 'user added to group and project' do
+ let(:group) { create(:group) }
+ let!(:other_project) { create(:project) }
+ let!(:group_project) { create(:project, namespace: group) }
+ let!(:owned_project) { create(:project) }
+ let(:user) { owned_project.namespace.owner }
- it 'includes the correct projects' do
- expect(authorizations.pluck(:project_id))
- .to include(owned_project.id, other_project.id, group_project.id)
- end
+ before do
+ other_project.add_reporter(user)
+ group.add_developer(user)
+ end
+
+ it 'returns the correct number of authorizations' do
+ expect(authorizations.length).to eq(3)
+ end
- it 'includes the correct access levels' do
- mapping = map_access_levels(authorizations)
+ it 'includes the correct projects' do
+ expect(authorizations.pluck(:project_id))
+ .to include(owned_project.id, other_project.id, group_project.id)
+ end
+
+ it 'includes the correct access levels' do
+ mapping = map_access_levels(authorizations)
- expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
- expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
- expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
+ expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
+ expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
end
context 'with nested groups' do
+ let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:nested_project) { create(:project, namespace: nested_group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_developer(user)
+ end
it 'includes nested groups' do
expect(authorizations.pluck(:project_id)).to include(nested_project.id)
@@ -64,4 +71,114 @@ describe Gitlab::ProjectAuthorizations do
expect(mapping[nested_project.id]).to eq(Gitlab::Access::MAINTAINER)
end
end
+
+ context 'with shared groups' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ set(:group_parent) { create(:group, :private) }
+ set(:group) { create(:group, :private, parent: group_parent) }
+ set(:group_child) { create(:group, :private, parent: group) }
+
+ set(:shared_group_parent) { create(:group, :private) }
+ set(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ set(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ set(:project_parent) { create(:project, group: shared_group_parent) }
+ set(:project) { create(:project, group: shared_group) }
+ set(:project_child) { create(:project, group: shared_group_child) }
+
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group)
+ end
+
+ context 'when feature flag share_group_with_group is enabled' do
+ before do
+ stub_feature_flags(share_group_with_group: true)
+ end
+
+ context 'group user' do
+ let(:user) { group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER)
+ expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+ end
+
+ context 'when feature flag share_group_with_group is disabled' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ context 'group user' do
+ let(:user) { group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
new file mode 100644
index 00000000000..e4ad5703a10
--- /dev/null
+++ b/spec/models/group_group_link_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupGroupLink do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:shared_group) { create(:group) }
+ let_it_be(:group_group_link) do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: group)
+ end
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:shared_group) }
+ it { is_expected.to belong_to(:shared_with_group) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:shared_group) }
+
+ it do
+ is_expected.to(
+ validate_uniqueness_of(:shared_group_id)
+ .scoped_to(:shared_with_group_id)
+ .with_message('The group has already been shared with this group'))
+ end
+
+ it { is_expected.to validate_presence_of(:shared_with_group) }
+ it { is_expected.to validate_presence_of(:group_access) }
+
+ it do
+ is_expected.to(
+ validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 520421ac5e3..9a89ffb1b2d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -525,6 +525,128 @@ describe Group do
it { expect(subject.parent).to be_kind_of(described_class) }
end
+ describe '#max_member_access_for_user' do
+ context 'group shared with another group' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ set(:group_parent) { create(:group, :private) }
+ set(:group) { create(:group, :private, parent: group_parent) }
+ set(:group_child) { create(:group, :private, parent: group) }
+
+ set(:shared_group_parent) { create(:group, :private) }
+ set(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ set(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ end
+
+ context 'when feature flag share_group_with_group is enabled' do
+ before do
+ stub_feature_flags(share_group_with_group: true)
+ end
+
+ context 'with user in the group' do
+ let(:user) { group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ context 'with user in the parent group' do
+ let(:user) { parent_group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+
+ context 'with user in the child group' do
+ let(:user) { child_group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+ end
+
+ context 'when feature flag share_group_with_group is disabled' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ context 'with user in the group' do
+ let(:user) { group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+
+ context 'with user in the parent group' do
+ let(:user) { parent_group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+
+ context 'with user in the child group' do
+ let(:user) { child_group_user }
+
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+ end
+ end
+
+ context 'multiple groups shared with group' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :private) }
+ let(:shared_group_parent) { create(:group, :private) }
+ let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+
+ before do
+ stub_feature_flags(share_group_with_group: true)
+
+ group.add_owner(user)
+
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group_parent,
+ group_access: GroupMember::MAINTAINER })
+ end
+
+ it 'returns correct access level' do
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
+ end
+ end
+ end
+
describe '#members_with_parents' do
let!(:group) { create(:group, :nested) }
let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 5c9e5746683..f2942020e16 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -151,9 +151,15 @@ describe API::Members do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |u| u['id'] }).to eq [developer.id, maintainer.id, nested_user.id, project_user.id, linked_group_user.id]
- expect(json_response.map { |u| u['access_level'] }).to eq [Gitlab::Access::DEVELOPER, Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER,
- Gitlab::Access::DEVELOPER, Gitlab::Access::DEVELOPER]
+
+ expected_users_and_access_levels = [
+ [developer.id, Gitlab::Access::DEVELOPER],
+ [maintainer.id, Gitlab::Access::OWNER],
+ [nested_user.id, Gitlab::Access::DEVELOPER],
+ [project_user.id, Gitlab::Access::DEVELOPER],
+ [linked_group_user.id, Gitlab::Access::DEVELOPER]
+ ]
+ expect(json_response.map { |u| [u['id'], u['access_level']] }).to match_array(expected_users_and_access_levels)
end
it 'finds all group members including inherited members' do
diff --git a/spec/services/groups/group_links/create_service_spec.rb b/spec/services/groups/group_links/create_service_spec.rb
new file mode 100644
index 00000000000..ca005536e0d
--- /dev/null
+++ b/spec/services/groups/group_links/create_service_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::GroupLinks::CreateService, '#execute' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ set(:group_parent) { create(:group, :private) }
+ set(:group) { create(:group, :private, parent: group_parent) }
+ set(:group_child) { create(:group, :private, parent: group) }
+
+ set(:shared_group_parent) { create(:group, :private) }
+ set(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ set(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ set(:project_parent) { create(:project, group: shared_group_parent) }
+ set(:project) { create(:project, group: shared_group) }
+ set(:project_child) { create(:project, group: shared_group_child) }
+
+ let(:opts) do
+ {
+ shared_group_access: Gitlab::Access::DEVELOPER,
+ expires_at: nil
+ }
+ end
+ let(:user) { group_user }
+
+ subject { described_class.new(group, user, opts) }
+
+ before do
+ group.add_guest(group_user)
+ shared_group.add_owner(group_user)
+ end
+
+ it 'adds group to another group' do
+ expect { subject.execute(shared_group) }.to change { group.shared_group_links.count }.from(0).to(1)
+ end
+
+ it 'returns false if shared group is blank' do
+ expect { subject.execute(nil) }.not_to change { group.shared_group_links.count }
+ end
+
+ context 'user does not have access to group' do
+ let(:user) { create(:user) }
+
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'returns error' do
+ result = subject.execute(shared_group)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(404)
+ end
+ end
+
+ context 'user does not have admin access to shared group' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_guest(user)
+ shared_group.add_developer(user)
+ end
+
+ it 'returns error' do
+ result = subject.execute(shared_group)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(404)
+ end
+ end
+
+ context 'group hierarchies' do
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+ end
+
+ context 'group user' do
+ let(:user) { group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_truthy
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_truthy
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_falsey
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index b7ba4d61723..5ceb54eb2d5 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -21,8 +21,8 @@ describe 'Every Sidekiq worker' do
missing_from_file = worker_queues - file_worker_queues
expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
- unncessarily_in_file = file_worker_queues - worker_queues
- expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
+ unnecessarily_in_file = file_worker_queues - worker_queues
+ expect(unnecessarily_in_file).to be_empty, "expected #{unnecessarily_in_file.to_a.inspect} not to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
end
it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do
@@ -42,7 +42,7 @@ describe 'Every Sidekiq worker' do
end
# All Sidekiq worker classes should declare a valid `feature_category`
- # or explicitely be excluded with the `feature_category_not_owned!` annotation.
+ # or explicitly be excluded with the `feature_category_not_owned!` annotation.
# Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do
Gitlab::SidekiqConfig.workers.each do |worker|
@@ -62,5 +62,36 @@ describe 'Every Sidekiq worker' do
expect(feature_categories).to include(worker.get_feature_category), "expected #{worker.inspect} to declare a valid feature_category, but got #{worker.get_feature_category}"
end
end
+
+ # Memory-bound workers are very expensive to run, since they need to run on nodes with very low
+ # concurrency, so that each job can consume a large amounts of memory. For this reason, on
+ # GitLab.com, when a large number of memory-bound jobs arrive at once, we let them queue up
+ # rather than scaling the hardware to meet the SLO. For this reason, memory-bound,
+ # latency-sensitive jobs are explicitly discouraged and disabled.
+ it 'is (exclusively) memory-bound or latency-sentitive, not both', :aggregate_failures do
+ latency_sensitive_workers = Gitlab::SidekiqConfig.workers
+ .select(&:latency_sensitive_worker?)
+
+ latency_sensitive_workers.each do |worker|
+ expect(worker.get_worker_resource_boundary).not_to eq(:memory), "#{worker.inspect} cannot be both memory-bound and latency sensitive"
+ end
+ end
+
+ # In high traffic installations, such as GitLab.com, `latency_sensitive` workers run in a
+ # dedicated fleet. In order to ensure short queue times, `latency_sensitive` jobs have strict
+ # SLOs in order to ensure throughput. However, when a worker depends on an external service,
+ # such as a user's k8s cluster or a third-party internet service, we cannot guarantee latency,
+ # and therefore throughput. An outage to an 3rd party service could therefore impact throughput
+ # on other latency_sensitive jobs, leading to degradation through the GitLab application.
+ # Please see doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for more
+ # details.
+ it 'has (exclusively) external dependencies or is latency-sentitive, not both', :aggregate_failures do
+ latency_sensitive_workers = Gitlab::SidekiqConfig.workers
+ .select(&:latency_sensitive_worker?)
+
+ latency_sensitive_workers.each do |worker|
+ expect(worker.worker_has_external_dependencies?).to be_falsey, "#{worker.inspect} cannot have both external dependencies and be latency sensitive"
+ end
+ end
end
end