diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 15:09:35 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 15:09:35 +0000 |
commit | 9c6578ed4e0bc92cd838ef96d978df54403e9609 (patch) | |
tree | 5dff7ad20ae6402e4b7a5a44fe4e81ef04855cdf /spec | |
parent | 2af44d609eb8a1579169f9a350bc531d1081d77f (diff) | |
download | gitlab-ce-9c6578ed4e0bc92cd838ef96d978df54403e9609.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |