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
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/components/pajamas_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/controllers/environments_controller_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/cascading_settings_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/features/container_registry_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb1471
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/n_plus_one_query_examples.rb (renamed from spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb)0
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb28
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/commit_signature_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb323
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb12
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/views/pagination_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb238
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/workers/idempotency_shared_examples.rb14
42 files changed, 2360 insertions, 395 deletions
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 15590fd10dc..0e6f6f12c3f 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'switches current board' do
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
wait_for_requests
@@ -66,7 +66,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'adds a list to the none default board' do
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
wait_for_requests
@@ -88,7 +88,7 @@ RSpec.shared_examples 'multiple issue boards' do
expect(page).to have_selector('.board', count: 3)
in_boards_switcher_dropdown do
- click_link board.name
+ click_button board.name
end
wait_for_requests
@@ -100,7 +100,7 @@ RSpec.shared_examples 'multiple issue boards' do
assert_boards_nav_active
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
assert_boards_nav_active
@@ -108,7 +108,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'switches current board back' do
in_boards_switcher_dropdown do
- click_link board.name
+ click_button board.name
end
wait_for_requests
diff --git a/spec/support/shared_examples/components/pajamas_shared_examples.rb b/spec/support/shared_examples/components/pajamas_shared_examples.rb
new file mode 100644
index 00000000000..5c0ad1a1bc9
--- /dev/null
+++ b/spec/support/shared_examples/components/pajamas_shared_examples.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'it renders help text' do
+ it 'renders help text' do
+ expect(rendered_component).to have_selector('[data-testid="pajamas-component-help-text"]', text: help_text)
+ end
+end
+
+RSpec.shared_examples 'it does not render help text' do
+ it 'does not render help text' do
+ expect(rendered_component).not_to have_selector('[data-testid="pajamas-component-help-text"]')
+ end
+end
diff --git a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
index a79b94209f3..c6e880635aa 100644
--- a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
@@ -65,20 +65,3 @@ RSpec.shared_examples 'failed response for #cancel_auto_stop' do
end
end
end
-
-RSpec.shared_examples 'avoids N+1 queries on environment detail page' do
- render_views
-
- before do
- create_deployment_with_associations(sequence: 0)
- end
-
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new { get :show, params: environment_params }
-
- create_deployment_with_associations(sequence: 1)
- create_deployment_with_associations(sequence: 2)
-
- expect { get :show, params: environment_params }.not_to exceed_query_limit(control.count).with_threshold(34)
- end
-end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 2ea98002de1..5faf462c23c 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -36,6 +36,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST personal_access_toke
expect(session[:"#{provider}_access_token"]).to eq(token)
expect(controller).to redirect_to(status_import_url)
end
+
+ it 'passes namespace_id param as query param if it was present' do
+ namespace_id = 5
+ status_import_url = public_send("status_import_#{provider}_url", { namespace_id: namespace_id })
+
+ allow_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
+ allow(client).to receive(:user).and_return(true)
+ end
+
+ post :personal_access_token, params: { personal_access_token: 'some-token', namespace_id: 5 }
+
+ expect(controller).to redirect_to(status_import_url)
+ end
end
RSpec.shared_examples 'a GitHub-ish import controller: GET new' do
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index 94c91556ea7..44f30c32472 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -2,6 +2,7 @@
RSpec.shared_examples 'hardware device for 2fa' do |device_type|
include Spec::Support::Helpers::Features::TwoFactorHelpers
+ include Spec::Support::Helpers::ModalHelpers
def register_device(device_type, **kwargs)
case device_type.downcase
@@ -18,7 +19,6 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
let(:user) { create(:user) }
before do
- stub_feature_flags(bootstrap_confirmation_modals: false)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
end
@@ -59,7 +59,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
expect(page).to have_content(first_device.name)
expect(page).to have_content(second_device.name)
- accept_confirm { click_on 'Delete', match: :first }
+ accept_gl_confirm(button_text: 'Delete') { click_on 'Delete', match: :first }
expect(page).to have_content('Successfully deleted')
expect(page.body).not_to have_content(first_device.name)
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index 215d9d3e5a8..c162ed36881 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -51,7 +51,7 @@ RSpec.shared_examples 'resource access tokens creation disallowed' do |error_mes
it 'does not show access token creation form' do
visit resource_settings_access_tokens_path
- expect(page).not_to have_selector('#new_resource_access_token')
+ expect(page).not_to have_selector('#js-new-access-token-form')
end
it 'shows access token creation disabled text' do
@@ -135,7 +135,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
- accept_confirm { click_on 'Revoke' }
+ accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
@@ -156,7 +156,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
- accept_confirm { click_on 'Revoke' }
+ accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
index 395f4fc54e0..cb80751ff49 100644
--- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
+++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
@@ -6,7 +6,8 @@ RSpec.shared_examples 'a cascading setting' do
visit group_path
page.within form_group_selector do
- find(setting_field_selector).check
+ enable_setting.call
+
find('[data-testid="enforce-for-all-subgroups-checkbox"]').check
end
diff --git a/spec/support/shared_examples/features/container_registry_shared_examples.rb b/spec/support/shared_examples/features/container_registry_shared_examples.rb
index 6aa7e6e6270..784f82fdda1 100644
--- a/spec/support/shared_examples/features/container_registry_shared_examples.rb
+++ b/spec/support/shared_examples/features/container_registry_shared_examples.rb
@@ -19,8 +19,7 @@ RSpec.shared_examples 'rejecting tags destruction for an importing repository on
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
- alert_body = find('.gl-alert-body')
- expect(alert_body).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
- expect(alert_body).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
+ expect(page).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
+ expect(page).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
end
end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index c93d8e3d511..591f7973454 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -21,6 +21,31 @@ RSpec.shared_examples 'edits content using the content editor' do
end
end
+ describe 'code block' do
+ before do
+ visit(profile_preferences_path)
+
+ find('.syntax-theme').choose('Dark')
+
+ wait_for_requests
+
+ page.go_back
+ refresh
+
+ click_button 'Edit rich text'
+ end
+
+ it 'applies theme classes to code blocks' do
+ expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
+
+ find(content_editor_testid).send_keys [:enter, :enter]
+ find(content_editor_testid).send_keys '```js ' # trigger input rule
+ find(content_editor_testid).send_keys 'var a = 0'
+
+ expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
+ end
+ end
+
describe 'code block bubble menu' do
it 'shows a code block bubble menu for a code block' do
find(content_editor_testid).send_keys [:enter, :enter]
@@ -51,4 +76,49 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)')
end
end
+
+ describe 'mermaid diagram' do
+ before do
+ find(content_editor_testid).send_keys [:enter, :enter]
+
+ find(content_editor_testid).send_keys '```mermaid '
+ find(content_editor_testid).send_keys ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
+ end
+
+ it 'renders and updates the diagram correctly in a sandboxed iframe' do
+ iframe = find(content_editor_testid).find('iframe')
+ expect(iframe['src']).to include('/-/sandbox/mermaid')
+
+ within_frame(iframe) do
+ expect(find('svg').text).to include('JohnDoe12')
+ expect(find('svg').text).to include('HelloWorld34')
+ end
+
+ expect(iframe['height'].to_i).to be > 100
+
+ find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56']
+
+ within_frame(iframe) do
+ page.has_content?('JaneDoe34')
+
+ expect(find('svg').text).to include('JaneDoe34')
+ expect(find('svg').text).to include('HelloWorld56')
+ end
+ end
+
+ it 'toggles the diagram when preview button is clicked' do
+ find('[data-testid="preview-diagram"]').click
+
+ expect(find(content_editor_testid)).not_to have_selector('iframe')
+
+ find('[data-testid="preview-diagram"]').click
+
+ iframe = find(content_editor_testid).find('iframe')
+
+ within_frame(iframe) do
+ expect(find('svg').text).to include('JohnDoe12')
+ expect(find('svg').text).to include('HelloWorld34')
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
deleted file mode 100644
index 1848b4fffd9..00000000000
--- a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'issuable user dropdown behaviors' do
- include FilteredSearchHelpers
-
- before do
- issuable # ensure we have at least one issuable
- sign_in(user_in_dropdown)
- end
-
- %w[author assignee].each do |dropdown|
- describe "#{dropdown} dropdown", :js do
- it 'only includes members of the project/group' do
- visit issuables_path
-
- filtered_search.set("#{dropdown}:=")
-
- expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
- expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index d9460c7b8f1..52f3fd60c07 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do
it 'has a registration token' do
click_on 'Click to reveal'
- expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token)
+ expect(page.find_field('token-value').value).to have_content(registration_token)
end
describe 'reset registration token' do
- let!(:old_registration_token) { find('[data-testid="token-value"] input').value }
+ let!(:old_registration_token) { find_field('token-value').value }
before do
click_on 'Reset registration token'
@@ -62,7 +62,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
end
end
-RSpec.shared_examples 'shows no runners' do
+RSpec.shared_examples 'shows no runners registered' do
it 'shows counts with 0' do
expect(page).to have_text "Online runners 0"
expect(page).to have_text "Offline runners 0"
@@ -70,13 +70,19 @@ RSpec.shared_examples 'shows no runners' do
end
it 'shows "no runners" message' do
- expect(page).to have_text 'No runners found'
+ expect(page).to have_text s_('Runners|Get started with runners')
+ end
+end
+
+RSpec.shared_examples 'shows no runners found' do
+ it 'shows "no runners" message' do
+ expect(page).to have_text s_('Runners|No results found')
end
end
RSpec.shared_examples 'shows runner in list' do
it 'does not show empty state' do
- expect(page).not_to have_content 'No runners found'
+ expect(page).not_to have_content s_('Runners|Get started with runners')
end
it 'shows runner row' do
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index af3ea0600a2..77334db6a36 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -109,9 +109,8 @@ RSpec.shared_examples 'issue boards sidebar' do
wait_for_requests
expect(page).to have_content(
- _('Only project members with at least' \
- ' Reporter role can view or be' \
- ' notified about this issue.')
+ _('Only project members with at least the Reporter role, the author, and assignees' \
+ ' can view or be notified about this issue.')
)
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
new file mode 100644
index 00000000000..622a88e8323
--- /dev/null
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -0,0 +1,1471 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'issues or work items finder' do |factory, execute_context|
+ describe '#execute' do
+ include_context execute_context
+
+ context 'scope: all' do
+ let(:scope) { 'all' }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+
+ context 'user does not have read permissions' do
+ let(:search_user) { user2 }
+
+ context 'when filtering by project id' do
+ let(:params) { { project_id: project1.id } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when filtering by group id' do
+ let(:params) { { group_id: group.id } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+ end
+
+ context 'assignee filtering' do
+ let(:issuables) { items }
+
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [item1, item2, item5] }
+ end
+
+ it_behaves_like 'assignee NOT ID filter' do
+ let(:params) { { not: { assignee_id: user.id } } }
+ let(:expected_issuables) { [item3, item4] }
+ end
+
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_id: [user.id, user2.id] } } }
+ let(:expected_issuables) { [item1, item2, item3, item5] }
+ end
+
+ context 'when assignee_id does not exist' do
+ it_behaves_like 'assignee NOT ID filter' do
+ let(:params) { { not: { assignee_id: -100 } } }
+ let(:expected_issuables) { [item1, item2, item3, item4, item5] }
+ end
+ end
+
+ context 'filter by username' do
+ let_it_be(:user3) { create(:user) }
+
+ before do
+ project2.add_developer(user3)
+ item2.assignees = [user2]
+ item3.assignees = [user3]
+ end
+
+ it_behaves_like 'assignee username filter' do
+ let(:params) { { assignee_username: [user2.username] } }
+ let(:expected_issuables) { [item2] }
+ end
+
+ it_behaves_like 'assignee NOT username filter' do
+ before do
+ item2.assignees = [user2]
+ end
+
+ let(:params) { { not: { assignee_username: [user.username, user2.username] } } }
+ let(:expected_issuables) { [item3, item4] }
+ end
+
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_username: [user2.username, user3.username] } } }
+ let(:expected_issuables) { [item2, item3] }
+ end
+
+ context 'when assignee_username does not exist' do
+ it_behaves_like 'assignee NOT username filter' do
+ before do
+ item2.assignees = [user2]
+ end
+
+ let(:params) { { not: { assignee_username: 'non_existent_username' } } }
+ let(:expected_issuables) { [item1, item2, item3, item4, item5] }
+ end
+ end
+ end
+
+ it_behaves_like 'no assignee filter' do
+ let_it_be(:user3) { create(:user) }
+ let(:expected_issuables) { [item4] }
+ end
+
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [item1, item2, item3, item5] }
+ end
+ end
+
+ context 'filtering by release' do
+ context 'when the release tag is none' do
+ let(:params) { { release_tag: 'none' } }
+
+ it 'returns items without releases' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'when the release tag exists' do
+ let(:params) { { project_id: project1.id, release_tag: release.tag } }
+
+ it 'returns the items associated with that release' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+ end
+
+ context 'filtering by projects' do
+ context 'when projects are passed in a list of ids' do
+ let(:params) { { projects: [project1.id] } }
+
+ it 'returns the item belonging to the projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when projects are passed in a subquery' do
+ let(:params) { { projects: Project.id_in(project1.id) } }
+
+ it 'returns the item belonging to the projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'filtering by group_id' do
+ let(:params) { { group_id: group.id } }
+
+ context 'when include_subgroup param not set' do
+ it 'returns all group items' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+
+ context 'when projects outside the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id] } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when projects of the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project1.id] } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when projects of the group are passed as a subquery' do
+ let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when release_tag is passed as a parameter' do
+ let(:params) { { group_id: group.id, release_tag: 'dne-release-tag' } }
+
+ it 'ignores the release_tag parameter' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'when include_subgroup param is true' do
+ before do
+ params[:include_subgroups] = true
+ end
+
+ it 'returns all group and subgroup items' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+
+ context 'when mixed projects are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item4)
+ end
+ end
+ end
+ end
+
+ context 'filtering by author' do
+ context 'by author ID' do
+ let(:params) { { author_id: user2.id } }
+
+ it 'returns items created by that user' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'using OR' do
+ let(:item6) { create(factory, project: project2) }
+ let(:params) { { or: { author_username: [item3.author.username, item6.author.username] } } }
+
+ it 'returns items created by any of the given users' do
+ expect(items).to contain_exactly(item3, item6)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(or_issuable_queries: false)
+ end
+
+ it 'does not add any filter' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5, item6)
+ end
+ end
+ end
+
+ context 'filtering by NOT author ID' do
+ let(:params) { { not: { author_id: user2.id } } }
+
+ it 'returns items not created by that user' do
+ expect(items).to contain_exactly(item1, item2, item4, item5)
+ end
+ end
+
+ context 'filtering by nonexistent author ID and issue term using CTE for search' do
+ let(:params) do
+ {
+ author_id: 'does-not-exist',
+ search: 'git',
+ attempt_group_search_optimizations: true
+ }
+ end
+
+ it 'returns no results' do
+ expect(items).to be_empty
+ end
+ end
+ end
+
+ context 'filtering by milestone' do
+ let(:params) { { milestone_title: milestone.title } }
+
+ it 'returns items assigned to that milestone' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by not milestone' do
+ let(:params) { { not: { milestone_title: milestone.title } } }
+
+ it 'returns items not assigned to that milestone' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'filtering by group milestone' do
+ let!(:group) { create(:group, :public) }
+ let(:group_milestone) { create(:milestone, group: group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+ let(:params) { { milestone_title: group_milestone.title } }
+
+ before do
+ project2.update!(namespace: group)
+ item2.update!(milestone: group_milestone)
+ item3.update!(milestone: group_milestone)
+ end
+
+ it 'returns items assigned to that group milestone' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: group_milestone.title } } }
+
+ it 'returns items not assigned to that group milestone' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by no milestone' do
+ let(:params) { { milestone_title: 'None' } }
+
+ it 'returns items with no milestone' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+
+ it 'returns items with no milestone (deprecated)' do
+ params[:milestone_title] = Milestone::None.title
+
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'filtering by any milestone' do
+ let(:params) { { milestone_title: 'Any' } }
+
+ it 'returns items with any assigned milestone' do
+ expect(items).to contain_exactly(item1)
+ end
+
+ it 'returns items with any assigned milestone (deprecated)' do
+ params[:milestone_title] = Milestone::Any.title
+
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by upcoming milestone' do
+ let(:params) { { milestone_title: Milestone::Upcoming.name } }
+
+ let!(:group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ let(:project_no_upcoming_milestones) { create(:project, :public) }
+ let(:project_next_1_1) { create(:project, :public) }
+ let(:project_next_8_8) { create(:project, :public) }
+ let(:project_in_group) { create(:project, :public, namespace: group) }
+
+ let(:yesterday) { Date.current - 1.day }
+ let(:tomorrow) { Date.current + 1.day }
+ let(:two_days_from_now) { Date.current + 2.days }
+ let(:ten_days_from_now) { Date.current + 10.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, :closed, project: project_no_upcoming_milestones),
+ create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
+ create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now),
+ create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday),
+ create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow),
+ create(:milestone, group: group, title: '9.9', due_date: tomorrow)
+ ]
+ end
+
+ let!(:created_items) do
+ milestones.map do |milestone|
+ create(factory, project: milestone.project || project_in_group,
+ milestone: milestone, author: user, assignees: [user])
+ end
+ end
+
+ it 'returns items in the upcoming milestone for each project or group' do
+ expect(items.map { |item| item.milestone.title })
+ .to contain_exactly('1.1', '8.8', '9.9')
+ expect(items.map { |item| item.milestone.due_date })
+ .to contain_exactly(tomorrow, two_days_from_now, tomorrow)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: Milestone::Upcoming.name } } }
+
+ it 'returns items not in upcoming milestones for each project or group, but must have a due date' do
+ target_items = created_items.select do |item|
+ item.milestone&.due_date && item.milestone.due_date <= Date.current
+ end
+
+ expect(items).to contain_exactly(*target_items)
+ end
+ end
+ end
+
+ context 'filtering by started milestone' do
+ let(:params) { { milestone_title: Milestone::Started.name } }
+
+ let(:project_no_started_milestones) { create(:project, :public) }
+ let(:project_started_1_and_2) { create(:project, :public) }
+ let(:project_started_8) { create(:project, :public) }
+
+ let(:yesterday) { Date.current - 1.day }
+ let(:tomorrow) { Date.current + 1.day }
+ let(:two_days_ago) { Date.current - 2.days }
+ let(:three_days_ago) { Date.current - 3.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
+ create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
+ create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
+ create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+ create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago),
+ create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago),
+ create(:milestone, project: project_started_8, title: '7.0'),
+ create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
+ create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
+ ]
+ end
+
+ before do
+ milestones.each do |milestone|
+ create(factory, project: milestone.project, milestone: milestone, author: user, assignees: [user])
+ end
+ end
+
+ it 'returns items in the started milestones for each project' do
+ expect(items.map { |item| item.milestone.title })
+ .to contain_exactly('1.0', '2.0', '8.0')
+ expect(items.map { |item| item.milestone.start_date })
+ .to contain_exactly(two_days_ago, yesterday, yesterday)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: Milestone::Started.name } } }
+
+ it 'returns items not in the started milestones for each project' do
+ target_items = items_model.where(milestone: Milestone.not_started)
+
+ expect(items).to contain_exactly(*target_items)
+ end
+ end
+ end
+
+ context 'filtering by label' do
+ let(:params) { { label_name: label.title } }
+
+ it 'returns items with that label' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: label.title } } }
+
+ it 'returns items that do not have that label' do
+ expect(items).to contain_exactly(item1, item3, item4, item5)
+ end
+
+ # IssuableFinder first filters using the outer params (the ones not inside the `not` key.)
+ # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param
+ # do not take precedence over the outer params with the same name.
+ context 'shadowing the same outside param' do
+ let(:params) { { label_name: label2.title, not: { label_name: label.title } } }
+
+ it 'does not take precedence over labels outside NOT' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'further filtering outside params' do
+ let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } }
+
+ it 'further filters on the returned resultset' do
+ expect(items).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'filtering by multiple labels' do
+ let(:params) { { label_name: [label.title, label2.title].join(',') } }
+ let(:label2) { create(:label, project: project2) }
+
+ before do
+ create(:label_link, label: label2, target: item2)
+ end
+
+ it 'returns the unique items with all those labels' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } }
+
+ it 'returns items that do not have any of the labels provided' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by a label that includes any or none in the title' do
+ let(:params) { { label_name: [label.title, label2.title].join(',') } }
+ let(:label) { create(:label, title: 'any foo', project: project2) }
+ let(:label2) { create(:label, title: 'bar none', project: project2) }
+
+ before do
+ create(:label_link, label: label2, target: item2)
+ end
+
+ it 'returns the unique items with all those labels' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } }
+
+ it 'returns items that do not have ANY ONE of the labels provided' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by no label' do
+ let(:params) { { label_name: described_class::Params::FILTER_NONE } }
+
+ it 'returns items with no labels' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+
+ context 'filtering by any label' do
+ let(:params) { { label_name: described_class::Params::FILTER_ANY } }
+
+ it 'returns items that have one or more label' do
+ create_list(:label_link, 2, label: create(:label, project: project2), target: item3)
+
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+
+ context 'when the same label exists on project and group levels' do
+ let(:item1) { create(factory, project: project1) }
+ let(:item2) { create(factory, project: project1) }
+
+ # Skipping validation to reproduce a "real-word" scenario.
+ # We still have legacy labels on PRD that have the same title on the group and project levels, example: `bug`
+ let(:project_label) do
+ build(:label, title: 'somelabel', project: project1).tap { |r| r.save!(validate: false) }
+ end
+
+ let(:group_label) { create(:group_label, title: 'somelabel', group: project1.group) }
+
+ let(:params) { { label_name: 'somelabel' } }
+
+ before do
+ create(:label_link, label: group_label, target: item1)
+ create(:label_link, label: project_label, target: item2)
+ end
+
+ it 'finds both item records' do
+ expect(items).to contain_exactly(item1, item2)
+ end
+ end
+
+ context 'filtering by item term' do
+ let(:params) { { search: search_term } }
+
+ let_it_be(:english) { create(factory, project: project1, title: 'title', description: 'something english') }
+
+ let_it_be(:japanese) do
+ create(factory, project: project1, title: '日本語 title', description: 'another english description')
+ end
+
+ context 'with latin search term' do
+ let(:search_term) { 'title english' }
+
+ it 'returns matching items' do
+ expect(items).to contain_exactly(english, japanese)
+ end
+ end
+
+ context 'with non-latin search term' do
+ let(:search_term) { '日本語' }
+
+ it 'returns matching items' do
+ expect(items).to contain_exactly(japanese)
+ end
+ end
+
+ context 'when full-text search is disabled' do
+ let(:search_term) { 'somet' }
+
+ before do
+ stub_feature_flags(issues_full_text_search: false)
+ end
+
+ it 'allows partial word matches' do
+ expect(items).to contain_exactly(english)
+ end
+ end
+
+ context 'with anonymous user' do
+ let_it_be(:public_project) { create(:project, :public, group: subgroup) }
+ let_it_be(:item6) { create(factory, project: public_project, title: 'tanuki') }
+ let_it_be(:item7) { create(factory, project: public_project, title: 'ikunat') }
+
+ let(:search_user) { nil }
+ let(:params) { { search: 'tanuki' } }
+
+ context 'with disable_anonymous_search feature flag enabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: true)
+ end
+
+ it 'does not perform search' do
+ expect(items).to contain_exactly(item6, item7)
+ end
+ end
+
+ context 'with disable_anonymous_search feature flag disabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: false)
+ end
+
+ it 'finds one public item' do
+ expect(items).to contain_exactly(item6)
+ end
+ end
+ end
+ end
+
+ context 'filtering by item term in title' do
+ let(:params) { { search: 'git', in: 'title' } }
+
+ it 'returns items with title match for search term' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by items iids' do
+ let(:params) { { iids: [item3.iid] } }
+
+ it 'returns items where iids match' do
+ expect(items).to contain_exactly(item3, item5)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { iids: [item3.iid] } } }
+
+ it 'returns items with no iids match' do
+ expect(items).to contain_exactly(item1, item2, item4)
+ end
+ end
+ end
+
+ context 'filtering by state' do
+ context 'with opened' do
+ let(:params) { { state: 'opened' } }
+
+ it 'returns only opened items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'with closed' do
+ let(:params) { { state: 'closed' } }
+
+ it 'returns only closed items' do
+ expect(items).to contain_exactly(closed_item)
+ end
+ end
+
+ context 'with all' do
+ let(:params) { { state: 'all' } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5)
+ end
+ end
+
+ context 'with invalid state' do
+ let(:params) { { state: 'invalid_state' } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by created_at' do
+ context 'through created_after' do
+ let(:params) { { created_after: item3.created_at } }
+
+ it 'returns items created on or after the given date' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'through created_before' do
+ let(:params) { { created_before: item1.created_at } }
+
+ it 'returns items created on or before the given date' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'through created_after and created_before' do
+ let(:params) { { created_after: item2.created_at, created_before: item3.created_at } }
+
+ it 'returns items created between the given dates' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+ end
+
+ context 'filtering by updated_at' do
+ context 'through updated_after' do
+ let(:params) { { updated_after: item3.updated_at } }
+
+ it 'returns items updated on or after the given date' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'through updated_before' do
+ let(:params) { { updated_before: item1.updated_at } }
+
+ it 'returns items updated on or before the given date' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'through updated_after and updated_before' do
+ let(:params) { { updated_after: item2.updated_at, updated_before: item3.updated_at } }
+
+ it 'returns items updated between the given dates' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+ end
+
+ context 'filtering by closed_at' do
+ let!(:closed_item1) { create(factory, project: project1, state: :closed, closed_at: 1.week.ago) }
+ let!(:closed_item2) { create(factory, project: project2, state: :closed, closed_at: 1.week.from_now) }
+ let!(:closed_item3) { create(factory, project: project2, state: :closed, closed_at: 2.weeks.from_now) }
+
+ context 'through closed_after' do
+ let(:params) { { state: :closed, closed_after: closed_item3.closed_at } }
+
+ it 'returns items closed on or after the given date' do
+ expect(items).to contain_exactly(closed_item3)
+ end
+ end
+
+ context 'through closed_before' do
+ let(:params) { { state: :closed, closed_before: closed_item1.closed_at } }
+
+ it 'returns items closed on or before the given date' do
+ expect(items).to contain_exactly(closed_item1)
+ end
+ end
+
+ context 'through closed_after and closed_before' do
+ let(:params) do
+ { state: :closed, closed_after: closed_item2.closed_at, closed_before: closed_item3.closed_at }
+ end
+
+ it 'returns items closed between the given dates' do
+ expect(items).to contain_exactly(closed_item2, closed_item3)
+ end
+ end
+ end
+
+ context 'filtering by reaction name' do
+ context 'user searches by no reaction' do
+ let(:params) { { my_reaction_emoji: 'None' } }
+
+ it 'returns items that the user did not react to' do
+ expect(items).to contain_exactly(item2, item4, item5)
+ end
+ end
+
+ context 'user searches by any reaction' do
+ let(:params) { { my_reaction_emoji: 'Any' } }
+
+ it 'returns items that the user reacted to' do
+ expect(items).to contain_exactly(item1, item3)
+ end
+ end
+
+ context 'user searches by "thumbsup" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns items that the user thumbsup to' do
+ expect(items).to contain_exactly(item1)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } }
+
+ it 'returns items that the user did not thumbsup to' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+ end
+
+ context 'user2 searches by "thumbsup" reaction' do
+ let(:search_user) { user2 }
+
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns items that the user2 thumbsup to' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } }
+
+ it 'returns items that the user2 thumbsup to' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+ end
+
+ context 'user searches by "thumbsdown" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsdown' } }
+
+ it 'returns items that the user thumbsdown to' do
+ expect(items).to contain_exactly(item3)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsdown' } } }
+
+ it 'returns items that the user thumbsdown to' do
+ expect(items).to contain_exactly(item1, item2, item4, item5)
+ end
+ end
+ end
+ end
+
+ context 'filtering by confidential' do
+ let_it_be(:confidential_item) { create(factory, project: project1, confidential: true) }
+
+ context 'no filtering' do
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5, confidential_item)
+ end
+ end
+
+ context 'user filters confidential items' do
+ let(:params) { { confidential: true } }
+
+ it 'returns only confidential items' do
+ expect(items).to contain_exactly(confidential_item)
+ end
+ end
+
+ context 'user filters only public items' do
+ let(:params) { { confidential: false } }
+
+ it 'returns only public items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by item type' do
+ let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) }
+
+ context 'no type given' do
+ let(:params) { { issue_types: [] } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'incident type' do
+ let(:params) { { issue_types: ['incident'] } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(incident_item)
+ end
+ end
+
+ context 'item type' do
+ let(:params) { { issue_types: ['issue'] } }
+
+ it 'returns all items with type issue' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'multiple params' do
+ let(:params) { { issue_types: %w(issue incident) } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'without array' do
+ let(:params) { { issue_types: 'incident' } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(incident_item)
+ end
+ end
+
+ context 'invalid params' do
+ let(:params) { { issue_types: ['nonsense'] } }
+
+ it 'returns no items' do
+ expect(items).to eq(items_model.none)
+ end
+ end
+ end
+
+ context 'filtering by crm contact' do
+ let_it_be(:contact1) { create(:contact, group: group) }
+ let_it_be(:contact2) { create(:contact, group: group) }
+
+ let_it_be(:contact1_item1) { create(factory, project: project1) }
+ let_it_be(:contact1_item2) { create(factory, project: project1) }
+ let_it_be(:contact2_item1) { create(factory, project: project1) }
+
+ let(:params) { { crm_contact_id: contact1.id } }
+
+ it 'returns for that contact' do
+ create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2)
+
+ expect(items).to contain_exactly(contact1_item1, contact1_item2)
+ end
+ end
+
+ context 'filtering by crm organization' do
+ let_it_be(:organization) { create(:organization, group: group) }
+ let_it_be(:contact1) { create(:contact, group: group, organization: organization) }
+ let_it_be(:contact2) { create(:contact, group: group, organization: organization) }
+
+ let_it_be(:contact1_item1) { create(factory, project: project1) }
+ let_it_be(:contact1_item2) { create(factory, project: project1) }
+ let_it_be(:contact2_item1) { create(factory, project: project1) }
+
+ let(:params) { { crm_organization_id: organization.id } }
+
+ it 'returns for that contact' do
+ create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2)
+
+ expect(items).to contain_exactly(contact1_item1, contact1_item2, contact2_item1)
+ end
+ end
+
+ context 'when the user is unauthorized' do
+ let(:search_user) { nil }
+
+ it 'returns no results' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when the user can see some, but not all, items' do
+ let(:search_user) { user2 }
+
+ it 'returns only items they can see' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+
+ it 'finds items user can access due to group' do
+ group = create(:group)
+ project = create(:project, group: group)
+ item = create(factory, project: project)
+ group.add_user(user, :owner)
+
+ expect(items).to include(item)
+ end
+ end
+
+ context 'personal scope' do
+ let(:scope) { 'assigned_to_me' }
+
+ it 'returns item assigned to the user' do
+ expect(items).to contain_exactly(item1, item2, item5)
+ end
+
+ context 'filtering by project' do
+ let(:params) { { project_id: project1.id } }
+
+ it 'returns items assigned to the user in that project' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'when project restricts items' do
+ let(:scope) { nil }
+
+ it "doesn't return team-only items to non team members" do
+ project = create(:project, :public, :issues_private)
+ item = create(factory, project: project)
+
+ expect(items).not_to include(item)
+ end
+
+ it "doesn't return items if feature disabled" do
+ [project1, project2, project3].each do |project|
+ project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+ end
+
+ expect(items.count).to eq 0
+ end
+ end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(factory, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
+
+ context 'filtering by due date' do
+ let_it_be(:item_due_today) { create(factory, project: project1, due_date: Date.current) }
+ let_it_be(:item_due_tomorrow) { create(factory, project: project1, due_date: 1.day.from_now) }
+ let_it_be(:item_overdue) { create(factory, project: project1, due_date: 2.days.ago) }
+ let_it_be(:item_due_soon) { create(factory, project: project1, due_date: 2.days.from_now) }
+
+ let(:scope) { 'all' }
+ let(:base_params) { { project_id: project1.id } }
+
+ context 'with param set to no due date' do
+ let(:params) { base_params.merge(due_date: items_model::NoDueDate.name) }
+
+ it 'returns items with no due date' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'with param set to any due date' do
+ let(:params) { base_params.merge(due_date: items_model::AnyDueDate.name) }
+
+ it 'returns items with any due date' do
+ expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon)
+ end
+ end
+
+ context 'with param set to due today' do
+ let(:params) { base_params.merge(due_date: items_model::DueToday.name) }
+
+ it 'returns items due today' do
+ expect(items).to contain_exactly(item_due_today)
+ end
+ end
+
+ context 'with param set to due tomorrow' do
+ let(:params) { base_params.merge(due_date: items_model::DueTomorrow.name) }
+
+ it 'returns items due today' do
+ expect(items).to contain_exactly(item_due_tomorrow)
+ end
+ end
+
+ context 'with param set to overdue' do
+ let(:params) { base_params.merge(due_date: items_model::Overdue.name) }
+
+ it 'returns overdue items' do
+ expect(items).to contain_exactly(item_overdue)
+ end
+ end
+
+ context 'with param set to next month and previous two weeks' do
+ let(:params) { base_params.merge(due_date: items_model::DueNextMonthAndPreviousTwoWeeks.name) }
+
+ it 'returns items due in the previous two weeks and next month' do
+ expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon)
+ end
+ end
+
+ context 'with invalid param' do
+ let(:params) { base_params.merge(due_date: 'foo') }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#row_count', :request_store do
+ let_it_be(:admin) { create(:admin) }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns the number of rows for the default state' do
+ finder = described_class.new(admin)
+
+ expect(finder.row_count).to eq(5)
+ end
+
+ it 'returns the number of rows for a given state' do
+ finder = described_class.new(admin, state: 'closed')
+
+ expect(finder.row_count).to be_zero
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'returns no rows' do
+ finder = described_class.new(admin)
+
+ expect(finder.row_count).to be_zero
+ end
+ end
+
+ it 'returns -1 if the query times out' do
+ finder = described_class.new(admin)
+
+ expect_next_instance_of(described_class) do |subfinder|
+ expect(subfinder).to receive(:execute).and_raise(ActiveRecord::QueryCanceled)
+ end
+
+ expect(finder.row_count).to eq(-1)
+ end
+ end
+
+ describe '#with_confidentiality_access_check' do
+ let(:guest) { create(:user) }
+
+ let_it_be(:authorized_user) { create(:user) }
+ let_it_be(:banned_user) { create(:user, :banned) }
+ let_it_be(:project) { create(:project, namespace: authorized_user.namespace) }
+ let_it_be(:public_item) { create(factory, project: project) }
+ let_it_be(:confidential_item) { create(factory, project: project, confidential: true) }
+ let_it_be(:hidden_item) { create(factory, project: project, author: banned_user) }
+
+ shared_examples 'returns public, does not return hidden or confidential' do
+ it 'returns only public items' do
+ expect(subject).to include(public_item)
+ expect(subject).not_to include(confidential_item, hidden_item)
+ end
+ end
+
+ shared_examples 'returns public and confidential, does not return hidden' do
+ it 'returns only public and confidential items' do
+ expect(subject).to include(public_item, confidential_item)
+ expect(subject).not_to include(hidden_item)
+ end
+ end
+
+ shared_examples 'returns public and hidden, does not return confidential' do
+ it 'returns only public and hidden items' do
+ expect(subject).to include(public_item, hidden_item)
+ expect(subject).not_to include(confidential_item)
+ end
+ end
+
+ shared_examples 'returns public, confidential, and hidden' do
+ it 'returns all items' do
+ expect(subject).to include(public_item, confidential_item, hidden_item)
+ end
+ end
+
+ context 'when no project filter is given' do
+ let(:params) { {} }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a project member with access to view confidential items' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public and confidential, does not return hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+ end
+
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
+
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'returns public, confidential, and hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+ end
+ end
+
+ context 'when searching within a specific project' do
+ let(:params) { { project_id: project.id } }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+ subject
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+
+ context 'for a project member with access to view confidential items' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public and confidential, does not return hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
+
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'returns public, confidential, and hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+ end
+ end
+ end
+
+ describe '#use_cte_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the force_cte param is falsey' do
+ let(:params) { { search: '日本語' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when a non-simple sort is given' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'popularity' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ context "uses group search optimization" do
+ let(:params) { { search: '日本語', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context "uses project search optimization" do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context 'with simple sort' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'updated_desc' } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context 'with simple sort as a symbol' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: :updated_desc } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+ end
+ end
+
+ describe '#parent_param=' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:finder) { described_class.new(nil) }
+
+ subject { finder.parent_param = obj }
+
+ where(:klass, :param) do
+ :Project | :project_id
+ :Group | :group_id
+ end
+
+ with_them do
+ let(:obj) { Object.const_get(klass, false).new }
+
+ it 'sets the params' do
+ subject
+
+ expect(finder.params[param]).to eq(obj)
+ end
+ end
+
+ context 'unexpected parent' do
+ let(:obj) { MergeRequest.new }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Unexpected parent: MergeRequest')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index 8e9e22f4359..110706c730b 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -39,7 +39,8 @@ RSpec.shared_examples 'querying members with a group' do
let(:base_args) { { relations: described_class.arguments['relations'].default_value } }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: user_4 })
+ resolve(described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: user_4 }, arg_style: :internal)
end
describe '#resolve' do
@@ -73,7 +74,8 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:other_user) { create(:user) }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user })
+ resolve(described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: other_user }, arg_style: :internal)
end
it 'generates an error' do
diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
index b989dbc6524..cd591248ff6 100644
--- a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
@@ -21,6 +21,7 @@ RSpec.shared_examples 'creating an incident timeline event' do
expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at)
expect(timeline_event.incident).to eq(expected_timeline_event.incident)
expect(timeline_event.author).to eq(expected_timeline_event.author)
+ expect(timeline_event.editable).to eq(expected_timeline_event.editable)
end
end
diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
index 21260e4d954..dfb8ce64391 100644
--- a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns an array of errors' do
- expect(result).to match(
+ expect(result).to include(
branch: be_nil,
success_path: be_nil,
errors: match_array([error_message])
@@ -92,7 +92,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns a success path' do
- expect(result).to match(
+ expect(result).to include(
branch: branch,
success_path: success_path,
errors: []
@@ -108,7 +108,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns an array of errors' do
- expect(result).to match(
+ expect(result).to include(
branch: be_nil,
success_path: be_nil,
errors: match_array([error])
diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
index 738edd43c92..738edd43c92 100644
--- a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb
+++ b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
index da8562161e7..3017f62a7c9 100644
--- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
@@ -24,7 +24,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
create(:maven_package, name: 'baz', project: project, created_at: 1.minute.ago, version: nil)
end
- [:created_desc, :name_desc, :version_desc, :type_asc].each do |order|
+ %w[CREATED_DESC NAME_DESC VERSION_DESC TYPE_ASC].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
@@ -32,7 +32,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
end
- [:created_asc, :name_asc, :version_asc, :type_desc].each do |order|
+ %w[CREATED_ASC NAME_ASC VERSION_ASC TYPE_DESC].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
@@ -41,25 +41,25 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
context 'filter by package_name' do
- let(:args) { { package_name: 'bar', sort: :created_desc } }
+ let(:args) { { package_name: 'bar', sort: 'CREATED_DESC' } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by package_type' do
- let(:args) { { package_type: 'conan', sort: :created_desc } }
+ let(:args) { { package_type: 'conan', sort: 'CREATED_DESC' } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by status' do
- let(:args) { { status: 'error', sort: :created_desc } }
+ let(:args) { { status: 'error', sort: 'CREATED_DESC' } }
it { is_expected.to eq([maven_package]) }
end
context 'include_versionless' do
- let(:args) { { include_versionless: true, sort: :created_desc } }
+ let(:args) { { include_versionless: true, sort: 'CREATED_DESC' } }
it { is_expected.to include(repository3) }
end
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index cf9c36fafe8..7fd54408b11 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -53,18 +53,20 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'adds information about the replacement if provided' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed, replacement: 'Foo.bar' })
- expect(deprecable.deprecation_reason).to include 'Please use `Foo.bar`'
+ expect(deprecable.deprecation_reason).to include('Please use `Foo.bar`')
end
it 'supports named reasons: renamed' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed })
- expect(deprecable.deprecation_reason).to include 'This was renamed.'
+ expect(deprecable.deprecation_reason).to eq('This was renamed. Deprecated in 1.10.')
end
it 'supports named reasons: alpha' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha })
- expect(deprecable.deprecation_reason).to include 'This feature is in Alpha'
+ expect(deprecable.deprecation_reason).to eq(
+ 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.'
+ )
end
end
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index d8a46180796..dfe5a071f91 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -20,10 +20,18 @@ RSpec.shared_examples 'integration settings form' do
"#{integration.title} field #{field_name} not present"
end
+ sections = integration.sections
events = parse_json(trigger_events_for_integration(integration))
+
events.each do |trigger|
- expect(page).to have_field(trigger[:title], type: 'checkbox', wait: 0),
- "#{integration.title} field #{title} checkbox not present"
+ trigger_title = if sections.any? { |s| s[:type] == 'trigger' }
+ trigger_event_title(trigger[:name])
+ else
+ trigger[:title]
+ end
+
+ expect(page).to have_field(trigger_title, type: 'checkbox', wait: 0),
+ "#{integration.title} field #{trigger_title} checkbox not present"
end
end
end
@@ -35,4 +43,20 @@ RSpec.shared_examples 'integration settings form' do
def parse_json(json)
Gitlab::Json.parse(json, symbolize_names: true)
end
+
+ def trigger_event_title(name)
+ # Should match `integrationTriggerEventTitles` in app/assets/javascripts/integrations/constants.js
+ event_titles = {
+ push_events: s_('IntegrationEvents|A push is made to the repository'),
+ issues_events: s_('IntegrationEvents|IntegrationEvents|An issue is created, updated, or closed'),
+ confidential_issues_events: s_('IntegrationEvents|A confidential issue is created, updated, or closed'),
+ merge_requests_events: s_('IntegrationEvents|A merge request is created, updated, or merged'),
+ note_events: s_('IntegrationEvents|A comment is added on an issue'),
+ confidential_note_events: s_('IntegrationEvents|A comment is added on a confidential issue'),
+ tag_push_events: s_('IntegrationEvents|A tag is pushed to the repository'),
+ pipeline_events: s_('IntegrationEvents|A pipeline status changes'),
+ wiki_page_events: s_('IntegrationEvents|A wiki page is created or updated')
+ }.with_indifferent_access
+ event_titles[name]
+ end
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index e886ec65b02..284c129221b 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -834,8 +834,8 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
end
end
- describe '#live_trace_exist?' do
- subject { trace.live_trace_exist? }
+ describe '#live?' do
+ subject { trace.live? }
context 'when trace does not exist' do
it { is_expected.to be_falsy }
diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
index 67d739b79ab..d14216ec5ff 100644
--- a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
@@ -22,7 +22,7 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
end
end
- def validate_connections!
+ def validate_connections_stack!
model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class|
[model_class, Gitlab::Database.db_config_name(model_class.connection)]
end
diff --git a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
index 4fc15cacab4..db2f2f2d0f0 100644
--- a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
@@ -11,6 +11,8 @@ RSpec.shared_examples 'subscribes to event' do
::Gitlab::EventStore.publish(event)
end
+
+ it_behaves_like 'an idempotent worker'
end
def consume_event(subscriber:, event:)
diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
new file mode 100644
index 00000000000..a5e4df1c272
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
+ context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores => true)
+ end
+
+ it 'multi store is enabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_and_secondary_stores?).to be true
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores => false)
+ end
+
+ it 'multi store is disabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_and_secondary_stores?).to be false
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_store_as_default} is enabled" do
+ before do
+ stub_feature_flags(use_primary_store_as_default => true)
+ end
+
+ it 'primary store is enabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_store_as_default?).to be true
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_store_as_default} is disabled" do
+ before do
+ stub_feature_flags(use_primary_store_as_default => false)
+ end
+
+ it 'primary store is disabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_store_as_default?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 74ec6474e80..6e7d04d3cba 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -238,8 +238,16 @@ RSpec.shared_examples 'application settings examples' do
end
describe '#allowed_key_types' do
- it 'includes all key types by default' do
- expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types)
+ context 'in non-FIPS mode', fips_mode: false do
+ it 'includes all key types by default' do
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types)
+ end
+ end
+
+ context 'in FIPS mode', :fips_mode do
+ it 'excludes DSA from supported key types' do
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i(dsa))
+ end
end
it 'excludes disabled key types' do
diff --git a/spec/support/shared_examples/models/commit_signature_shared_examples.rb b/spec/support/shared_examples/models/commit_signature_shared_examples.rb
new file mode 100644
index 00000000000..56d5c1da3af
--- /dev/null
+++ b/spec/support/shared_examples/models/commit_signature_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'commit signature' do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).required }
+ end
+
+ describe 'validation' do
+ subject { described_class.new }
+
+ it { is_expected.to validate_presence_of(:commit_sha) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ end
+
+ describe '.safe_create!' do
+ it 'finds a signature by commit sha if it existed' do
+ signature
+
+ expect(described_class.safe_create!(commit_sha: commit_sha)).to eq(signature)
+ end
+
+ it 'creates a new signature if it was not found' do
+ expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
+ end
+
+ it 'assigns the correct attributes when creating' do
+ signature = described_class.safe_create!(attributes)
+
+ expect(signature).to have_attributes(attributes)
+ end
+
+ it 'does not raise an error in case of a race condition' do
+ expect(described_class).to receive(:find_by).and_return(nil, instance_double(described_class, persisted?: true))
+
+ expect(described_class).to receive(:create).and_raise(ActiveRecord::RecordNotUnique)
+ allow(described_class).to receive(:create).and_call_original
+
+ described_class.safe_create!(attributes)
+ end
+ end
+
+ describe '#commit' do
+ it 'fetches the commit through the project' do
+ expect_next_instance_of(Project) do |instance|
+ expect(instance).to receive(:commit).with(commit_sha).and_return(commit)
+ end
+
+ signature.commit
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
index 0ff0895b861..3d393e6dcb5 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -1,6 +1,30 @@
# frozen_string_literal: true
RSpec.shared_examples 'includes Limitable concern' do
+ describe '#exceeds_limits?' do
+ let(:plan_limits) { create(:plan_limits, :default_plan) }
+
+ context 'without plan limits configured' do
+ it { expect(subject.exceeds_limits?).to eq false }
+ end
+
+ context 'without plan limits configured' do
+ before do
+ plan_limits.update!(subject.class.limit_name => 1)
+ end
+
+ it { expect(subject.exceeds_limits?).to eq false }
+
+ context 'with an existing model' do
+ before do
+ subject.clone.save!
+ end
+
+ it { expect(subject.exceeds_limits?).to eq true }
+ end
+ end
+ end
+
describe 'validations' do
let(:plan_limits) { create(:plan_limits, :default_plan) }
diff --git a/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb
new file mode 100644
index 00000000000..211beb5b32f
--- /dev/null
+++ b/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples Integrations::BaseDataFields do
+ subject(:model) { described_class.new }
+
+ describe 'associations' do
+ it { is_expected.to belong_to :integration }
+ end
+
+ describe '#activated?' do
+ subject(:activated?) { model.activated? }
+
+ context 'with integration' do
+ let(:integration) { instance_spy(Integration, activated?: activated) }
+
+ before do
+ allow(model).to receive(:integration).and_return(integration)
+ end
+
+ context 'with value set to false' do
+ let(:activated) { false }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with value set to true' do
+ let(:activated) { true }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context 'without integration' do
+ before do
+ allow(model).to receive(:integration).and_return(nil)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#to_database_hash' do
+ it 'does not include certain attributes' do
+ hash = model.to_database_hash
+
+ expect(hash.keys).not_to include('id', 'service_id', 'integration_id', 'created_at', 'updated_at')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index e293d10964b..75fff11cecd 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -80,7 +80,7 @@ RSpec.shared_examples_for "member creation" do
let_it_be(:admin) { create(:admin) }
it 'returns a Member object', :aggregate_failures do
- member = described_class.new(source, user, :maintainer).execute
+ member = described_class.add_user(source, user, :maintainer)
expect(member).to be_a member_type
expect(member).to be_persisted
@@ -99,7 +99,7 @@ RSpec.shared_examples_for "member creation" do
end
it 'does not update the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ member = described_class.add_user(source, project_bot, :maintainer, current_user: user)
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
@@ -110,7 +110,7 @@ RSpec.shared_examples_for "member creation" do
context 'when project_bot is not already a member' do
it 'adds the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ member = described_class.add_user(source, project_bot, :maintainer, current_user: user)
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
@@ -120,7 +120,7 @@ RSpec.shared_examples_for "member creation" do
context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
it 'sets members.created_by to the given admin current_user' do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member).to be_persisted
expect(source.users.reload).to include(user)
@@ -130,7 +130,7 @@ RSpec.shared_examples_for "member creation" do
context 'when admin mode is disabled' do
it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member).not_to be_persisted
expect(source.users.reload).not_to include(user)
@@ -139,7 +139,7 @@ RSpec.shared_examples_for "member creation" do
end
it 'sets members.expires_at to the given expires_at' do
- member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
+ member = described_class.add_user(source, user, :maintainer, expires_at: Date.new(2016, 9, 22))
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
@@ -148,7 +148,7 @@ RSpec.shared_examples_for "member creation" do
it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
expect(source.users).not_to include(user)
- member = described_class.new(source, user.id, sym_key).execute
+ member = described_class.add_user(source, user.id, sym_key)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
@@ -157,7 +157,7 @@ RSpec.shared_examples_for "member creation" do
it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
expect(source.users).not_to include(user)
- member = described_class.new(source, user.id, int_access_level).execute
+ member = described_class.add_user(source, user.id, int_access_level)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
@@ -169,7 +169,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user.id, :maintainer).execute
+ described_class.add_user(source, user.id, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -179,7 +179,7 @@ RSpec.shared_examples_for "member creation" do
it 'does not add the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, non_existing_record_id, :maintainer).execute
+ described_class.add_user(source, non_existing_record_id, :maintainer)
expect(source.users.reload).not_to include(user)
end
@@ -189,7 +189,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.add_user(source, user, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -200,12 +200,12 @@ RSpec.shared_examples_for "member creation" do
source.request_access(user)
end
- it 'adds the requester as a member', :aggregate_failures do
+ it 'does not add the requester as a regular member', :aggregate_failures do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
expect do
- described_class.new(source, user, :maintainer).execute
+ described_class.add_user(source, user, :maintainer)
end.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
@@ -217,7 +217,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user.email, :maintainer).execute
+ described_class.add_user(source, user.email, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -227,7 +227,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates an invited member' do
expect(source.users).not_to include(user)
- described_class.new(source, 'user@example.com', :maintainer).execute
+ described_class.add_user(source, 'user@example.com', :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
@@ -237,7 +237,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates an invited member', :aggregate_failures do
email_starting_with_number = "#{user.id}_email@example.com"
- described_class.new(source, email_starting_with_number, :maintainer).execute
+ described_class.add_user(source, email_starting_with_number, :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
expect(source.users.reload).not_to include(user)
@@ -249,7 +249,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates the member' do
expect(source.users).not_to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
end
@@ -263,7 +263,7 @@ RSpec.shared_examples_for "member creation" do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
@@ -275,7 +275,7 @@ RSpec.shared_examples_for "member creation" do
it 'does not create the member', :aggregate_failures do
expect(source.users).not_to include(user)
- member = described_class.new(source, user, :maintainer, current_user: user).execute
+ member = described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
@@ -290,7 +290,7 @@ RSpec.shared_examples_for "member creation" do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
@@ -307,7 +307,7 @@ RSpec.shared_examples_for "member creation" do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.add_user(source, user, :maintainer)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
@@ -317,7 +317,7 @@ RSpec.shared_examples_for "member creation" do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
@@ -327,221 +327,194 @@ RSpec.shared_examples_for "member creation" do
it 'does not update the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
+end
- context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
- let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
-
- it 'creates a member_task with the correct attributes', :aggregate_failures do
- described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
+RSpec.shared_examples_for "bulk member creation" do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
- member = source.members.last
+ context 'when current user does not have permission' do
+ it 'does not succeed' do
+ # maintainers cannot add owners
+ source.add_maintainer(user)
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
- expect(member.member_task.project).to eq(task_project)
+ expect(described_class.add_users(source, [user1, user2], :owner, current_user: user)).to be_empty
end
+ end
- context 'with an already existing member' do
- before do
- source.add_user(user, :developer)
- end
-
- it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
- member = source.members.find_by(user_id: user.id)
- create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
-
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.not_to change(MemberTask, :count)
+ it 'returns Member objects' do
+ members = described_class.add_users(source, [user1, user2], :maintainer)
- member.reset
- expect(member.tasks_to_be_done).to match_array([:code, :ci])
- expect(member.member_task.project).to eq(task_project)
- end
+ expect(members.map(&:user)).to contain_exactly(user1, user2)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
- it 'adds tasks to be done if they do not exist', :aggregate_failures do
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.to change(MemberTask, :count).by(1)
+ it 'returns an empty array' do
+ members = described_class.add_users(source, [], :maintainer)
- member = source.members.find_by(user_id: user.id)
- expect(member.tasks_to_be_done).to match_array([:issues])
- expect(member.member_task.project).to eq(task_project)
- end
- end
+ expect(members).to be_a Array
+ expect(members).to be_empty
end
-end
-RSpec.shared_examples_for "bulk member creation" do
- let_it_be(:user) { create(:user) }
- let_it_be(:admin) { create(:admin) }
+ it 'supports different formats' do
+ list = ['joe@local.test', admin, user1.id, user2.id.to_s]
- describe '#execute' do
- it 'raises an error when exiting_members is not passed in the args hash' do
- expect do
- described_class.new(source, user, :maintainer, current_user: user).execute
- end.to raise_error(ArgumentError, 'existing_members must be included in the args hash')
- end
- end
+ members = described_class.add_users(source, list, :maintainer)
- describe '.add_users', :aggregate_failures do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
+ expect(members.size).to eq(4)
+ expect(members.first).to be_invite
+ end
- it 'returns a Member objects' do
- members = described_class.add_users(source, [user1, user2], :maintainer)
+ context 'with de-duplication' do
+ it 'has the same user by id and user' do
+ members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
expect(members.map(&:user)).to contain_exactly(user1, user2)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
end
- it 'returns an empty array' do
- members = described_class.add_users(source, [], :maintainer)
+ it 'has the same user sent more than once' do
+ members = described_class.add_users(source, [user1, user1], :maintainer)
- expect(members).to be_a Array
- expect(members).to be_empty
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
end
+ end
- it 'supports different formats' do
- list = ['joe@local.test', admin, user1.id, user2.id.to_s]
+ it 'with the same user sent more than once by user and by email' do
+ members = described_class.add_users(source, [user1, user1.email], :maintainer)
- members = described_class.add_users(source, list, :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
- expect(members.size).to eq(4)
- expect(members.first).to be_invite
- end
+ it 'with the same user sent more than once by user id and by email' do
+ members = described_class.add_users(source, [user1.id, user1.email], :maintainer)
- context 'with de-duplication' do
- it 'has the same user by id and user' do
- members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+
+ context 'when a member already exists' do
+ before do
+ source.add_user(user1, :developer)
+ end
+ it 'has the same user sent more than once with the member already existing' do
+ expect do
+ members = described_class.add_users(source, [user1, user1, user2], :maintainer)
expect(members.map(&:user)).to contain_exactly(user1, user2)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
- end
+ end.to change { Member.count }.by(1)
+ end
- it 'has the same user sent more than once' do
- members = described_class.add_users(source, [user1, user1], :maintainer)
+ it 'supports existing users as expected with user_ids passed' do
+ user3 = create(:user)
- expect(members.map(&:user)).to contain_exactly(user1)
+ expect do
+ members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
- end
+ end.to change { Member.count }.by(2)
end
- it 'with the same user sent more than once by user and by email' do
- members = described_class.add_users(source, [user1, user1.email], :maintainer)
+ it 'supports existing users as expected without user ids passed' do
+ user3 = create(:user)
- expect(members.map(&:user)).to contain_exactly(user1)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
+ expect do
+ members = described_class.add_users(source, [user1, user2, user3], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end.to change { Member.count }.by(2)
end
+ end
- it 'with the same user sent more than once by user id and by email' do
- members = described_class.add_users(source, [user1.id, user1.email], :maintainer)
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
- expect(members.map(&:user)).to contain_exactly(user1)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
+ member = members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
end
- context 'when a member already exists' do
+ context 'with an already existing member' do
before do
source.add_user(user1, :developer)
end
- it 'has the same user sent more than once with the member already existing' do
- expect do
- members = described_class.add_users(source, [user1, user1, user2], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(1)
- end
-
- it 'supports existing users as expected with user_ids passed' do
- user3 = create(:user)
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user1.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
expect do
- members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(2)
- end
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.not_to change(MemberTask, :count)
- it 'supports existing users as expected without user ids passed' do
- user3 = create(:user)
+ member.reset
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project).to eq(task_project)
+ end
+ it 'adds tasks to be done if they do not exist', :aggregate_failures do
expect do
- members = described_class.add_users(source, [user1, user2, user3], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(2)
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user1.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
end
end
+ end
+end
+
+RSpec.shared_examples 'owner management' do
+ describe '.cannot_manage_owners?' do
+ subject { described_class.cannot_manage_owners?(source, user) }
- context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
- let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
+ context 'when maintainer' do
+ before do
+ source.add_maintainer(user)
+ end
- it 'creates a member_task with the correct attributes', :aggregate_failures do
- members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
- member = members.last
+ it 'cannot manage owners' do
+ expect(subject).to be_truthy
+ end
+ end
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
- expect(member.member_task.project).to eq(task_project)
+ context 'when owner' do
+ before do
+ source.add_owner(user)
end
- context 'with an already existing member' do
- before do
- source.add_user(user1, :developer)
- end
-
- it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
- member = source.members.find_by(user_id: user1.id)
- create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
-
- expect do
- described_class.add_users(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
- end.not_to change(MemberTask, :count)
-
- member.reset
- expect(member.tasks_to_be_done).to match_array([:code, :ci])
- expect(member.member_task.project).to eq(task_project)
- end
-
- it 'adds tasks to be done if they do not exist', :aggregate_failures do
- expect do
- described_class.add_users(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
- end.to change(MemberTask, :count).by(1)
-
- member = source.members.find_by(user_id: user1.id)
- expect(member.tasks_to_be_done).to match_array([:issues])
- expect(member.member_task.project).to eq(task_project)
- end
+ it 'can manage owners' do
+ expect(subject).to be_falsey
end
end
end
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index 04af3935d15..75eed0203a7 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -33,6 +33,18 @@ RSpec.shared_examples 'members notifications' do |entity_type|
end
end
+ describe '#after_commit' do
+ context 'on creation of a member requesting access' do
+ let(:member) { build(:"#{entity_type}_member", :access_request) }
+
+ it "calls NotificationService.new_access_request" do
+ expect(notification_service).to receive(:new_access_request).with(member)
+
+ member.save!
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:"#{entity_type}_member", :access_request) }
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 6f17231a040..604c57768fe 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -540,14 +540,6 @@ RSpec.shared_examples 'wiki model' do
end
end
end
-
- context 'when feature flag :gitaly_replace_wiki_create_page is disabled' do
- before do
- stub_feature_flags(gitaly_replace_wiki_create_page: false)
- end
-
- it_behaves_like 'create_page tests'
- end
end
describe '#update_page' do
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index f1ace9878e9..45da1d382c1 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -238,6 +238,12 @@ RSpec.shared_examples 'namespace traversal scopes' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants(include_self: false) }
it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) }
+
+ context 'with duplicate descendants' do
+ subject { described_class.where(id: [group_1, nested_group_1]).self_and_descendants(include_self: false) }
+
+ it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1) }
+ end
end
context 'with offset and limit' do
@@ -267,6 +273,14 @@ RSpec.shared_examples 'namespace traversal scopes' do
include_examples '.self_and_descendants'
end
+
+ context 'with linear_scopes_superset feature flag disabled' do
+ before do
+ stub_feature_flags(linear_scopes_superset: false)
+ end
+
+ include_examples '.self_and_descendants'
+ end
end
shared_examples '.self_and_descendant_ids' do
@@ -310,6 +324,14 @@ RSpec.shared_examples 'namespace traversal scopes' do
include_examples '.self_and_descendant_ids'
end
+
+ context 'with linear_scopes_superset feature flag disabled' do
+ before do
+ stub_feature_flags(linear_scopes_superset: false)
+ end
+
+ include_examples '.self_and_descendant_ids'
+ end
end
shared_examples '.self_and_hierarchy' do
diff --git a/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb b/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb
new file mode 100644
index 00000000000..7c3f4781472
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'preventing request because of ongoing project stats refresh' do |entrypoint|
+ before do
+ create(:project_build_artifacts_size_refresh, :pending, project: project)
+ end
+
+ it 'logs about the rejected request' do
+ expect(Gitlab::ProjectStatsRefreshConflictsLogger)
+ .to receive(:warn_request_rejected_during_stats_refresh)
+ .with(project.id)
+
+ make_request
+ end
+
+ it 'returns 409 error' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:conflict)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index aff086d1ba3..795545e4ad1 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -124,6 +124,23 @@ RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member
end
end
+RSpec.shared_examples 'PyPI package index' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it 'returns the package index' do
+ subject
+
+ expect(response.body).to match(package.name)
+ end
+
+ it_behaves_like 'returning response status', status
+ end
+end
+
RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
@@ -259,6 +276,45 @@ RSpec.shared_examples 'pypi simple API endpoint' do
end
end
+RSpec.shared_examples 'pypi simple index API endpoint' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'PyPI package index' | :success
+ :public | :guest | true | true | 'PyPI package index' | :success
+ :public | :developer | true | false | 'PyPI package index' | :success
+ :public | :guest | true | false | 'PyPI package index' | :success
+ :public | :developer | false | true | 'PyPI package index' | :success
+ :public | :guest | false | true | 'PyPI package index' | :success
+ :public | :developer | false | false | 'PyPI package index' | :success
+ :public | :guest | false | false | 'PyPI package index' | :success
+ :public | :anonymous | false | true | 'PyPI package index' | :success
+ :private | :developer | true | true | 'PyPI package index' | :success
+ :private | :guest | true | true | 'process PyPI api request' | :forbidden
+ :private | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :private | :developer | false | true | 'process PyPI api request' | :not_found
+ :private | :guest | false | true | 'process PyPI api request' | :not_found
+ :private | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :private | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+end
+
RSpec.shared_examples 'pypi file download endpoint' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb b/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb
new file mode 100644
index 00000000000..31218b104bd
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'avoids N+1 queries on environment detail page' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
+ create_deployment_with_associations(commit_depth: 19)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get project_environment_path(project, environment), params: environment_params
+ end
+
+ 18.downto(0).each { |n| create_deployment_with_associations(commit_depth: n) }
+
+ # N+1s exist for loading commit emails and users
+ expect do
+ get project_environment_path(project, environment), params: environment_params
+ end.not_to exceed_all_query_limit(control).with_threshold(9)
+ end
+end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index e1baa594f3c..6d59943d91c 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -8,9 +8,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
create_environment_with_associations(project)
- # Fix N+1 queries introduced by multi stop_actions for environment.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
- relax_count = 14
+ # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
+ relax_count = 1
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count)
end
@@ -23,9 +22,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
create_environment_with_associations(project)
- # Fix N+1 queries introduced by multi stop_actions for environment.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
- relax_count = 14
+ # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
+ relax_count = 1
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count)
end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 23aee912d2d..f644f1a1687 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -32,7 +32,7 @@ RSpec.shared_context 'incident management settings enabled' do
end
before do
- allow(ProjectServiceWorker).to receive(:perform_async)
+ allow(Integrations::ExecuteWorker).to receive(:perform_async)
allow(service)
.to receive(:incident_management_setting)
.and_return(incident_management_setting)
diff --git a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
index 9a3a0cc9cc8..ed05a150f8b 100644
--- a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
@@ -3,17 +3,17 @@
RSpec.shared_examples 'items list service' do
it 'avoids N+1' do
params = { board_id: board.id }
- control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute }
+ control = ActiveRecord::QueryRecorder.new { list_service(params).execute }
new_list
- expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control)
+ expect { list_service(params).execute }.not_to exceed_query_limit(control)
end
- it 'returns opened items when list_id is missing' do
+ it 'returns opened items when list_id and list are missing' do
params = { board_id: board.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(backlog_items)
end
@@ -21,7 +21,7 @@ RSpec.shared_examples 'items list service' do
it 'returns opened items when listing items from Backlog' do
params = { board_id: board.id, id: backlog.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(backlog_items)
end
@@ -29,7 +29,7 @@ RSpec.shared_examples 'items list service' do
it 'returns opened items that have label list applied when listing items from a label list' do
params = { board_id: board.id, id: list1.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(list1_items)
end
@@ -37,20 +37,24 @@ RSpec.shared_examples 'items list service' do
it 'returns closed items when listing items from Closed sorted by closed_at in descending order' do
params = { board_id: board.id, id: closed.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to eq(closed_items)
end
it 'raises an error if the list does not belong to the board' do
list = create(list_factory) # rubocop:disable Rails/SaveBang
- service = described_class.new(parent, user, board_id: board.id, id: list.id)
+ params = { board_id: board.id, id: list.id }
+
+ service = list_service(params)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'raises an error if list id is invalid' do
- service = described_class.new(parent, user, board_id: board.id, id: nil)
+ it 'raises an error if list and list id are invalid or missing' do
+ params = { board_id: board.id, id: nil, list: nil }
+
+ service = list_service(params)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -58,8 +62,22 @@ RSpec.shared_examples 'items list service' do
it 'returns items from all lists if :all_list is used' do
params = { board_id: board.id, all_lists: true }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(all_items)
end
+
+ it 'returns opened items that have label list applied when using list param' do
+ params = { board_id: board.id, list: list1 }
+
+ items = list_service(params).execute
+
+ expect(items).to match_array(list1_items)
+ end
+
+ def list_service(params)
+ args = [parent, user].push(params)
+
+ described_class.new(*args)
+ end
end
diff --git a/spec/support/shared_examples/views/pagination_shared_examples.rb b/spec/support/shared_examples/views/pagination_shared_examples.rb
new file mode 100644
index 00000000000..3932f320859
--- /dev/null
+++ b/spec/support/shared_examples/views/pagination_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'correct pagination' do
+ it 'paginates correctly to page 3 and back' do
+ expect(page).to have_selector(item_selector, count: per_page)
+ page1_item_text = page.find(item_selector).text
+ click_next_page(next_button_selector)
+
+ expect(page).to have_selector(item_selector, count: per_page)
+ page2_item_text = page.find(item_selector).text
+ click_next_page(next_button_selector)
+
+ expect(page).to have_selector(item_selector, count: per_page)
+ page3_item_text = page.find(item_selector).text
+ click_prev_page(prev_button_selector)
+
+ expect(page3_item_text).not_to eql(page2_item_text)
+ expect(page.find(item_selector).text).to eql(page2_item_text)
+
+ click_prev_page(prev_button_selector)
+
+ expect(page.find(item_selector).text).to eql(page1_item_text)
+ expect(page).to have_selector(item_selector, count: per_page)
+ end
+
+ def click_next_page(next_button_selector)
+ page.find(next_button_selector).click
+ wait_for_requests
+ end
+
+ def click_prev_page(prev_button_selector)
+ page.find(prev_button_selector).click
+ wait_for_requests
+ end
+end
diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
index 7fdf049a823..8ecb04bfdd6 100644
--- a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
@@ -42,159 +42,195 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database|
describe '#perform' do
let(:worker) { described_class.new }
- before do
- allow(worker).to receive(:jid).and_return(1)
- allow(worker).to receive(:always_perform?).and_return(false)
+ context 'when execute_background_migrations feature flag is disabled' do
+ before do
+ stub_feature_flags(execute_background_migrations: false)
+ end
- allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false)
- end
+ it 'does not perform the job, reschedules it in the future, and logs a message' do
+ expect(worker).not_to receive(:perform_with_connection)
- it 'performs jobs using the coordinator for the worker' do
- expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ expect(Sidekiq.logger).to receive(:info) do |payload|
+ expect(payload[:class]).to eq(described_class.name)
+ expect(payload[:database]).to eq(tracking_database)
+ expect(payload[:message]).to match(/skipping execution, migration rescheduled/)
+ end
- expect(coordinator.worker_class).to eq(described_class)
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- end
+ lease_attempts = 3
+ delay = described_class::BACKGROUND_MIGRATIONS_DELAY
+ job_args = [10, 20]
- worker.perform('Foo', [10, 20])
- end
+ freeze_time do
+ worker.perform('Foo', job_args, lease_attempts)
- context 'when lease can be obtained' do
- let(:coordinator) { double('job coordinator') }
+ job = described_class.jobs.find { |job| job['args'] == ['Foo', job_args, lease_attempts] }
+ expect(job).to be, "Expected the job to be rescheduled with (#{job_args}, #{lease_attempts}), but it was not."
+ expected_time = delay.to_i + Time.now.to_i
+ expect(job['at']).to eq(expected_time),
+ "Expected the job to be rescheduled in #{expected_time} seconds, " \
+ "but it was rescheduled in #{job['at']} seconds."
+ end
+ end
+ end
+
+ context 'when execute_background_migrations feature flag is enabled' do
before do
- allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with(tracking_database)
- .and_return(coordinator)
+ stub_feature_flags(execute_background_migrations: true)
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ allow(worker).to receive(:jid).and_return(1)
+ allow(worker).to receive(:always_perform?).and_return(false)
+
+ allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false)
end
- it 'sets up the shared connection before checking replication' do
- expect(coordinator).to receive(:with_shared_connection).and_yield.ordered
- expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered
+ it 'performs jobs using the coordinator for the worker' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
+ allow(coordinator).to receive(:with_shared_connection).and_yield
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ expect(coordinator.worker_class).to eq(described_class)
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ end
worker.perform('Foo', [10, 20])
end
- it 'performs a background migration' do
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ context 'when lease can be obtained' do
+ let(:coordinator) { double('job coordinator') }
- worker.perform('Foo', [10, 20])
- end
+ before do
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with(tracking_database)
+ .and_return(coordinator)
+
+ allow(coordinator).to receive(:with_shared_connection).and_yield
+ end
+
+ it 'sets up the shared connection before checking replication' do
+ expect(coordinator).to receive(:with_shared_connection).and_yield.ordered
+ expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered
- context 'when lease_attempts is 1' do
- it 'performs a background migration' do
expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- worker.perform('Foo', [10, 20], 1)
+ worker.perform('Foo', [10, 20])
end
- end
- it 'can run scheduled job and retried job concurrently' do
- expect(coordinator)
- .to receive(:perform)
- .with('Foo', [10, 20])
- .exactly(2).time
-
- worker.perform('Foo', [10, 20])
- worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1)
- end
+ it 'performs a background migration' do
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- it 'sets the class that will be executed as the caller_id' do
- expect(coordinator).to receive(:perform) do
- expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo')
+ worker.perform('Foo', [10, 20])
end
- worker.perform('Foo', [10, 20])
- end
- end
+ context 'when lease_attempts is 1' do
+ it 'performs a background migration' do
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- context 'when lease not obtained (migration of same class was performed recently)' do
- let(:timeout) { described_class.minimum_interval }
- let(:lease_key) { "#{described_class.name}:Foo" }
- let(:coordinator) { double('job coordinator') }
+ worker.perform('Foo', [10, 20], 1)
+ end
+ end
- before do
- allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with(tracking_database)
- .and_return(coordinator)
+ it 'can run scheduled job and retried job concurrently' do
+ expect(coordinator)
+ .to receive(:perform)
+ .with('Foo', [10, 20])
+ .exactly(2).time
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ worker.perform('Foo', [10, 20])
+ worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1)
+ end
- expect(coordinator).not_to receive(:perform)
+ it 'sets the class that will be executed as the caller_id' do
+ expect(coordinator).to receive(:perform) do
+ expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo')
+ end
- Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain
+ worker.perform('Foo', [10, 20])
+ end
end
- it 'reschedules the migration and decrements the lease_attempts' do
- expect(described_class)
- .to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
+ context 'when lease not obtained (migration of same class was performed recently)' do
+ let(:timeout) { described_class.minimum_interval }
+ let(:lease_key) { "#{described_class.name}:Foo" }
+ let(:coordinator) { double('job coordinator') }
- worker.perform('Foo', [10, 20], 5)
- end
+ before do
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with(tracking_database)
+ .and_return(coordinator)
- context 'when lease_attempts is 1' do
- let(:lease_key) { "#{described_class.name}:Foo:retried" }
+ allow(coordinator).to receive(:with_shared_connection).and_yield
+
+ expect(coordinator).not_to receive(:perform)
+
+ Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain
+ end
it 'reschedules the migration and decrements the lease_attempts' do
expect(described_class)
.to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 0)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
- worker.perform('Foo', [10, 20], 1)
+ worker.perform('Foo', [10, 20], 5)
end
- end
- context 'when lease_attempts is 0' do
- let(:lease_key) { "#{described_class.name}:Foo:retried" }
+ context 'when lease_attempts is 1' do
+ let(:lease_key) { "#{described_class.name}:Foo:retried" }
- it 'gives up performing the migration' do
- expect(described_class).not_to receive(:perform_in)
- expect(Sidekiq.logger).to receive(:warn).with(
- class: 'Foo',
- message: 'Job could not get an exclusive lease after several tries. Giving up.',
- job_id: 1)
+ it 'reschedules the migration and decrements the lease_attempts' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 0)
- worker.perform('Foo', [10, 20], 0)
+ worker.perform('Foo', [10, 20], 1)
+ end
end
- end
- end
- context 'when database is not healthy' do
- before do
- expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true)
- end
+ context 'when lease_attempts is 0' do
+ let(:lease_key) { "#{described_class.name}:Foo:retried" }
- it 'reschedules a migration if the database is not healthy' do
- expect(described_class)
- .to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
+ it 'gives up performing the migration' do
+ expect(described_class).not_to receive(:perform_in)
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: 'Foo',
+ message: 'Job could not get an exclusive lease after several tries. Giving up.',
+ job_id: 1)
- worker.perform('Foo', [10, 20])
+ worker.perform('Foo', [10, 20], 0)
+ end
+ end
end
- it 'increments the unhealthy counter' do
- counter = Gitlab::Metrics.counter(:background_migration_database_health_reschedules, 'msg')
+ context 'when database is not healthy' do
+ before do
+ expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true)
+ end
- expect(described_class).to receive(:perform_in)
+ it 'reschedules a migration if the database is not healthy' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
- expect { worker.perform('Foo', [10, 20]) }.to change { counter.get(db_config_name: tracking_database) }.by(1)
- end
+ worker.perform('Foo', [10, 20])
+ end
+
+ it 'increments the unhealthy counter' do
+ counter = Gitlab::Metrics.counter(:background_migration_database_health_reschedules, 'msg')
+
+ expect(described_class).to receive(:perform_in)
+
+ expect { worker.perform('Foo', [10, 20]) }.to change { counter.get(db_config_name: tracking_database) }.by(1)
+ end
- context 'when lease_attempts is 0' do
- it 'gives up performing the migration' do
- expect(described_class).not_to receive(:perform_in)
- expect(Sidekiq.logger).to receive(:warn).with(
- class: 'Foo',
- message: 'Database was unhealthy after several tries. Giving up.',
- job_id: 1)
+ context 'when lease_attempts is 0' do
+ it 'gives up performing the migration' do
+ expect(described_class).not_to receive(:perform_in)
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: 'Foo',
+ message: 'Database was unhealthy after several tries. Giving up.',
+ job_id: 1)
- worker.perform('Foo', [10, 20], 0)
+ worker.perform('Foo', [10, 20], 0)
+ end
end
end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 3d4e840fe2d..54962eac100 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, feature_flag:|
+RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database|
include ExclusiveLeaseHelpers
describe 'defining the job attributes' do
@@ -40,13 +40,17 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
describe '.enabled?' do
- it 'does not raise an error' do
- expect { described_class.enabled? }.not_to raise_error
- end
+ it 'returns true when execute_batched_migrations_on_schedule feature flag is enabled' do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
- it 'returns true' do
expect(described_class.enabled?).to be_truthy
end
+
+ it 'returns false when execute_batched_migrations_on_schedule feature flag is disabled' do
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
end
describe '#perform' do
@@ -86,7 +90,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
context 'when the feature flag is disabled' do
before do
- stub_feature_flags(feature_flag => false)
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
end
it 'does nothing' do
@@ -98,10 +102,26 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
context 'when the feature flag is enabled' do
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
+
before do
- stub_feature_flags(feature_flag => true)
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration).and_return(nil)
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: base_model.connection)
+ .and_return(nil)
+ end
+
+ context 'when database config is shared' do
+ it 'does nothing' do
+ expect(Gitlab::Database).to receive(:db_config_share_with)
+ .with(base_model.connection_db_config).and_return('main')
+
+ expect(worker).not_to receive(:active_migration)
+ expect(worker).not_to receive(:run_active_migration)
+
+ worker.perform
+ end
end
context 'when no active migrations exist' do
@@ -121,6 +141,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
before do
allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: base_model.connection)
.and_return(migration)
allow(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(true)
@@ -222,6 +243,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
+ let(:gitlab_schema) { "gitlab_#{tracking_database}" }
let!(:migration) do
create(
:batched_background_migration,
@@ -232,10 +254,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
batch_size: batch_size,
sub_batch_size: sub_batch_size,
job_class_name: 'ExampleDataMigration',
- job_arguments: [1]
+ job_arguments: [1],
+ gitlab_schema: gitlab_schema
)
end
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
let(:table_name) { 'example_data' }
let(:batch_size) { 5 }
let(:sub_batch_size) { 2 }
diff --git a/spec/support/shared_examples/workers/idempotency_shared_examples.rb b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
index 9d9b371d61a..be43ea7d5f0 100644
--- a/spec/support/shared_examples/workers/idempotency_shared_examples.rb
+++ b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
@@ -20,7 +20,11 @@ RSpec.shared_examples 'an idempotent worker' do
# Avoid stubbing calls for a more accurate run.
subject do
- defined?(job_args) ? perform_multiple(job_args) : perform_multiple
+ if described_class.include?(::Gitlab::EventStore::Subscriber)
+ event_worker
+ else
+ standard_worker
+ end
end
it 'is labeled as idempotent' do
@@ -30,4 +34,12 @@ RSpec.shared_examples 'an idempotent worker' do
it 'performs multiple times sequentially without raising an exception' do
expect { subject }.not_to raise_error
end
+
+ def event_worker
+ consume_event(subscriber: described_class, event: event)
+ end
+
+ def standard_worker
+ defined?(job_args) ? perform_multiple(job_args) : perform_multiple
+ end
end