diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-01 15:09:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-01 15:09:50 +0300 |
commit | 02f6aecd47c847bb2a7d101678813fe5077ae299 (patch) | |
tree | 886df070fd7a3b3f0d364e879b89ea5eb31aeada /spec | |
parent | 5ffb2b7bcde1c76f939c5dca2ff65bac3404a88f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
13 files changed, 385 insertions, 145 deletions
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 57ae1d5a1db..9771141a955 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -41,21 +41,25 @@ RSpec.describe SearchController, feature_category: :global_search do describe 'rate limit scope' do it 'uses current_user and search scope' do %w[projects blobs users issues merge_requests].each do |scope| - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user, scope], users_allowlist: []) get :show, params: { search: 'hello', scope: scope } end end it 'uses just current_user when no search scope is used' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :show, params: { search: 'hello' } end it 'uses just current_user when search scope is abusive' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get(:show, params: { search: 'hello', scope: 'hack-the-mainframe' }) - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :show, params: { search: 'hello', scope: 'blobs' * 1000 } end end @@ -298,6 +302,14 @@ RSpec.describe SearchController, feature_category: :global_search do end end + it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do + let(:current_user) { user } + + def request + get(:show, params: { search: 'foo@bar.com', scope: 'users' }) + end + end + it 'increments the custom search sli apdex' do expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with( elapsed: a_kind_of(Numeric), @@ -370,16 +382,19 @@ RSpec.describe SearchController, feature_category: :global_search do describe 'rate limit scope' do it 'uses current_user and search scope' do %w[projects blobs users issues merge_requests].each do |scope| - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user, scope], users_allowlist: []) get :count, params: { search: 'hello', scope: scope } end end it 'uses just current_user when search scope is abusive' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :count, params: { search: 'hello', scope: 'hack-the-mainframe' } - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :count, params: { search: 'hello', scope: 'blobs' * 1000 } end end @@ -432,6 +447,14 @@ RSpec.describe SearchController, feature_category: :global_search do get(:count, params: { search: 'foo@bar.com', scope: 'users' }) end end + + it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do + let(:current_user) { user } + + def request + get(:count, params: { search: 'foo@bar.com', scope: 'users' }) + end + end end describe 'GET #autocomplete' do @@ -454,16 +477,19 @@ RSpec.describe SearchController, feature_category: :global_search do describe 'rate limit scope' do it 'uses current_user and search scope' do %w[projects blobs users issues merge_requests].each do |scope| - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user, scope], users_allowlist: []) get :autocomplete, params: { term: 'hello', scope: scope } end end it 'uses just current_user when search scope is abusive' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :autocomplete, params: { term: 'hello', scope: 'hack-the-mainframe' } - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user]) + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, + scope: [user], users_allowlist: []) get :autocomplete, params: { term: 'hello', scope: 'blobs' * 1000 } end end @@ -476,6 +502,14 @@ RSpec.describe SearchController, feature_category: :global_search do end end + it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do + let(:current_user) { user } + + def request + get(:autocomplete, params: { term: 'foo@bar.com', scope: 'users' }) + end + end + it 'can be filtered with params[:filter]' do get :autocomplete, params: { term: 'setting', filter: 'generic' } expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js index c3c7b2e6c65..b967fb18a39 100644 --- a/spec/frontend/super_sidebar/components/create_menu_spec.js +++ b/spec/frontend/super_sidebar/components/create_menu_spec.js @@ -1,7 +1,6 @@ import { nextTick } from 'vue'; import { GlDisclosureDropdown, - GlTooltip, GlDisclosureDropdownGroup, GlDisclosureDropdownItem, } from '@gitlab/ui'; @@ -9,6 +8,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import { __ } from '~/locale'; import CreateMenu from '~/super_sidebar/components/create_menu.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createNewMenuGroups } from '../mock_data'; describe('CreateMenu component', () => { @@ -18,7 +18,6 @@ describe('CreateMenu component', () => { const findGlDisclosureDropdownGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup); const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem); const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); - const findGlTooltip = () => wrapper.findComponent(GlTooltip); const createWrapper = ({ provide = {} } = {}) => { wrapper = shallowMountExtended(CreateMenu, { @@ -33,6 +32,9 @@ describe('CreateMenu component', () => { InviteMembersTrigger, GlDisclosureDropdown, }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, }); }; @@ -74,16 +76,12 @@ describe('CreateMenu component', () => { expect(findInviteMembersTrigger().exists()).toBe(true); }); - it("sets the toggle ID and tooltip's target", () => { - expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId); - expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`); - }); - it('hides the tooltip when the dropdown is opened', async () => { findGlDisclosureDropdown().vm.$emit('shown'); await nextTick(); - expect(findGlTooltip().exists()).toBe(false); + const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip'); + expect(tooltip.value).toBe(''); }); it('shows the tooltip when the dropdown is closed', async () => { @@ -91,7 +89,8 @@ describe('CreateMenu component', () => { findGlDisclosureDropdown().vm.$emit('hidden'); await nextTick(); - expect(findGlTooltip().exists()).toBe(true); + const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip'); + expect(tooltip.value).toBe('Create new...'); }); }); diff --git a/spec/frontend/super_sidebar/components/flyout_menu_spec.js b/spec/frontend/super_sidebar/components/flyout_menu_spec.js index b894d29c875..bf24de870d9 100644 --- a/spec/frontend/super_sidebar/components/flyout_menu_spec.js +++ b/spec/frontend/super_sidebar/components/flyout_menu_spec.js @@ -1,16 +1,26 @@ -import { shallowMount } from '@vue/test-utils'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue'; jest.mock('@floating-ui/dom'); describe('FlyoutMenu', () => { let wrapper; + let dummySection; const createComponent = () => { - wrapper = shallowMount(FlyoutMenu, { + dummySection = document.createElement('section'); + dummySection.addEventListener = jest.fn(); + + dummySection.getBoundingClientRect = jest.fn(); + dummySection.getBoundingClientRect.mockReturnValue({ top: 0, bottom: 5, width: 10 }); + + document.querySelector = jest.fn(); + document.querySelector.mockReturnValue(dummySection); + + wrapper = mountExtended(FlyoutMenu, { propsData: { targetId: 'section-1', - items: [], + items: [{ id: 1, title: 'item 1', link: 'https://example.com' }], }, }); }; diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js index 288e317d4c6..e76bb699301 100644 --- a/spec/frontend/super_sidebar/components/menu_section_spec.js +++ b/spec/frontend/super_sidebar/components/menu_section_spec.js @@ -79,39 +79,55 @@ describe('MenuSection component', () => { }); describe('when hasFlyout is true', () => { - it('is rendered', () => { + it('is not yet rendered', () => { createWrapper({ title: 'Asdf' }, { 'has-flyout': true }); - expect(findFlyout().exists()).toBe(true); + expect(findFlyout().exists()).toBe(false); }); describe('on mouse hover', () => { describe('when section is expanded', () => { - it('is not shown', async () => { + it('is not rendered', async () => { createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: true }); await findButton().trigger('pointerover', { pointerType: 'mouse' }); - expect(findFlyout().isVisible()).toBe(false); + expect(findFlyout().exists()).toBe(false); }); }); describe('when section is not expanded', () => { - it('is shown', async () => { - createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false }); - await findButton().trigger('pointerover', { pointerType: 'mouse' }); - expect(findFlyout().isVisible()).toBe(true); + describe('when section has no items', () => { + it('is not rendered', async () => { + createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false }); + await findButton().trigger('pointerover', { pointerType: 'mouse' }); + expect(findFlyout().exists()).toBe(false); + }); + }); + + describe('when section has items', () => { + it('is rendered and shown', async () => { + createWrapper( + { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] }, + { 'has-flyout': true, expanded: false }, + ); + await findButton().trigger('pointerover', { pointerType: 'mouse' }); + expect(findFlyout().isVisible()).toBe(true); + }); }); }); }); describe('when section gets closed', () => { beforeEach(async () => { - createWrapper({ title: 'Asdf' }, { expanded: true, 'has-flyout': true }); + createWrapper( + { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] }, + { expanded: true, 'has-flyout': true }, + ); await findButton().trigger('click'); await findButton().trigger('pointerover', { pointerType: 'mouse' }); }); it('shows the flyout only after section title gets hovered out and in again', async () => { expect(findCollapse().props('visible')).toBe(false); - expect(findFlyout().isVisible()).toBe(false); + expect(findFlyout().exists()).toBe(false); await findButton().trigger('pointerleave'); await findButton().trigger('pointerover', { pointerType: 'mouse' }); diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index 7734afb7416..b58b65f09f5 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -65,8 +65,20 @@ describe('UserBar component', () => { createWrapper(); }); - it('passes the "Create new..." menu groups to the create-menu component', () => { - expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups); + describe('"Create new..." menu', () => { + describe('when there are no menu items for it', () => { + // This scenario usually happens for an "External" user. + it('does not render it', () => { + createWrapper({ sidebarData: { ...mockSidebarData, create_new_menu_groups: [] } }); + expect(findCreateMenu().exists()).toBe(false); + }); + }); + + describe('when there are menu items for it', () => { + it('passes the "Create new..." menu groups to the create-menu component', () => { + expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups); + }); + }); }); it('passes the "Merge request" menu groups to the merge_request_menu component', () => { diff --git a/spec/migrations/20230802212443_add_current_user_todos_widget_to_epic_work_item_type_spec.rb b/spec/migrations/20230802212443_add_current_user_todos_widget_to_epic_work_item_type_spec.rb index 2fb4bd6b448..22a8f93b524 100644 --- a/spec/migrations/20230802212443_add_current_user_todos_widget_to_epic_work_item_type_spec.rb +++ b/spec/migrations/20230802212443_add_current_user_todos_widget_to_epic_work_item_type_spec.rb @@ -4,118 +4,22 @@ require 'spec_helper' require_migration! RSpec.describe AddCurrentUserTodosWidgetToEpicWorkItemType, :migration, feature_category: :team_planning do - include MigrationHelpers::WorkItemTypesHelper - - let(:work_item_types) { table(:work_item_types) } - let(:work_item_widget_definitions) { table(:work_item_widget_definitions) } - let(:base_types) do - { - issue: 0, - incident: 1, - test_case: 2, - requirement: 3, - task: 4, - objective: 5, - key_result: 6, - epic: 7 - } - end - - let(:epic_widgets) do - { - 'Assignees' => 0, - 'Description' => 1, - 'Hierarchy' => 2, - 'Labels' => 3, - 'Notes' => 5, - 'Start and due date' => 6, - 'Health status' => 7, - 'Status' => 11, - 'Notifications' => 14, - 'Award emoji' => 16 - }.freeze - end - - after(:all) do - # Make sure base types are recreated after running the migration - # because migration specs are not run in a transaction - reset_work_item_types - end - - before do - reset_db_state_prior_to_migration - end - - describe '#up' do - it 'adds current user todos widget to epic work item type', :aggregate_failures do - expect do - migrate! - end.to change { work_item_widget_definitions.count }.by(1) - - epic_type = work_item_types.find_by(namespace_id: nil, base_type: described_class::EPIC_ENUM_VALUE) - created_widget = work_item_widget_definitions.last - - expect(created_widget).to have_attributes( - widget_type: described_class::WIDGET_ENUM_VALUE, - name: described_class::WIDGET_NAME, - work_item_type_id: epic_type.id - ) - end - - context 'when epic type does not exist' do - it 'skips creating the new widget definition' do - work_item_types.where(namespace_id: nil, base_type: base_types[:epic]).delete_all - - expect do - migrate! - end.to not_change(work_item_widget_definitions, :count) - end - end - end - - describe '#down' do - it 'removes current user todos widget from epic work item type' do - migrate! - - expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1) - end - end - - def reset_db_state_prior_to_migration - # Database needs to be in a similar state as when this migration was created - work_item_types.delete_all - - create_work_item!('Issue', :issue, 'issue-type-issue') - create_work_item!('Incident', :incident, 'issue-type-incident') - create_work_item!('Test Case', :test_case, 'issue-type-test-case') - create_work_item!('Requirement', :requirement, 'issue-type-requirements') - create_work_item!('Task', :task, 'issue-type-task') - create_work_item!('Objective', :objective, 'issue-type-objective') - create_work_item!('Key Result', :key_result, 'issue-type-keyresult') - - epic_type = create_work_item!('Epic', :epic, 'issue-type-epic') - - widgets = epic_widgets.map do |widget_name, widget_enum_value| + it_behaves_like 'migration that adds a widget to a work item type' do + let(:target_type_enum_value) { described_class::EPIC_ENUM_VALUE } + let(:target_type) { :epic } + let(:widgets_for_type) do { - work_item_type_id: epic_type.id, - name: widget_name, - widget_type: widget_enum_value - } + 'Assignees' => 0, + 'Description' => 1, + 'Hierarchy' => 2, + 'Labels' => 3, + 'Notes' => 5, + 'Start and due date' => 6, + 'Health status' => 7, + 'Status' => 11, + 'Notifications' => 14, + 'Award emoji' => 16 + }.freeze end - - # Creating all widgets for the type so the state in the DB is as close as possible to the actual state - work_item_widget_definitions.upsert_all( - widgets, - unique_by: :index_work_item_widget_definitions_on_default_witype_and_name - ) - end - - def create_work_item!(type_name, base_type, icon_name) - work_item_types.create!( - name: type_name, - namespace_id: nil, - base_type: base_types[base_type], - icon_name: icon_name - ) end end diff --git a/spec/migrations/20230823140934_add_linked_items_widget_to_ticket_work_item_type_spec.rb b/spec/migrations/20230823140934_add_linked_items_widget_to_ticket_work_item_type_spec.rb new file mode 100644 index 00000000000..6a83b4b1a7c --- /dev/null +++ b/spec/migrations/20230823140934_add_linked_items_widget_to_ticket_work_item_type_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddLinkedItemsWidgetToTicketWorkItemType, :migration, feature_category: :portfolio_management do + it_behaves_like 'migration that adds a widget to a work item type' do + let(:target_type_enum_value) { described_class::TICKET_ENUM_VALUE } + let(:target_type) { :ticket } + let(:additional_types) { { ticket: 8 } } + let(:widgets_for_type) do + { + 'Assignees' => 0, + 'Description' => 1, + 'Hierarchy' => 2, + 'Labels' => 3, + 'Notes' => 5, + 'Iteration' => 9, + 'Milestone' => 4, + 'Weight' => 8, + 'Current user todos' => 15, + 'Start and due date' => 6, + 'Health status' => 7, + 'Notifications' => 14, + 'Award emoji' => 16 + }.freeze + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 6214e5f60f9..2e7bf4eaa52 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -241,6 +241,11 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do it { is_expected.not_to allow_value(nil).for(:users_get_by_id_limit_allowlist) } it { is_expected.to allow_value([]).for(:users_get_by_id_limit_allowlist) } + it { is_expected.to allow_value(many_usernames(100)).for(:search_rate_limit_allowlist) } + it { is_expected.not_to allow_value(many_usernames(101)).for(:search_rate_limit_allowlist) } + it { is_expected.not_to allow_value(nil).for(:search_rate_limit_allowlist) } + it { is_expected.to allow_value([]).for(:search_rate_limit_allowlist) } + it { is_expected.to allow_value('all_tiers').for(:whats_new_variant) } it { is_expected.to allow_value('current_tier').for(:whats_new_variant) } it { is_expected.to allow_value('disabled').for(:whats_new_variant) } diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 0feff90d088..6a57cf52466 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -473,6 +473,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' } end end + + context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do + before do + stub_application_setting(search_rate_limit: 1) + end + + it 'allows user whose username is in the allowlist' do + stub_application_setting(search_rate_limit_allowlist: [user.username]) + + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end end describe "GET /groups/:id/search" do @@ -658,6 +673,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' } end end + + context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do + before do + stub_application_setting(search_rate_limit: 1) + end + + it 'allows user whose username is in the allowlist' do + stub_application_setting(search_rate_limit_allowlist: [user.username]) + + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end end end @@ -1057,6 +1087,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' } end end + + context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do + before do + stub_application_setting(search_rate_limit: 1) + end + + it 'allows user whose username is in the allowlist' do + stub_application_setting(search_rate_limit_allowlist: [user.username]) + + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end end end end diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb index e17fb2afa16..732c75b4ed3 100644 --- a/spec/services/releases/destroy_service_spec.rb +++ b/spec/services/releases/destroy_service_spec.rb @@ -28,6 +28,26 @@ RSpec.describe Releases::DestroyService, feature_category: :release_orchestratio it 'returns the destroyed object' do is_expected.to include(status: :success, release: release) end + + context 'when the release is for a catalog resource' do + let!(:catalog_resource) { create(:catalog_resource, project: project, state: 'published') } + let!(:version) { create(:catalog_resource_version, catalog_resource: catalog_resource, release: release) } + + it 'does not update the catalog resources if there are still releases' do + second_release = create(:release, project: project, tag: 'v1.2.0') + create(:catalog_resource_version, catalog_resource: catalog_resource, release: second_release) + + subject + + expect(catalog_resource.reload.state).to eq('published') + end + + it 'updates the catalog resource if there are no more releases' do + subject + + expect(catalog_resource.reload.state).to eq('draft') + end + end end context 'when tag does not exist in the repository' do diff --git a/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb new file mode 100644 index 00000000000..aefcdc70082 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Requires a context containing: +# - user +# - params + +RSpec.shared_examples 'search request exceeding rate limit' do + include_examples 'rate limited endpoint', rate_limit_key: :search_rate_limit + + it 'allows user in allow-list to search without applying rate limit', :freeze_time, + :clean_gitlab_redis_rate_limiting do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1) + + stub_application_setting(search_rate_limit_allowlist: [current_user.username]) + + request + request + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb index fdb31fa5d9d..37c338a7712 100644 --- a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb +++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb @@ -32,3 +32,110 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do end end end + +# Shared examples for testing migration that adds a single widget to a work item type +# +# It expects the following variables +# - `target_type_enum_value`: Int, enum value for the target work item type, typically defined in the migration +# as a constant +# - `target_type`: Symbol, the target type's name +# - `additional_types`: Hash (optional), name of work item types and their corresponding enum value that are defined +# at the time the migration was created but are missing from `base_types`. +# - `widgets_for_type`: Hash, name of the widgets included in the target type with their corresponding enum value +RSpec.shared_examples 'migration that adds a widget to a work item type' do + include MigrationHelpers::WorkItemTypesHelper + + let(:work_item_types) { table(:work_item_types) } + let(:work_item_widget_definitions) { table(:work_item_widget_definitions) } + let(:additional_base_types) { try(:additional_types) || {} } + let(:base_types) do + { + issue: 0, + incident: 1, + test_case: 2, + requirement: 3, + task: 4, + objective: 5, + key_result: 6, + epic: 7 + }.merge!(additional_base_types) + end + + after(:all) do + # Make sure base types are recreated after running the migration + # because migration specs are not run in a transaction + reset_work_item_types + end + + before do + # Database needs to be in a similar state as when the migration was created + reset_db_state_prior_to_migration + end + + describe '#up' do + it "adds widget to work item type", :aggregate_failures do + expect do + migrate! + end.to change { work_item_widget_definitions.count }.by(1) + + work_item_type = work_item_types.find_by(namespace_id: nil, base_type: target_type_enum_value) + created_widget = work_item_widget_definitions.last + + expect(created_widget).to have_attributes( + widget_type: described_class::WIDGET_ENUM_VALUE, + name: described_class::WIDGET_NAME, + work_item_type_id: work_item_type.id + ) + end + + context 'when type does not exist' do + it 'skips creating the new widget definition' do + work_item_types.where(namespace_id: nil, base_type: base_types[target_type]).delete_all + + expect do + migrate! + end.to not_change(work_item_widget_definitions, :count) + end + end + end + + describe '#down' do + it "removes widget from work item type" do + migrate! + + expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1) + end + end + + def reset_db_state_prior_to_migration + work_item_types.delete_all + + base_types.each do |type_sym, type_enum| + create_work_item_type!(type_sym.to_s.titleize, type_enum) + end + + target_type_record = work_item_types.find_by_name(target_type.to_s.titleize) + + widgets = widgets_for_type.map do |widget_name_value, widget_enum_value| + { + work_item_type_id: target_type_record.id, + name: widget_name_value, + widget_type: widget_enum_value + } + end + + # Creating all widgets for the type so the state in the DB is as close as possible to the actual state + work_item_widget_definitions.upsert_all( + widgets, + unique_by: :index_work_item_widget_definitions_on_default_witype_and_name + ) + end + + def create_work_item_type!(type_name, type_enum_value) + work_item_types.create!( + name: type_name, + namespace_id: nil, + base_type: type_enum_value + ) + end +end diff --git a/spec/workers/ci/initialize_pipelines_iid_sequence_worker_spec.rb b/spec/workers/ci/initialize_pipelines_iid_sequence_worker_spec.rb new file mode 100644 index 00000000000..fcf57e21ac9 --- /dev/null +++ b/spec/workers/ci/initialize_pipelines_iid_sequence_worker_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::InitializePipelinesIidSequenceWorker, feature_category: :continuous_integration do + let_it_be_with_refind(:project) { create(:project) } + + let(:project_created_event) do + Projects::ProjectCreatedEvent.new( + data: { + project_id: project.id, + namespace_id: project.namespace_id, + root_namespace_id: project.root_namespace.id + }) + end + + it_behaves_like 'subscribes to event' do + let(:event) { project_created_event } + end + + it 'creates an internal_ids sequence for ci_pipelines' do + consume_event(subscriber: described_class, event: project_created_event) + + expect(project.internal_ids.ci_pipelines).to be_any + expect(project.internal_ids.ci_pipelines).to all be_persisted + end + + context 'when the internal_ids sequence is already initialized' do + before do + create_list(:ci_pipeline, 2, project: project) + end + + it 'does not reset the sequence' do + expect { consume_event(subscriber: described_class, event: project_created_event) } + .not_to change { project.internal_ids.ci_pipelines.pluck(:last_value) } + end + end +end |