Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-01 15:09:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-01 15:09:50 +0300
commit02f6aecd47c847bb2a7d101678813fe5077ae299 (patch)
tree886df070fd7a3b3f0d364e879b89ea5eb31aeada /spec
parent5ffb2b7bcde1c76f939c5dca2ff65bac3404a88f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/search_controller_spec.rb54
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js17
-rw-r--r--spec/frontend/super_sidebar/components/flyout_menu_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js36
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js16
-rw-r--r--spec/migrations/20230802212443_add_current_user_todos_widget_to_epic_work_item_type_spec.rb126
-rw-r--r--spec/migrations/20230823140934_add_linked_items_widget_to_ticket_work_item_type_spec.rb29
-rw-r--r--spec/models/application_setting_spec.rb5
-rw-r--r--spec/requests/api/search_spec.rb45
-rw-r--r--spec/services/releases/destroy_service_spec.rb20
-rw-r--r--spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb107
-rw-r--r--spec/workers/ci/initialize_pipelines_iid_sequence_worker_spec.rb38
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