summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-21 15:09:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-21 15:09:35 +0000
commit9c6578ed4e0bc92cd838ef96d978df54403e9609 (patch)
tree5dff7ad20ae6402e4b7a5a44fe4e81ef04855cdf /spec
parent2af44d609eb8a1579169f9a350bc531d1081d77f (diff)
downloadgitlab-ce-9c6578ed4e0bc92cd838ef96d978df54403e9609.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js56
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js124
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js1
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js2
-rw-r--r--spec/graphql/mutations/boards/lists/update_spec.rb58
-rw-r--r--spec/lib/api/helpers_spec.rb98
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb16
-rw-r--r--spec/lib/gitlab/regex_spec.rb130
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb8
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/update_spec.rb40
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb505
-rw-r--r--spec/requests/rack_attack_global_spec.rb186
-rw-r--r--spec/services/application_settings/update_service_spec.rb26
-rw-r--r--spec/services/git/wiki_push_service_spec.rb8
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb5
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb199
-rw-r--r--spec/workers/container_expiration_policy_worker_spec.rb12
26 files changed, 1222 insertions, 526 deletions
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index e8fb036368a..30914ba99a5 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -22,6 +22,7 @@ describe('graph component', () => {
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
+ showLinks: false,
viewType: STAGE_VIEW,
configPaths: {
metricsPath: '',
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 8c469966be4..d785818166d 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -15,6 +15,7 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import { mockPipelineResponse } from './mock_data';
@@ -31,7 +32,9 @@ describe('Pipeline graph wrapper', () => {
let wrapper;
const getAlert = () => wrapper.find(GlAlert);
+ const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getLinksLayer = () => wrapper.findComponent(LinksLayer);
const getGraph = () => wrapper.find(PipelineGraph);
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const getAllStageColumnGroupsInColumn = () =>
@@ -59,6 +62,7 @@ describe('Pipeline graph wrapper', () => {
};
const createComponentWithApollo = ({
+ data = {},
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMount,
provide = {},
@@ -66,7 +70,7 @@ describe('Pipeline graph wrapper', () => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
- createComponent({ apolloProvider, provide, mountFn });
+ createComponent({ apolloProvider, data, provide, mountFn });
};
afterEach(() => {
@@ -74,6 +78,15 @@ describe('Pipeline graph wrapper', () => {
wrapper = null;
});
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
@@ -282,6 +295,36 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when pipelineGraphLayersView feature flag is on and layers view is selected', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ },
+ mountFn: mount,
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('sets showLinks to true', async () => {
+ /* This spec uses .props for performance reasons. */
+ expect(getLinksLayer().exists()).toBe(true);
+ expect(getLinksLayer().props('showLinks')).toBe(false);
+ expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
+ await getDependenciesToggle().trigger('click');
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
+ });
+ });
+
describe('when feature flag is on and local storage is set', () => {
beforeEach(async () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
@@ -299,10 +342,15 @@ describe('Pipeline graph wrapper', () => {
await wrapper.vm.$nextTick();
});
+ afterEach(() => {
+ localStorage.clear();
+ });
+
it('reads the view type from localStorage when available', () => {
- expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain(
- 'needs:',
- );
+ const viewSelectorNeedsSegment = wrapper
+ .findAll('[data-testid="pipeline-view-selector"] > label')
+ .at(1);
+ expect(viewSelectorNeedsSegment.classes()).toContain('active');
});
});
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
new file mode 100644
index 00000000000..abf25a84634
--- /dev/null
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -0,0 +1,124 @@
+import { GlLoadingIcon, GlSegmentedControl } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
+import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
+
+describe('the graph view selector component', () => {
+ let wrapper;
+
+ const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
+ const findViewTypeSelector = () => wrapper.findComponent(GlSegmentedControl);
+ const findStageViewLabel = () => findViewTypeSelector().findAll('label').at(0);
+ const findLayersViewLabel = () => findViewTypeSelector().findAll('label').at(1);
+ const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
+ const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
+
+ const defaultProps = {
+ showLinks: false,
+ type: STAGE_VIEW,
+ };
+
+ const defaultData = {
+ showLinksActive: false,
+ isToggleLoading: false,
+ isSwitcherLoading: false,
+ };
+
+ const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(GraphViewSelector, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return {
+ ...defaultData,
+ ...data,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when showing stage view', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
+ });
+
+ it('shows the Stage view label as active in the selector', () => {
+ expect(findStageViewLabel().classes()).toContain('active');
+ });
+
+ it('does not show the Job dependencies (links) toggle', () => {
+ expect(findDependenciesToggle().exists()).toBe(false);
+ });
+ });
+
+ describe('when showing Job dependencies view', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mount,
+ props: {
+ type: LAYER_VIEW,
+ },
+ });
+ });
+
+ it('shows the Job dependencies view label as active in the selector', () => {
+ expect(findLayersViewLabel().classes()).toContain('active');
+ });
+
+ it('shows the Job dependencies (links) toggle', () => {
+ expect(findDependenciesToggle().exists()).toBe(true);
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ createComponent({
+ mountFn: mount,
+ props: {
+ type: LAYER_VIEW,
+ },
+ });
+ });
+
+ it('shows loading state and emits updateViewType when view type toggled', async () => {
+ expect(wrapper.emitted().updateViewType).toBeUndefined();
+ expect(findSwitcherLoader().exists()).toBe(false);
+
+ await findStageViewLabel().trigger('click');
+ /*
+ Loading happens before the event is emitted or timers are run.
+ Then we run the timer because the event is emitted in setInterval
+ which is what gives the loader a chace to show up.
+ */
+ expect(findSwitcherLoader().exists()).toBe(true);
+ jest.runOnlyPendingTimers();
+
+ expect(wrapper.emitted().updateViewType).toHaveLength(1);
+ expect(wrapper.emitted().updateViewType).toEqual([[STAGE_VIEW]]);
+ });
+
+ it('shows loading state and emits updateShowLinks when show links toggle is clicked', async () => {
+ expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
+ expect(findToggleLoader().exists()).toBe(false);
+
+ await findDependenciesToggle().trigger('click');
+ /*
+ Loading happens before the event is emitted or timers are run.
+ Then we run the timer because the event is emitted in setInterval
+ which is what gives the loader a chace to show up.
+ */
+ expect(findToggleLoader().exists()).toBe(true);
+ jest.runOnlyPendingTimers();
+
+ expect(wrapper.emitted().updateShowLinksState).toHaveLength(1);
+ expect(wrapper.emitted().updateShowLinksState).toEqual([[true]]);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 8aecfc1b649..8a37c3cae4e 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -26,6 +26,7 @@ describe('Linked Pipelines Column', () => {
const defaultProps = {
columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
+ showLinks: false,
type: DOWNSTREAM,
viewType: STAGE_VIEW,
configPaths: {
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 5e5365eef30..1c4c34f317c 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -1,6 +1,4 @@
-import { GlAlert } from '@gitlab/ui';
-import { fireEvent, within } from '@testing-library/dom';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
@@ -8,25 +6,18 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
let wrapper;
- const withinComponent = () => within(wrapper.element);
- const findAlert = () => wrapper.find(GlAlert);
- const findShowAnyways = () =>
- withinComponent().getByText(wrapper.vm.$options.i18n.showLinksAnyways);
const findLinksInner = () => wrapper.find(LinksInner);
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
const containerId = `pipeline-links-container-${pipeline.id}`;
const slotContent = "<div>Ceci n'est pas un graphique</div>";
- const tooManyStages = Array(101)
- .fill(0)
- .flatMap(() => pipeline.stages);
-
const defaultProps = {
containerId,
containerMeasurements: { width: 400, height: 400 },
pipelineId: pipeline.id,
pipelineData: pipeline.stages,
+ showLinks: false,
};
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
@@ -49,7 +40,7 @@ describe('links layer component', () => {
wrapper = null;
});
- describe('with data under max stages', () => {
+ describe('with show links off', () => {
beforeEach(() => {
createComponent();
});
@@ -58,63 +49,40 @@ describe('links layer component', () => {
expect(wrapper.html()).toContain(slotContent);
});
- it('renders the inner links component', () => {
- expect(findLinksInner().exists()).toBe(true);
+ it('does not render inner links component', () => {
+ expect(findLinksInner().exists()).toBe(false);
});
});
- describe('with more than the max number of stages', () => {
- describe('rendering', () => {
- beforeEach(() => {
- createComponent({ props: { pipelineData: tooManyStages } });
- });
-
- it('renders the default slot', () => {
- expect(wrapper.html()).toContain(slotContent);
- });
-
- it('renders the alert component', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('does not render the inner links component', () => {
- expect(findLinksInner().exists()).toBe(false);
+ describe('with show links on', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showLinks: true,
+ },
});
});
- describe('with width or height measurement at 0', () => {
- beforeEach(() => {
- createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
- });
-
- it('renders the default slot', () => {
- expect(wrapper.html()).toContain(slotContent);
- });
-
- it('does not render the alert component', () => {
- expect(findAlert().exists()).toBe(false);
- });
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
- it('does not render the inner links component', () => {
- expect(findLinksInner().exists()).toBe(false);
- });
+ it('renders the inner links component', () => {
+ expect(findLinksInner().exists()).toBe(true);
});
+ });
- describe('interactions', () => {
- beforeEach(() => {
- createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } });
- });
+ describe('with width or height measurement at 0', () => {
+ beforeEach(() => {
+ createComponent({ props: { containerMeasurements: { width: 0, height: 100 } } });
+ });
- it('renders the disable button', () => {
- expect(findShowAnyways()).not.toBe(null);
- });
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
- it('shows links when override is clicked', async () => {
- expect(findLinksInner().exists()).toBe(false);
- fireEvent(findShowAnyways(), new MouseEvent('click', { bubbles: true }));
- await wrapper.vm.$nextTick();
- expect(findLinksInner().exists()).toBe(true);
- });
+ it('does not render the inner links component', () => {
+ expect(findLinksInner().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 66ceebed489..6a31742141b 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -32,8 +32,8 @@ describe('Commit component', () => {
createComponent({
tag: false,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -55,8 +55,8 @@ describe('Commit component', () => {
props = {
tag: true,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -122,8 +122,8 @@ describe('Commit component', () => {
props = {
tag: false,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -145,8 +145,8 @@ describe('Commit component', () => {
props = {
tag: false,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -158,7 +158,7 @@ describe('Commit component', () => {
createComponent(props);
const refEl = wrapper.find('.ref-name');
- expect(refEl.text()).toContain('master');
+ expect(refEl.text()).toContain('main');
expect(refEl.attributes('href')).toBe(props.commitRef.ref_url);
@@ -173,8 +173,8 @@ describe('Commit component', () => {
props = {
tag: false,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -206,8 +206,8 @@ describe('Commit component', () => {
props = {
tag: false,
commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/main',
},
commitUrl:
'https://gitlab.com/gitlab-org/gitlab-foss/commit/b7836eddf62d663c665769e1b0960197fd215067',
@@ -232,8 +232,8 @@ describe('Commit component', () => {
it('should render path as href attribute', () => {
props = {
commitRef: {
- name: 'master',
- path: 'http://localhost/namespace2/gitlabhq/tree/master',
+ name: 'main',
+ path: 'http://localhost/namespace2/gitlabhq/tree/main',
},
};
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index c24528ba4d2..46b7e49979e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -37,7 +37,7 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
-export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }];
+export const mockBranches = [{ name: 'Main' }, { name: 'v1.x' }, { name: 'my-Branch' }];
export const mockRegularMilestone = {
id: 1,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index a20bc4986fc..28741fe0ab4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -77,7 +77,7 @@ describe('BranchToken', () => {
describe('currentValue', () => {
it('returns lowercase string for `value.data`', () => {
- expect(wrapper.vm.currentValue).toBe('master');
+ expect(wrapper.vm.currentValue).toBe('main');
});
});
diff --git a/spec/graphql/mutations/boards/lists/update_spec.rb b/spec/graphql/mutations/boards/lists/update_spec.rb
index d5d8a2af6bf..c82cbbfdd83 100644
--- a/spec/graphql/mutations/boards/lists/update_spec.rb
+++ b/spec/graphql/mutations/boards/lists/update_spec.rb
@@ -3,54 +3,14 @@
require 'spec_helper'
RSpec.describe Mutations::Boards::Lists::Update do
- let_it_be(:group) { create(:group, :private) }
- let_it_be(:board) { create(:board, group: group) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:list) { create(:list, board: board, position: 0) }
- let_it_be(:list2) { create(:list, board: board) }
- let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
- let(:list_update_params) { { position: 1, collapsed: true } }
-
- before_all do
- group.add_reporter(reporter)
- group.add_guest(guest)
- list.update_preferences_for(reporter, collapsed: false)
- end
-
- subject { mutation.resolve(list: list, **list_update_params) }
-
- describe '#resolve' do
- context 'with permission to admin board lists' do
- let(:current_user) { reporter }
-
- it 'updates the list position and collapsed state as expected' do
- subject
-
- reloaded_list = list.reload
- expect(reloaded_list.position).to eq(1)
- expect(reloaded_list.collapsed?(current_user)).to eq(true)
- end
- end
-
- context 'with permission to read board lists' do
- let(:current_user) { guest }
-
- it 'updates the list collapsed state but not the list position' do
- subject
-
- reloaded_list = list.reload
- expect(reloaded_list.position).to eq(0)
- expect(reloaded_list.collapsed?(current_user)).to eq(true)
- end
- end
-
- context 'without permission to read board lists' do
- let(:current_user) { create(:user) }
-
- it 'raises Resource Not Found error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
- end
+ context 'on group issue boards' do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:board) { create(:board, group: group) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let_it_be(:list2) { create(:list, board: board) }
+
+ it_behaves_like 'update board list mutation'
end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 15b22fcf25e..87cd0d4388c 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::Helpers do
+ using RSpec::Parameterized::TableSyntax
+
subject { Class.new.include(described_class).new }
describe '#find_project' do
@@ -99,6 +101,59 @@ RSpec.describe API::Helpers do
end
end
+ describe '#find_project!' do
+ let_it_be(:project) { create(:project) }
+
+ let(:user) { project.owner}
+
+ before do
+ allow(subject).to receive(:current_user).and_return(user)
+ allow(subject).to receive(:authorized_project_scope?).and_return(true)
+ allow(subject).to receive(:job_token_authentication?).and_return(false)
+ allow(subject).to receive(:authenticate_non_public?).and_return(false)
+ end
+
+ shared_examples 'project finder' do
+ context 'when project exists' do
+ it 'returns requested project' do
+ expect(subject.find_project!(existing_id)).to eq(project)
+ end
+
+ it 'returns nil' do
+ expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404)
+ expect(subject.find_project!(non_existing_id)).to be_nil
+ end
+ end
+ end
+
+ context 'when ID is used as an argument' do
+ let(:existing_id) { project.id }
+ let(:non_existing_id) { non_existing_record_id }
+
+ it_behaves_like 'project finder'
+ end
+
+ context 'when PATH is used as an argument' do
+ let(:existing_id) { project.full_path }
+ let(:non_existing_id) { 'something/else' }
+
+ it_behaves_like 'project finder'
+
+ context 'with an invalid PATH' do
+ let(:non_existing_id) { 'undefined' } # path without slash
+
+ it_behaves_like 'project finder'
+
+ it 'does not hit the database' do
+ expect(Project).not_to receive(:find_by_full_path)
+ expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404)
+
+ subject.find_project!(non_existing_id)
+ end
+ end
+ end
+ end
+
describe '#find_namespace' do
let(:namespace) { create(:namespace) }
@@ -191,6 +246,49 @@ RSpec.describe API::Helpers do
it_behaves_like 'user namespace finder'
end
+ describe '#authorized_project_scope?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:job) { create(:ci_build) }
+
+ let(:send_authorized_project_scope) { subject.authorized_project_scope?(project) }
+
+ where(:job_token_authentication, :route_setting, :feature_flag, :same_job_project, :expected_result) do
+ false | false | false | false | true
+ false | false | false | true | true
+ false | false | true | false | true
+ false | false | true | true | true
+ false | true | false | false | true
+ false | true | false | true | true
+ false | true | true | false | true
+ false | true | true | true | true
+ true | false | false | false | true
+ true | false | false | true | true
+ true | false | true | false | true
+ true | false | true | true | true
+ true | true | false | false | false
+ true | true | false | true | false
+ true | true | true | false | false
+ true | true | true | true | true
+ end
+
+ with_them do
+ before do
+ allow(subject).to receive(:job_token_authentication?).and_return(job_token_authentication)
+ allow(subject).to receive(:route_authentication_setting).and_return(job_token_scope: route_setting ? :project : nil)
+ allow(subject).to receive(:current_authenticated_job).and_return(job)
+ allow(job).to receive(:project).and_return(same_job_project ? project : other_project)
+
+ stub_feature_flags(ci_job_token_scope: false)
+ stub_feature_flags(ci_job_token_scope: project) if feature_flag
+ end
+
+ it 'returns the expected result' do
+ expect(send_authorized_project_scope).to eq(expected_result)
+ end
+ end
+ end
+
describe '#send_git_blob' do
let(:repository) { double }
let(:blob) { double(name: 'foobar') }
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index 36bff42d937..eb7deb08063 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -58,22 +58,6 @@ RSpec.describe Gitlab::Git::Wiki do
end
end
- describe '#delete_page' do
- after do
- destroy_page('page1')
- end
-
- it 'only removes the page with the same path' do
- create_page('page1', 'content')
- create_page('*', 'content')
-
- subject.delete_page('*', commit_details('whatever'))
-
- expect(subject.list_pages.count).to eq 1
- expect(subject.list_pages.first.title).to eq 'page1'
- end
- end
-
describe '#preview_slug' do
where(:title, :format, :expected_slug) do
'The Best Thing' | :markdown | 'The-Best-Thing'
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index f62a3c74005..4c2a6d6f10c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -726,4 +726,134 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('v../../../../../1.2.3') }
it { is_expected.not_to match('v%2e%2e%2f1.2.3') }
end
+
+ describe 'Packages::API_PATH_REGEX' do
+ subject { described_class::Packages::API_PATH_REGEX }
+
+ it { is_expected.to match('/api/v4/group/12345/-/packages/composer/p/123456789') }
+ it { is_expected.to match('/api/v4/group/12345/-/packages/composer/p2/pkg_name') }
+ it { is_expected.to match('/api/v4/group/12345/-/packages/composer/packages') }
+ it { is_expected.to match('/api/v4/group/12345/-/packages/composer/pkg_name') }
+ it { is_expected.to match('/api/v4/groups/1234/-/packages/maven/a/path/file.jar') }
+ it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/index') }
+ it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/metadata/pkg_name/1.3.4') }
+ it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/metadata/pkg_name/index') }
+ it { is_expected.to match('/api/v4/groups/1234/-/packages/nuget/query') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/digest') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/download_urls') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/digest') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/download_urls') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/upload_urls') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/upload_urls') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/conans/search') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name/authorize') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name/authorize') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/ping') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/users/authenticate') }
+ it { is_expected.to match('/api/v4/packages/conan/v1/users/check_credentials') }
+ it { is_expected.to match('/api/v4/packages/maven/a/path/file.jar') }
+ it { is_expected.to match('/api/v4/packages/npm/-/package/pkg_name/dist-tags') }
+ it { is_expected.to match('/api/v4/packages/npm/-/package/pkg_name/dist-tags/tag') }
+ it { is_expected.to match('/api/v4/packages/npm/pkg_name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/composer') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/composer/archives/pkg_name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/digest') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/download_urls') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/digest') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/download_urls') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/packages/pkg_ref/upload_urls') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/pkg_name/1.2.3/username/stable/upload_urls') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/conans/search') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/export/file.name/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/files/pkg_name/1.2.3/username/stable/2.3/package/pkg_ref/pkg_revision/file.name/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/ping') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/users/authenticate') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/conan/v1/users/check_credentials') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/compon/binary-x64/Packages') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/InRelease') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/Release') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/dists/stable/Release.gpg') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/file.name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/file.name/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/debian/pool/compon/e/pkg/file.name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/generic/pkg_name/1.3.4/myfile.txt') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/generic/pkg_name/1.3.4/myfile.txt/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.info') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.mod') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/11.2.3.zip') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/go/my_module/@v/list') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/maven/a/path/file.jar') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/maven/a/path/file.jar/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/npm/-/package/pkg_name/dist-tags') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/npm/-/package/pkg_name/dist-tags/tag') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/npm/pkg_name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/npm/pkg_name/-/tarball.tgz') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/download/pkg_name/1.3.4/pkg.npkg') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/download/pkg_name/index') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/index') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/metadata/pkg_name/1.3.4') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/metadata/pkg_name/index') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/nuget/query') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/pypi') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/pypi/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/pypi/files/1234567890/file.identifier') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/pypi/simple/pkg_name') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/dependencies') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/gems') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/api/v1/gems/authorize') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/gems/pkg') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/pkg') }
+ it { is_expected.to match('/api/v4/projects/1234/packages/rubygems/quick/Marshal.4.8/pkg') }
+ it { is_expected.not_to match('') }
+ it { is_expected.not_to match('foo') }
+ it { is_expected.not_to match('/api/v4') }
+ it { is_expected.not_to match('/api/v4/version') }
+ it { is_expected.not_to match('/api/v4/packages') }
+ it { is_expected.not_to match('/api/v4/packages/') }
+ it { is_expected.not_to match('/api/v4/group') }
+ it { is_expected.not_to match('/api/v4/group/12345') }
+ it { is_expected.not_to match('/api/v4/group/12345/-') }
+ it { is_expected.not_to match('/api/v4/group/12345/-/packages') }
+ it { is_expected.not_to match('/api/v4/group/12345/-/packages/') }
+ it { is_expected.not_to match('/api/v4/group/12345/-/packages/50') }
+ it { is_expected.not_to match('/api/v4/groups') }
+ it { is_expected.not_to match('/api/v4/groups/12345') }
+ it { is_expected.not_to match('/api/v4/groups/12345/-') }
+ it { is_expected.not_to match('/api/v4/groups/12345/-/packages') }
+ it { is_expected.not_to match('/api/v4/groups/12345/-/packages/') }
+ it { is_expected.not_to match('/api/v4/groups/12345/-/packages/50') }
+ it { is_expected.not_to match('/api/v4/groups/12345/packages') }
+ it { is_expected.not_to match('/api/v4/groups/12345/packages/') }
+ it { is_expected.not_to match('/api/v4/groups/12345/badges') }
+ it { is_expected.not_to match('/api/v4/groups/12345/issues') }
+ it { is_expected.not_to match('/api/v4/projects') }
+ it { is_expected.not_to match('/api/v4/projects/1234') }
+ it { is_expected.not_to match('/api/v4/projects/1234/packages') }
+ it { is_expected.not_to match('/api/v4/projects/1234/packages/') }
+ it { is_expected.not_to match('/api/v4/projects/1234/packages/50') }
+ it { is_expected.not_to match('/api/v4/projects/1234/packages/50/package_files') }
+ it { is_expected.not_to match('/api/v4/projects/1234/merge_requests') }
+ it { is_expected.not_to match('/api/v4/projects/1234/registry/repositories') }
+ it { is_expected.not_to match('/api/v4/projects/1234/issues') }
+ it { is_expected.not_to match('/api/v4/projects/1234/members') }
+ it { is_expected.not_to match('/api/v4/projects/1234/milestones') }
+
+ # Group level Debian API endpoints are not matched as it's not using the correct prefix (groups/:id/-/packages/)
+ # TODO: Update Debian group level endpoints urls and adjust this specs: https://gitlab.com/gitlab-org/gitlab/-/issues/326805
+ it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/compon/binary-compo/Packages') }
+ it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/InRelease') }
+ it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/Release') }
+ it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/dists/stable/Release.gpg') }
+ it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/pool/compon/a/pkg/file.name') }
+ end
end
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 718ab3b2d95..6a529328cde 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -59,6 +59,14 @@ RSpec.describe Gitlab::UsageDataQueries do
end
end
+ describe '.histogram' do
+ it 'returns the histogram sql' do
+ expect(described_class.histogram(AlertManagement::HttpIntegration.active,
+ :project_id, buckets: 1..2, bucket_size: 101))
+ .to eq('WITH "count_cte" AS (SELECT COUNT(*) AS count_grouped FROM "alert_management_http_integrations" WHERE "alert_management_http_integrations"."active" = TRUE GROUP BY "alert_management_http_integrations"."project_id") SELECT WIDTH_BUCKET("count_cte"."count_grouped", 1, 2, 100) AS buckets, "count_cte"."count" FROM "count_cte" GROUP BY buckets ORDER BY buckets')
+ end
+ end
+
describe 'min/max methods' do
it 'returns nil' do
# user min/max
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 808932ce7e4..4b0731e0720 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -785,6 +785,10 @@ RSpec.describe ApplicationSetting do
throttle_authenticated_api_period_in_seconds
throttle_authenticated_web_requests_per_period
throttle_authenticated_web_period_in_seconds
+ throttle_unauthenticated_packages_api_requests_per_period
+ throttle_unauthenticated_packages_api_period_in_seconds
+ throttle_authenticated_packages_api_requests_per_period
+ throttle_authenticated_packages_api_period_in_seconds
]
end
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 9d63d675a02..8a05d20fb33 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -6,32 +6,32 @@ RSpec.describe API::DebianGroupPackages do
include WorkhorseHelpers
include_context 'Debian repository shared context', :group do
- describe 'GET groups/:id/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release.gpg" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/Release' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/InRelease" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" }
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
end
- describe 'GET groups/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
end
- describe 'GET groups/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
- let(:url) { "/groups/#{group.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+ describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
end
diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
index 8e24e053211..00140007616 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
@@ -14,43 +14,5 @@ RSpec.describe 'Update of an existing board list' do
let(:mutation) { graphql_mutation(:update_board_list, input) }
let(:mutation_response) { graphql_mutation_response(:update_board_list) }
- context 'the user is not allowed to read board lists' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- before do
- list.update_preferences_for(current_user, collapsed: false)
- end
-
- context 'when user has permissions to admin board lists' do
- before do
- group.add_reporter(current_user)
- end
-
- it 'updates the list position and collapsed state' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list']).to include(
- 'position' => 1,
- 'collapsed' => true
- )
- end
- end
-
- context 'when user has permissions to read board lists' do
- before do
- group.add_guest(current_user)
- end
-
- it 'updates the list collapsed state but not the list position' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list']).to include(
- 'position' => 0,
- 'collapsed' => true
- )
- end
- end
+ it_behaves_like 'a GraphQL request to update board list'
end
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 15871426ec5..f3da99573fe 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -6,12 +6,14 @@ RSpec.describe API::ProjectContainerRepositories do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project, :private) }
+ let_it_be(:project2) { create(:project, :public) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:root_repository) { create(:container_repository, :root, project: project) }
let(:test_repository) { create(:container_repository, project: project) }
+ let(:root_repository2) { create(:container_repository, :root, project: project2) }
let(:users) do
{
@@ -24,315 +26,408 @@ RSpec.describe API::ProjectContainerRepositories do
end
let(:api_user) { maintainer }
+ let(:job) { create(:ci_build, :running, user: api_user, project: project) }
+ let(:job2) { create(:ci_build, :running, user: api_user, project: project2) }
- before do
+ let(:method) { :get }
+ let(:params) { {} }
+
+ before_all do
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
- stub_container_registry_config(enabled: true)
+ project2.add_maintainer(maintainer)
+ project2.add_developer(developer)
+ project2.add_reporter(reporter)
+ project2.add_guest(guest)
+ end
+ before do
root_repository
test_repository
- end
- describe 'GET /projects/:id/registry/repositories' do
- let(:url) { "/projects/#{project.id}/registry/repositories" }
-
- subject { get api(url, api_user) }
+ stub_container_registry_config(enabled: true)
+ end
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
+ shared_context 'using API user' do
+ subject { public_send(method, api(url, api_user), params: params) }
+ end
- it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
- let(:object) { project }
+ shared_context 'using job token' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: true)
end
+
+ subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
end
- describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}", api_user) }
+ shared_context 'using job token from another project' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: true)
+ end
- it_behaves_like 'rejected container repository access', :developer, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
+ subject { public_send(method, api(url), params: { job_token: job2.token }) }
+ end
- context 'for maintainer' do
- let(:api_user) { maintainer }
+ shared_context 'using job token while ci_job_token_scope feature flag is disabled' do
+ before do
+ stub_exclusive_lease
+ stub_feature_flags(ci_job_token_scope: false)
+ end
- it 'schedules removal of repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id)
+ subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
+ end
- subject
+ shared_examples 'rejected job token scopes' do
+ include_context 'using job token from another project' do
+ it_behaves_like 'rejected container repository access', :maintainer, :forbidden
+ end
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ include_context 'using job token while ci_job_token_scope feature flag is disabled' do
+ it_behaves_like 'rejected container repository access', :maintainer, :forbidden
end
end
- describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
- subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user) }
-
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
-
- context 'for reporter' do
- let(:api_user) { reporter }
-
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
- end
-
- it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
-
- it 'returns a list of tags' do
- subject
+ describe 'GET /projects/:id/registry/repositories' do
+ let(:url) { "/projects/#{project.id}/registry/repositories" }
- expect(json_response.length).to eq(2)
- expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it 'returns a matching schema' do
- subject
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('registry/tags')
+ it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
+ let(:object) { project }
+ end
end
end
+
+ include_examples 'rejected job token scopes'
end
- describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user), params: params }
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}" }
- context 'disallowed' do
- let(:params) do
- { name_regex_delete: 'v10.*' }
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it_behaves_like 'rejected container repository access', :developer, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
- end
+ it_behaves_like 'rejected container repository access', :developer, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'delete_repository'
- context 'for maintainer' do
- let(:api_user) { maintainer }
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
- context 'without required parameters' do
- let(:params) { }
+ it 'schedules removal of repository' do
+ expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id)
- it 'returns bad request' do
- subject
+ subject
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
end
end
+ end
- context 'without name_regex' do
- let(:params) do
- { keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
-
- it 'returns bad request' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
+ include_examples 'rejected job token scopes'
+ end
- context 'passes all declared parameters' do
- let(:params) do
- { name_regex_delete: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
+ describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
- let(:worker_params) do
- { name_regex: nil,
- name_regex_delete: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- container_expiration_policy: false }
- end
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- it 'schedules cleanup of tags repository' do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id, worker_params)
+ context 'for reporter' do
+ let(:api_user) { reporter }
- subject
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
+ end
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ it_behaves_like 'a package tracking event', described_class.name, 'list_tags'
- context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
- it 'returns 400 with an error message' do
- stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+ it 'returns a list of tags' do
subject
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include('This request has already been made.')
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
end
- it 'executes service only for the first time' do
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
+ it 'returns a matching schema' do
+ subject
- 2.times { subject }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
end
end
end
+ end
- context 'with deprecated name_regex param' do
- let(:params) do
- { name_regex: 'v10.*',
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- other: 'some value' }
- end
-
- let(:worker_params) do
- { name_regex: 'v10.*',
- name_regex_delete: nil,
- name_regex_keep: 'v10.1.*',
- keep_n: 100,
- older_than: '1 day',
- container_expiration_policy: false }
- end
+ include_examples 'rejected job token scopes'
+ end
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags" }
- it 'schedules cleanup of tags repository' do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id, worker_params)
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- subject
+ context 'disallowed' do
+ let(:params) do
+ { name_regex_delete: 'v10.*' }
+ end
- expect(response).to have_gitlab_http_status(:accepted)
+ it_behaves_like 'rejected container repository access', :developer, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a package tracking event', described_class.name, 'delete_tag_bulk'
end
- end
- context 'with invalid regex' do
- let(:invalid_regex) { '*v10.' }
- let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
- RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
- it 'does not enqueue a job' do
- expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
+ context 'without required parameters' do
+ it 'returns bad request' do
+ subject
- subject
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it_behaves_like 'returning response status', :bad_request
+ context 'without name_regex' do
+ let(:params) do
+ { keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
- it 'returns an error message' do
- subject
+ it 'returns bad request' do
+ subject
- expect(json_response['error']).to include("#{param_name} is an invalid regexp")
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- end
- before do
- stub_last_activity_update
- stub_exclusive_lease(lease_key, timeout: 1.hour)
- end
+ context 'passes all declared parameters' do
+ let(:params) do
+ { name_regex_delete: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
+
+ let(:worker_params) do
+ { name_regex: nil,
+ name_regex_delete: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ container_expiration_policy: false }
+ end
+
+ let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
+
+ it 'schedules cleanup of tags repository' do
+ stub_last_activity_update
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id, worker_params)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
+ it 'returns 400 with an error message' do
+ stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('This request has already been made.')
+ end
+
+ it 'executes service only for the first time' do
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async).once
+
+ 2.times { subject }
+ end
+ end
+ end
+
+ context 'with deprecated name_regex param' do
+ let(:params) do
+ { name_regex: 'v10.*',
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
+
+ let(:worker_params) do
+ { name_regex: 'v10.*',
+ name_regex_delete: nil,
+ name_regex_keep: 'v10.1.*',
+ keep_n: 100,
+ older_than: '1 day',
+ container_expiration_policy: false }
+ end
+
+ it 'schedules cleanup of tags repository' do
+ stub_last_activity_update
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id, worker_params)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'with invalid regex' do
+ let(:invalid_regex) { '*v10.' }
+
+ RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
+ it 'does not enqueue a job' do
+ expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
+
+ subject
+ end
- %i[name_regex_delete name_regex name_regex_keep].each do |param_name|
- context "for #{param_name}" do
- let(:params) { { param_name => invalid_regex } }
+ it_behaves_like 'returning response status', :bad_request
- it_behaves_like 'rejecting the invalid regex', param_name
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['error']).to include("#{param_name} is an invalid regexp")
+ end
+ end
+
+ before do
+ stub_last_activity_update
+ end
+
+ %i[name_regex_delete name_regex name_regex_keep].each do |param_name|
+ context "for #{param_name}" do
+ let(:params) { { param_name => invalid_regex } }
+
+ it_behaves_like 'rejecting the invalid regex', param_name
+ end
+ end
end
end
end
end
+
+ include_examples 'rejected job token scopes'
end
describe 'GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
- subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
- it_behaves_like 'rejected container repository access', :guest, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- context 'for reporter' do
- let(:api_user) { reporter }
+ it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token'
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
- end
+ context 'for reporter' do
+ let(:api_user) { reporter }
- it 'returns a details of tag' do
- subject
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
- expect(json_response).to include(
- 'name' => 'rootA',
- 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
- 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
- 'total_size' => 2319870)
- end
+ it 'returns a details of tag' do
+ subject
+
+ expect(json_response).to include(
+ 'name' => 'rootA',
+ 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
+ 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
+ 'total_size' => 2319870)
+ end
- it 'returns a matching schema' do
- subject
+ it 'returns a matching schema' do
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('registry/tag')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tag')
+ end
+ end
end
end
+
+ include_examples 'rejected job token scopes'
end
describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA" }
let(:service) { double('service') }
- subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+ ['using API user', 'using job token'].each do |context|
+ context context do
+ include_context context
- it_behaves_like 'rejected container repository access', :reporter, :forbidden
- it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'rejected container repository access', :reporter, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
- context 'for developer', :snowplow do
- let(:api_user) { developer }
+ context 'for developer', :snowplow do
+ let(:api_user) { developer }
- context 'when there are multiple tags' do
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
- end
+ context 'when there are multiple tags' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA rootB), with_manifest: true)
+ end
- it 'properly removes tag' do
- expect(service).to receive(:execute).with(root_repository) { { status: :success } }
- expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
- subject
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
- end
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ end
+ end
- context 'when there\'s only one tag' do
- before do
- stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
- end
+ context 'when there\'s only one tag' do
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
- it 'properly removes tag' do
- expect(service).to receive(:execute).with(root_repository) { { status: :success } }
- expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ it 'properly removes tag' do
+ expect(service).to receive(:execute).with(root_repository) { { status: :success } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
- subject
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ end
+ end
end
end
end
+
+ include_examples 'rejected job token scopes'
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 972caec6eb3..f24f815e9c6 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -18,7 +18,11 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
throttle_authenticated_web_requests_per_period: 100,
throttle_authenticated_web_period_in_seconds: 1,
throttle_authenticated_protected_paths_request_per_period: 100,
- throttle_authenticated_protected_paths_in_seconds: 1
+ throttle_authenticated_protected_paths_in_seconds: 1,
+ throttle_unauthenticated_packages_api_requests_per_period: 100,
+ throttle_unauthenticated_packages_api_period_in_seconds: 1,
+ throttle_authenticated_packages_api_requests_per_period: 100,
+ throttle_authenticated_packages_api_period_in_seconds: 1
}
end
@@ -435,6 +439,186 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
end
+ describe 'Packages API' do
+ let(:request_method) { 'GET' }
+
+ context 'unauthenticated' do
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:throttle_setting_prefix) { 'throttle_unauthenticated_packages_api' }
+ let(:packages_path_that_does_not_require_authentication) { "/api/v4/projects/#{project.id}/packages/conan/v1/ping" }
+
+ def do_request
+ get packages_path_that_does_not_require_authentication
+ end
+
+ before do
+ settings_to_set[:throttle_unauthenticated_packages_api_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_unauthenticated_packages_api_period_in_seconds] = period_in_seconds
+ end
+
+ context 'when unauthenticated packages api throttle is disabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_packages_api_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when unauthenticated api throttle is enabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
+ settings_to_set[:throttle_unauthenticated_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the unauthenticated api rate limit' do
+ requests_per_period.times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { do_request }
+ end
+ end
+ end
+
+ context 'when unauthenticated packages api throttle is enabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_packages_api_requests_per_period] = requests_per_period # 1
+ settings_to_set[:throttle_unauthenticated_packages_api_period_in_seconds] = period_in_seconds # 10_000
+ settings_to_set[:throttle_unauthenticated_packages_api_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ requests_per_period.times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { do_request }
+ end
+
+ context 'when unauthenticated api throttle is lower' do
+ before do
+ settings_to_set[:throttle_unauthenticated_requests_per_period] = 0
+ settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
+ settings_to_set[:throttle_unauthenticated_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'ignores unauthenticated api throttle' do
+ requests_per_period.times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { do_request }
+ end
+ end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ let(:throttle_name) { 'throttle_unauthenticated_packages_api' }
+ end
+ end
+ end
+
+ context 'authenticated', :api do
+ let_it_be(:project) { create(:project, :internal) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { create(:personal_access_token, user: user) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:other_user_token) { create(:personal_access_token, user: other_user) }
+
+ let(:throttle_setting_prefix) { 'throttle_authenticated_packages_api' }
+ let(:api_partial_url) { "/projects/#{project.id}/packages/conan/v1/ping" }
+
+ before do
+ stub_application_setting(settings_to_set)
+ end
+
+ context 'with the token in the query string' do
+ let(:request_args) { [api(api_partial_url, personal_access_token: token), {}] }
+ let(:other_user_request_args) { [api(api_partial_url, personal_access_token: other_user_token), {}] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'precedence over authenticated api throttle' do
+ before do
+ settings_to_set[:throttle_authenticated_packages_api_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_authenticated_packages_api_period_in_seconds] = period_in_seconds
+ end
+
+ def do_request
+ get api(api_partial_url, personal_access_token: token)
+ end
+
+ context 'when authenticated packages api throttle is enabled' do
+ before do
+ settings_to_set[:throttle_authenticated_packages_api_enabled] = true
+ end
+
+ context 'when authenticated api throttle is lower' do
+ before do
+ settings_to_set[:throttle_authenticated_api_requests_per_period] = 0
+ settings_to_set[:throttle_authenticated_api_period_in_seconds] = period_in_seconds
+ settings_to_set[:throttle_authenticated_api_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'ignores authenticated api throttle' do
+ requests_per_period.times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { do_request }
+ end
+ end
+ end
+
+ context 'when authenticated packages api throttle is disabled' do
+ before do
+ settings_to_set[:throttle_authenticated_packages_api_enabled] = false
+ end
+
+ context 'when authenticated api throttle is enabled' do
+ before do
+ settings_to_set[:throttle_authenticated_api_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_authenticated_api_period_in_seconds] = period_in_seconds
+ settings_to_set[:throttle_authenticated_api_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the authenticated api rate limit' do
+ requests_per_period.times do
+ do_request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { do_request }
+ end
+ end
+ end
+ end
+ end
+ end
+
describe 'throttle bypass header' do
let(:headers) { {} }
let(:bypass_header) { 'gitlab-bypass-rate-limiting' }
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 258b3d25aee..56c1284927d 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -336,6 +336,32 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
+ context 'when package registry rate limits are passed' do
+ let(:params) do
+ {
+ throttle_unauthenticated_packages_api_enabled: 1,
+ throttle_unauthenticated_packages_api_period_in_seconds: 500,
+ throttle_unauthenticated_packages_api_requests_per_period: 20,
+ throttle_authenticated_packages_api_enabled: 1,
+ throttle_authenticated_packages_api_period_in_seconds: 600,
+ throttle_authenticated_packages_api_requests_per_period: 10
+ }
+ end
+
+ it 'updates package registry throttle settings' do
+ subject.execute
+
+ application_settings.reload
+
+ expect(application_settings.throttle_unauthenticated_packages_api_enabled).to be_truthy
+ expect(application_settings.throttle_unauthenticated_packages_api_period_in_seconds).to eq(500)
+ expect(application_settings.throttle_unauthenticated_packages_api_requests_per_period).to eq(20)
+ expect(application_settings.throttle_authenticated_packages_api_enabled).to be_truthy
+ expect(application_settings.throttle_authenticated_packages_api_period_in_seconds).to eq(600)
+ expect(application_settings.throttle_authenticated_packages_api_requests_per_period).to eq(10)
+ end
+ end
+
context 'when issues_create_limit is passed' do
let(:params) do
{
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index df9a48d7b1c..151c2a1d014 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Git::WikiPushService, services: true do
process_changes do
write_new_page
update_page(wiki_page_a.title)
- delete_page(wiki_page_b.page.path)
+ delete_page(wiki_page_b.page)
end
end
@@ -198,7 +198,7 @@ RSpec.describe Git::WikiPushService, services: true do
context 'when a page we do not know about has been deleted' do
def run_service
wiki_page = create(:wiki_page, wiki: wiki)
- process_changes { delete_page(wiki_page.page.path) }
+ process_changes { delete_page(wiki_page.page) }
end
it 'create a new meta-data record' do
@@ -350,8 +350,8 @@ RSpec.describe Git::WikiPushService, services: true do
git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details)
end
- def delete_page(path)
- git_wiki.delete_page(path, commit_details)
+ def delete_page(page)
+ wiki.delete_page(page, 'commit message')
end
def commit_details
diff --git a/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb
new file mode 100644
index 00000000000..4385cd519be
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'update board list mutation' do
+ describe '#resolve' do
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+ let(:list_update_params) { { position: 1, collapsed: true } }
+
+ subject { mutation.resolve(list: list, **list_update_params) }
+
+ before_all do
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ list.update_preferences_for(reporter, collapsed: false)
+ end
+
+ context 'with permission to admin board lists' do
+ let(:current_user) { reporter }
+
+ it 'updates the list position and collapsed state as expected' do
+ subject
+
+ reloaded_list = list.reload
+ expect(reloaded_list.position).to eq(1)
+ expect(reloaded_list.collapsed?(current_user)).to eq(true)
+ end
+ end
+
+ context 'with permission to read board lists' do
+ let(:current_user) { guest }
+
+ it 'updates the list collapsed state but not the list position' do
+ subject
+
+ reloaded_list = list.reload
+ expect(reloaded_list.position).to eq(0)
+ expect(reloaded_list.collapsed?(current_user)).to eq(true)
+ end
+ end
+
+ context 'without permission to read board lists' do
+ let(:current_user) { create(:user) }
+
+ it 'raises Resource Not Found error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 6b243aef3e6..2498bf35a09 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -469,34 +469,30 @@ RSpec.shared_examples 'wiki model' do
end
describe '#delete_page' do
- shared_examples 'delete_page operations' do
- let(:page) { create(:wiki_page, wiki: wiki) }
+ let(:page) { create(:wiki_page, wiki: wiki) }
- it 'deletes the page' do
- subject.delete_page(page)
+ it 'deletes the page' do
+ subject.delete_page(page)
- expect(subject.list_pages.count).to eq(0)
- end
+ expect(subject.list_pages.count).to eq(0)
+ end
- it 'sets the correct commit email' do
- subject.delete_page(page)
+ it 'sets the correct commit email' do
+ subject.delete_page(page)
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
- it 'runs after_wiki_activity callbacks' do
- page
+ it 'runs after_wiki_activity callbacks' do
+ page
- expect(subject).to receive(:after_wiki_activity)
+ expect(subject).to receive(:after_wiki_activity)
- subject.delete_page(page)
- end
+ subject.delete_page(page)
end
- it_behaves_like 'delete_page operations'
-
context 'when an error is raised' do
it 'logs the error and returns false' do
page = build(:wiki_page, wiki: wiki)
@@ -509,14 +505,6 @@ RSpec.shared_examples 'wiki model' do
expect(subject.delete_page(page)).to be_falsey
end
end
-
- context 'when feature flag :gitaly_replace_wiki_delete_page is disabled' do
- before do
- stub_feature_flags(gitaly_replace_wiki_delete_page: false)
- end
-
- it_behaves_like 'delete_page operations'
- end
end
describe '#ensure_repository' do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb
new file mode 100644
index 00000000000..9b55b0f061f
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a GraphQL request to update board list' do
+ context 'the user is not allowed to read board lists' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ before do
+ list.update_preferences_for(current_user, collapsed: false)
+ end
+
+ context 'when user has permissions to admin board lists' do
+ before do
+ group.add_reporter(current_user)
+ end
+
+ it 'updates the list position and collapsed state' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list']).to include(
+ 'position' => 1,
+ 'collapsed' => true
+ )
+ end
+ end
+
+ context 'when user has permissions to read board lists' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it 'updates the list collapsed state but not the list position' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list']).to include(
+ 'position' => 0,
+ 'collapsed' => true
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 926da827e75..95817624658 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
# Requires let variables:
-# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths"
+# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api"
# * request_method
# * request_args
# * other_user_request_args
@@ -13,7 +13,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
{
"throttle_protected_paths" => "throttle_authenticated_protected_paths_api",
"throttle_authenticated_api" => "throttle_authenticated_api",
- "throttle_authenticated_web" => "throttle_authenticated_web"
+ "throttle_authenticated_web" => "throttle_authenticated_web",
+ "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api"
}
end
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index eb4faaed769..5eea3b4ab00 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -106,96 +106,102 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
end
- context 'with repository in cleanup scheduled state' do
- it_behaves_like 'handling all repository conditions'
- end
-
- context 'with repository in cleanup unfinished state' do
+ context 'with loopless disabled' do
before do
- repository.cleanup_unfinished!
+ stub_feature_flags(container_registry_expiration_policies_loopless: false)
end
- it_behaves_like 'handling all repository conditions'
- end
-
- context 'with another repository in cleanup unfinished state' do
- let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
+ context 'with repository in cleanup scheduled state' do
+ it_behaves_like 'handling all repository conditions'
+ end
- it 'process the cleanup scheduled repository first' do
- service_response = cleanup_service_response(repository: repository)
- expect(ContainerExpirationPolicies::CleanupService)
- .to receive(:new).with(repository).and_return(double(execute: service_response))
- expect_log_extra_metadata(service_response: service_response)
+ context 'with repository in cleanup unfinished state' do
+ before do
+ repository.cleanup_unfinished!
+ end
- subject
+ it_behaves_like 'handling all repository conditions'
end
- end
- context 'with multiple repositories in cleanup unfinished state' do
- let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
- let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
+ context 'with another repository in cleanup unfinished state' do
+ let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
- before do
- repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
+ it 'process the cleanup scheduled repository first' do
+ service_response = cleanup_service_response(repository: repository)
+ expect(ContainerExpirationPolicies::CleanupService)
+ .to receive(:new).with(repository).and_return(double(execute: service_response))
+ expect_log_extra_metadata(service_response: service_response)
+
+ subject
+ end
end
- it 'process the repository with the oldest expiration_policy_started_at' do
- service_response = cleanup_service_response(repository: repository)
- expect(ContainerExpirationPolicies::CleanupService)
- .to receive(:new).with(repository).and_return(double(execute: service_response))
- expect_log_extra_metadata(service_response: service_response)
+ context 'with multiple repositories in cleanup unfinished state' do
+ let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
+ let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
- subject
- end
- end
+ before do
+ repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
+ end
- context 'with repository in cleanup ongoing state' do
- before do
- repository.cleanup_ongoing!
+ it 'process the repository with the oldest expiration_policy_started_at' do
+ service_response = cleanup_service_response(repository: repository)
+ expect(ContainerExpirationPolicies::CleanupService)
+ .to receive(:new).with(repository).and_return(double(execute: service_response))
+ expect_log_extra_metadata(service_response: service_response)
+
+ subject
+ end
end
- it 'does not process it' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+ context 'with repository in cleanup ongoing state' do
+ before do
+ repository.cleanup_ongoing!
+ end
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- expect(repository.cleanup_ongoing?).to be_truthy
- end
- end
+ it 'does not process it' do
+ expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
- context 'with no repository in any cleanup state' do
- before do
- repository.cleanup_unscheduled!
+ expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
+ expect(repository.cleanup_ongoing?).to be_truthy
+ end
end
- it 'does not process it' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+ context 'with no repository in any cleanup state' do
+ before do
+ repository.cleanup_unscheduled!
+ end
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- expect(repository.cleanup_unscheduled?).to be_truthy
- end
- end
+ it 'does not process it' do
+ expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
- context 'with no container repository waiting' do
- before do
- repository.destroy!
+ expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
+ expect(repository.cleanup_unscheduled?).to be_truthy
+ end
end
- it 'does not execute the cleanup tags service' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+ context 'with no container repository waiting' do
+ before do
+ repository.destroy!
+ end
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- end
- end
+ it 'does not execute the cleanup tags service' do
+ expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_throttling: false)
+ expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
+ end
end
- it 'is a no-op' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_expiration_policies_throttling: false)
+ end
+
+ it 'is a no-op' do
+ expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
+ expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
+ end
end
end
@@ -230,37 +236,42 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
describe '#remaining_work_count' do
subject { worker.remaining_work_count }
- context 'with container repositoires waiting for cleanup' do
- let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
+ context 'with loopless disabled' do
+ before do
+ stub_feature_flags(container_registry_expiration_policies_loopless: false)
+ end
+ context 'with container repositoires waiting for cleanup' do
+ let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
- it { is_expected.to eq(3) }
+ it { is_expected.to eq(3) }
- it 'logs the work count' do
- expect_log_info(
- cleanup_scheduled_count: 1,
- cleanup_unfinished_count: 2,
- cleanup_total_count: 3
- )
+ it 'logs the work count' do
+ expect_log_info(
+ cleanup_scheduled_count: 1,
+ cleanup_unfinished_count: 2,
+ cleanup_total_count: 3
+ )
- subject
+ subject
+ end
end
- end
- context 'with no container repositories waiting for cleanup' do
- before do
- repository.cleanup_ongoing!
- end
+ context 'with no container repositories waiting for cleanup' do
+ before do
+ repository.cleanup_ongoing!
+ end
- it { is_expected.to eq(0) }
+ it { is_expected.to eq(0) }
- it 'logs 0 work count' do
- expect_log_info(
- cleanup_scheduled_count: 0,
- cleanup_unfinished_count: 0,
- cleanup_total_count: 0
- )
+ it 'logs 0 work count' do
+ expect_log_info(
+ cleanup_scheduled_count: 0,
+ cleanup_unfinished_count: 0,
+ cleanup_total_count: 0
+ )
- subject
+ subject
+ end
end
end
end
@@ -274,14 +285,20 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity)
end
- it { is_expected.to eq(capacity) }
-
- context 'with feature flag disabled' do
+ context 'with loopless disabled' do
before do
- stub_feature_flags(container_registry_expiration_policies_throttling: false)
+ stub_feature_flags(container_registry_expiration_policies_loopless: false)
end
- it { is_expected.to eq(0) }
+ it { is_expected.to eq(capacity) }
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_expiration_policies_throttling: false)
+ end
+
+ it { is_expected.to eq(0) }
+ end
end
end
diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb
index 2d5176e874d..e8f9a972f10 100644
--- a/spec/workers/container_expiration_policy_worker_spec.rb
+++ b/spec/workers/container_expiration_policy_worker_spec.rb
@@ -35,10 +35,16 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
context 'With no container expiration policies' do
- it 'does not execute any policies' do
- expect(ContainerRepository).not_to receive(:for_project_id)
+ context 'with loopless disabled' do
+ before do
+ stub_feature_flags(container_registry_expiration_policies_loopless: false)
+ end
- expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
+ it 'does not execute any policies' do
+ expect(ContainerRepository).not_to receive(:for_project_id)
+
+ expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
+ end
end
end