diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-30 18:14:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-30 18:14:17 +0300 |
commit | 3fe9588b1c1c4fb58f8ba8e9c27244fc2fc1c103 (patch) | |
tree | d19448d010ff9d58fed14846736ee358fb6b3327 /spec | |
parent | ad8eea383406037a207c80421e6e4bfa357f8044 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/groups/group_links_controller_spec.rb | 114 | ||||
-rw-r--r-- | spec/factories/group_group_links.rb | 9 | ||||
-rw-r--r-- | spec/frontend/monitoring/components/charts/anomaly_spec.js | 303 | ||||
-rw-r--r-- | spec/frontend/monitoring/mock_data.js | 161 | ||||
-rw-r--r-- | spec/frontend/repository/components/table/row_spec.js | 2 | ||||
-rw-r--r-- | spec/javascripts/monitoring/charts/time_series_spec.js | 42 | ||||
-rw-r--r-- | spec/javascripts/monitoring/mock_data.js | 6 | ||||
-rw-r--r-- | spec/javascripts/monitoring/panel_type_spec.js | 31 | ||||
-rw-r--r-- | spec/javascripts/monitoring/utils_spec.js | 38 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/all_models.yml | 1 | ||||
-rw-r--r-- | spec/lib/gitlab/project_authorizations_spec.rb | 167 | ||||
-rw-r--r-- | spec/models/group_group_link_spec.rb | 36 | ||||
-rw-r--r-- | spec/models/group_spec.rb | 122 | ||||
-rw-r--r-- | spec/requests/api/members_spec.rb | 12 | ||||
-rw-r--r-- | spec/services/groups/group_links/create_service_spec.rb | 119 | ||||
-rw-r--r-- | spec/workers/every_sidekiq_worker_spec.rb | 37 |
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 |