summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/support/shared_examples
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
downloadgitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/ci/jobs_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/controllers/binary_blob_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb47
-rw-r--r--spec/support/shared_examples/controllers/unique_visits_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb108
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/error_tracking_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/file_uploads_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/alert_management/payload.rb34
-rw-r--r--spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb145
-rw-r--r--spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/search/recent_items.rb87
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb2
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb419
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/models/throttled_touch_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/requests/api/boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb843
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/snippets_shared_examples.rb139
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/services/common_system_notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/services/issuable_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/services/merge_request_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb4
69 files changed, 2490 insertions, 592 deletions
diff --git a/spec/support/shared_examples/ci/jobs_shared_examples.rb b/spec/support/shared_examples/ci/jobs_shared_examples.rb
new file mode 100644
index 00000000000..d952d4a98eb
--- /dev/null
+++ b/spec/support/shared_examples/ci/jobs_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a job with artifacts and trace' do |result_is_array: true|
+ context 'with artifacts and trace' do
+ let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) }
+
+ it 'returns artifacts and trace data', :skip_before_request do
+ get api(api_endpoint, api_user)
+ json_job = json_response.is_a?(Array) ? json_response.find { |job| job['id'] == second_job.id } : json_response
+
+ expect(json_job['artifacts_file']).not_to be_nil
+ expect(json_job['artifacts_file']).not_to be_empty
+ expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename)
+ expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size)
+ expect(json_job['artifacts']).not_to be_nil
+ expect(json_job['artifacts']).to be_an Array
+ expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length)
+ json_job['artifacts'].each do |artifact|
+ expect(artifact).not_to be_nil
+ file_type = Ci::JobArtifact.file_types[artifact['file_type']]
+ expect(artifact['size']).to eq(second_job.job_artifacts.find_by(file_type: file_type).size)
+ expect(artifact['filename']).to eq(second_job.job_artifacts.find_by(file_type: file_type).filename)
+ expect(artifact['file_format']).to eq(second_job.job_artifacts.find_by(file_type: file_type).file_format)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb
deleted file mode 100644
index acce7642cfe..00000000000
--- a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'editing snippet checks blob is binary' do
- let(:snippets_binary_blob_value) { true }
-
- before do
- sign_in(user)
-
- allow_next_instance_of(Blob) do |blob|
- allow(blob).to receive(:binary?).and_return(binary)
- end
-
- stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value)
-
- subject
- end
-
- context 'when blob is text' do
- let(:binary) { false }
-
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:edit)
- end
- end
-
- context 'when blob is binary' do
- let(:binary) { true }
-
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:edit)
- end
-
- context 'when feature flag :snippets_binary_blob is disabled' do
- let(:snippets_binary_blob_value) { false }
-
- it 'redirects away' do
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
- end
- end
-end
-
-RSpec.shared_examples 'updating snippet checks blob is binary' do
- let(:snippets_binary_blob_value) { true }
-
- before do
- sign_in(user)
-
- allow_next_instance_of(Blob) do |blob|
- allow(blob).to receive(:binary?).and_return(binary)
- end
-
- stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value)
-
- subject
- end
-
- context 'when blob is text' do
- let(:binary) { false }
-
- it 'updates successfully' do
- expect(snippet.reload.title).to eq title
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
- end
-
- context 'when blob is binary' do
- let(:binary) { true }
-
- it 'updates successfully' do
- expect(snippet.reload.title).to eq title
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
-
- context 'when feature flag :snippets_binary_blob is disabled' do
- let(:snippets_binary_blob_value) { false }
-
- it 'redirects away without updating' do
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- expect(snippet.reload.title).not_to eq title
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
deleted file mode 100644
index 17087456720..00000000000
--- a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'instance statistics availability' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- stub_application_setting(usage_ping_enabled: true)
- end
-
- describe 'GET #index' do
- it 'is available when the feature is available publicly' do
- get :index
-
- expect(response).to have_gitlab_http_status(:success)
- end
-
- it 'renders a 404 when the feature is not available publicly' do
- stub_application_setting(instance_statistics_visibility_private: true)
-
- get :index
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- context 'for admins' do
- let(:user) { create(:admin) }
-
- context 'when admin mode disabled' do
- it 'forbids access when the feature is not available publicly' do
- stub_application_setting(instance_statistics_visibility_private: true)
-
- get :index
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'when admin mode enabled', :enable_admin_mode do
- it 'allows access when the feature is not available publicly' do
- stub_application_setting(instance_statistics_visibility_private: true)
-
- get :index
-
- expect(response).to have_gitlab_http_status(:success)
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
index 62a1a07b6c1..02915206cc5 100644
--- a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
@@ -42,10 +42,6 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil
let(:result_issuable) { issuables.first }
let(:search) { result_issuable.title }
- before do
- stub_feature_flags(attempt_project_search_optimizations: true)
- end
-
# .simple_sorts is the same across all Sortable classes
sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority]
sorts.each do |sort|
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
new file mode 100644
index 00000000000..7e5a225f020
--- /dev/null
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
+ context 'when format is HTML' do
+ let(:format) { :html }
+
+ it 'tracks unique event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+
+ it 'tracks unique event if DNT is not enabled' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id)
+ request.headers['DNT'] = '0'
+
+ subject
+ end
+
+ it 'does not track unique event if DNT is enabled' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+ request.headers['DNT'] = '1'
+
+ subject
+ end
+
+ context 'when feature flag is disabled' do
+ it 'does not track unique event' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+ end
+ end
+
+ context 'when format is JSON' do
+ let(:format) { :json }
+
+ it 'does not track unique event if the format is JSON' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
index 90588756eb0..428389a9a01 100644
--- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'tracking unique visits' do |method|
+ let(:request_params) { {} }
+
it 'tracks unique visit if the format is HTML' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id)
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index c89ee0d25ae..4ca400dd87b 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -388,7 +388,54 @@ RSpec.shared_examples 'wiki controller actions' do
end.not_to change { wiki.list_pages.size }
expect(response).to render_template('shared/wikis/edit')
- expect(flash[:alert]).to eq('Could not create wiki page')
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let(:id_param) { wiki_title }
+
+ subject do
+ delete(:destroy,
+ params: routing_params.merge(
+ id: id_param
+ ))
+ end
+
+ context 'when page exists' do
+ it 'deletes the page' do
+ expect do
+ subject
+ end.to change { wiki.list_pages.size }.by(-1)
+ end
+
+ context 'but page cannot be deleted' do
+ before do
+ allow_next_instance_of(WikiPage) do |page|
+ allow(page).to receive(:delete).and_return(false)
+ end
+ end
+
+ it 'renders the edit state' do
+ expect do
+ subject
+ end.not_to change { wiki.list_pages.size }
+
+ expect(response).to render_template('shared/wikis/edit')
+ expect(assigns(:error).message).to eq('Could not delete wiki page')
+ end
+ end
+ end
+
+ context 'when page does not exist' do
+ let(:id_param) { 'nil' }
+
+ it 'renders 404' do
+ expect do
+ subject
+ end.not_to change { wiki.list_pages.size }
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
new file mode 100644
index 00000000000..ddc03e178ba
--- /dev/null
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'hardware device for 2fa' do |device_type|
+ include Spec::Support::Helpers::Features::TwoFactorHelpers
+
+ def register_device(device_type, **kwargs)
+ case device_type.downcase
+ when "u2f"
+ register_u2f_device(**kwargs)
+ when "webauthn"
+ register_webauthn_device(**kwargs)
+ else
+ raise "Unknown device type #{device_type}"
+ end
+ end
+
+ describe "registration" do
+ let(:user) { create(:user) }
+
+ before do
+ gitlab_sign_in(user)
+ user.update_attribute(:otp_required_for_login, true)
+ end
+
+ describe 'when 2FA via OTP is disabled' do
+ before do
+ user.update_attribute(:otp_required_for_login, false)
+ end
+
+ it 'does not allow registering a new device' do
+ visit profile_account_path
+ click_on 'Enable two-factor authentication'
+
+ expect(page).to have_button("Set up new device", disabled: true)
+ end
+ end
+
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows registering a new device with a name' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("You've already enabled two-factor authentication using one time password authenticators")
+
+ device = register_device(device_type)
+
+ expect(page).to have_content(device.name)
+ expect(page).to have_content("Your #{device_type} device was registered")
+ end
+
+ it 'allows deleting a device' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("You've already enabled two-factor authentication using one time password authenticators")
+
+ first_device = register_device(device_type)
+ second_device = register_device(device_type, name: 'My other device')
+
+ expect(page).to have_content(first_device.name)
+ expect(page).to have_content(second_device.name)
+
+ accept_confirm { click_on 'Delete', match: :first }
+
+ expect(page).to have_content('Successfully deleted')
+ expect(page.body).not_to have_content(first_device.name)
+ expect(page.body).to have_content(second_device.name)
+ end
+ end
+ end
+
+ describe 'fallback code authentication' do
+ let(:user) { create(:user) }
+
+ before do
+ # Register and logout
+ gitlab_sign_in(user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ end
+
+ describe 'when no device is registered' do
+ before do
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ end
+
+ it 'shows the fallback otp code UI' do
+ assert_fallback_ui(page)
+ end
+ end
+
+ describe 'when a device is registered' do
+ before do
+ manage_two_factor_authentication
+ register_device(device_type)
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ end
+
+ it 'provides a button that shows the fallback otp code UI' do
+ expect(page).to have_link('Sign in via 2FA code')
+
+ click_link('Sign in via 2FA code')
+
+ assert_fallback_ui(page)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 487c38da7da..c9910487798 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -124,3 +124,16 @@ end
def get_textarea_height
page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
end
+
+RSpec.shared_examples 'an editable merge request with reviewers' do
+ it 'updates merge request', :js do
+ find('.js-reviewer-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-reviewer-search' do
+ expect(page).to have_content user.name
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb
index ae7d62f31a2..92fc54ce0b0 100644
--- a/spec/support/shared_examples/features/error_tracking_shared_example.rb
+++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb
@@ -36,10 +36,10 @@ end
RSpec.shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.stacktrace') do
- find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line
+ find("div.file-holder:nth-child(#{selected_line}) svg[data-testid='chevron-right-icon']").click if selected_line
expanded_line = find("div.file-holder:nth-child(#{expected_line})")
- expect(expanded_line).to have_css('svg.ic-chevron-down')
+ expect(expanded_line).to have_css('svg[data-testid="chevron-down-icon"]')
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'][-expected_line]['context'].each do |context|
expect(page).to have_content(context[0])
diff --git a/spec/support/shared_examples/features/file_uploads_shared_examples.rb b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
new file mode 100644
index 00000000000..ea8c8d44501
--- /dev/null
+++ b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling file uploads' do |shared_examples_name|
+ context 'with object storage disabled' do
+ context 'with upload_middleware_jwt_params_handler disabled' do
+ before do
+ stub_feature_flags(upload_middleware_jwt_params_handler: false)
+
+ expect_next_instance_of(Gitlab::Middleware::Multipart::Handler) do |handler|
+ expect(handler).to receive(:with_open_files).and_call_original
+ end
+ end
+
+ it_behaves_like shared_examples_name
+ end
+
+ context 'with upload_middleware_jwt_params_handler enabled' do
+ before do
+ stub_feature_flags(upload_middleware_jwt_params_handler: true)
+
+ expect_next_instance_of(Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler|
+ expect(handler).to receive(:with_open_files).and_call_original
+ end
+ end
+
+ it_behaves_like shared_examples_name
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 6debbf81fc0..f201421e827 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
def package_table_row(index)
- page.all("#{packages_table_selector} > [data-qa-selector=\"packages-row\"]")[index].text
+ page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text
end
end
@@ -32,7 +32,7 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_current_path(project_package_path(package.project, package))
- page.within('.detail-page-header') do
+ page.within('[data-qa-selector="package_title"]') do
expect(page).to have_content(package.name)
end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index a58e716efd2..3a046c3feec 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -20,3 +20,64 @@ RSpec.shared_examples 'a working membership object query' do |model_option|
).to eq('DEVELOPER')
end
end
+
+RSpec.shared_examples 'querying members with a group' do
+ let_it_be(:root_group) { create(:group, :private) }
+ let_it_be(:group_1) { create(:group, :private, parent: root_group, name: 'Main Group') }
+ let_it_be(:group_2) { create(:group, :private, parent: root_group) }
+
+ let_it_be(:user_1) { create(:user, name: 'test user') }
+ let_it_be(:user_2) { create(:user, name: 'test user 2') }
+ let_it_be(:user_3) { create(:user, name: 'another user 1') }
+ let_it_be(:user_4) { create(:user, name: 'another user 2') }
+
+ let_it_be(:root_group_member) { create(:group_member, user: user_4, group: root_group) }
+ let_it_be(:group_1_member) { create(:group_member, user: user_2, group: group_1) }
+ let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) }
+
+ let(:args) { {} }
+
+ subject do
+ resolve(described_class, obj: resource, args: args, ctx: { current_user: user_4 })
+ end
+
+ describe '#resolve' do
+ before do
+ group_1.add_maintainer(user_4)
+ end
+
+ it 'finds all resource members' do
+ expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member)
+ end
+
+ context 'with search' do
+ context 'when the search term matches a user' do
+ let(:args) { { search: 'test' } }
+
+ it 'searches users by user name' do
+ expect(subject).to contain_exactly(resource_member, group_1_member)
+ end
+ end
+
+ context 'when the search term does not match any user' do
+ let(:args) { { search: 'nothing' } }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ context 'when user can not see resource members' do
+ let_it_be(:other_user) { create(:user) }
+
+ subject do
+ resolve(described_class, obj: resource, args: args, ctx: { current_user: other_user })
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index 86d2bb6c747..b67cac94547 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -19,6 +19,13 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
end
end
+# There must be a method or let called `mutation` defined that executes
+# the mutation.
+RSpec.shared_examples 'a mutation that returns a top-level access error' do
+ include_examples 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+end
+
RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:|
it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) do
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index 2ef71d275a2..7627a7b4d59 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -84,6 +84,9 @@ RSpec.shared_examples 'sorted paginated query' do
cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info)
post_graphql(cursored_query, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param)
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
new file mode 100644
index 00000000000..ed139e638bf
--- /dev/null
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Gitlab-style deprecations' do
+ describe 'validations' do
+ it 'raises an informative error if `deprecation_reason` is used' do
+ expect { subject(deprecation_reason: 'foo') }.to raise_error(
+ ArgumentError,
+ 'Use `deprecated` property instead of `deprecation_reason`. ' \
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values'
+ )
+ end
+
+ it 'raises an error if a required property is missing', :aggregate_failures do
+ expect { subject(deprecated: { milestone: '1.10' }) }.to raise_error(
+ ArgumentError,
+ 'Please provide a `reason` within `deprecated`'
+ )
+ expect { subject(deprecated: { reason: 'Deprecation reason' }) }.to raise_error(
+ ArgumentError,
+ 'Please provide a `milestone` within `deprecated`'
+ )
+ end
+
+ it 'raises an error if milestone is not a String', :aggregate_failures do
+ expect { subject(deprecated: { milestone: 1.10, reason: 'Deprecation reason' }) }.to raise_error(
+ ArgumentError,
+ '`milestone` must be a `String`'
+ )
+ end
+ end
+
+ it 'adds a formatted `deprecated_reason` to the subject' do
+ deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' })
+
+ expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10')
+ end
+
+ it 'appends to the description if given' do
+ deprecable = subject(
+ deprecated: { milestone: '1.10', reason: 'Deprecation reason' },
+ description: 'Deprecable description'
+ )
+
+ expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason')
+ end
+
+ it 'does not append to the description if it is absent' do
+ deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' })
+
+ expect(deprecable.description).to be_nil
+ end
+end
diff --git a/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb b/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
index d903c0f10e0..479b26977e2 100644
--- a/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples "referenced feature visibility" do |*related_features|
+ let(:enable_user?) { false }
let(:feature_fields) do
related_features.map { |feature| (feature + "_access_level").to_sym }
end
@@ -35,8 +36,11 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features|
end
context "when feature is enabled" do
- # The project is public
+ # Allows implementing specs to enable finer-tuned permissions
+ let(:enable_user?) { true }
+
it "creates reference" do
+ # The project is public
set_features_fields_to(ProjectFeature::ENABLED)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
diff --git a/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb b/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb
new file mode 100644
index 00000000000..54b021e8371
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'parsable alert payload field with fallback' do |fallback, *paths|
+ context 'without payload' do
+ it { is_expected.to eq(fallback) }
+ end
+
+ paths.each do |path|
+ context "with #{path}" do
+ let(:value) { 'some value' }
+
+ before do
+ section, name = path.split('/')
+ raw_payload[section] = name ? { name => value } : value
+ end
+
+ it { is_expected.to eq(value) }
+ end
+ end
+end
+
+RSpec.shared_examples 'parsable alert payload field' do |*paths|
+ it_behaves_like 'parsable alert payload field with fallback', nil, *paths
+end
+
+RSpec.shared_examples 'subclass has expected api' do
+ it 'defines all public methods in the base class' do
+ default_methods = Gitlab::AlertManagement::Payload::Base.public_instance_methods
+ subclass_methods = described_class.public_instance_methods
+ missing_methods = subclass_methods - default_methods
+
+ expect(missing_methods).to be_empty
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb
new file mode 100644
index 00000000000..18a5087da3b
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'an atlassian identity' do
+ it 'sets the proper values' do
+ expect(identity.extern_uid).to eq(extern_uid)
+ expect(identity.token).to eq(credentials[:token])
+ expect(identity.refresh_token).to eq(credentials[:refresh_token])
+ expect(identity.expires_at.to_i).to eq(credentials[:expires_at])
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
index 8cf6babe146..e93077c42e1 100644
--- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
@@ -63,7 +63,7 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
- Timecop.freeze do
+ freeze_time do
resource_count = is_for_notes ? Note.count : resource_class.count
expect(resource_count).to eq 5
diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
index a3800f050bb..f018ece0d46 100644
--- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
@@ -5,18 +5,19 @@ RSpec.shared_examples 'network policy common specs' do
let(:namespace) { 'example-namespace' }
let(:labels) { nil }
+ describe '#generate' do
+ subject { policy.generate }
+
+ it { is_expected.to eq(Kubeclient::Resource.new(policy.resource)) }
+ end
+
describe 'as_json' do
let(:json_policy) do
{
name: name,
namespace: namespace,
creation_timestamp: nil,
- manifest: YAML.dump(
- {
- metadata: metadata,
- spec: spec
- }.deep_stringify_keys
- ),
+ manifest: YAML.dump(policy.resource.deep_stringify_keys),
is_autodevops: false,
is_enabled: true
}
diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
new file mode 100644
index 00000000000..6327367fcc2
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling all upload parameters conditions' do
+ context 'one root parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
+ let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id) }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+
+ subject
+ end
+ end
+
+ context 'two root parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('file1' => uploaded_filepath, 'file2' => uploaded_filepath2) }
+ let(:params) do
+ upload_parameters_for(filepath: uploaded_filepath, key: 'file1', filename: filename, remote_id: remote_id).merge(
+ upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', filename: filename2, remote_id: remote_id2)
+ )
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file1) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(file2) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'one nested parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath) }
+ let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar))
+
+ subject
+ end
+ end
+
+ context 'two nested parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath, 'user[screenshot]' => uploaded_filepath2) }
+ let(:params) do
+ {
+ 'user' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id),
+ 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
+ }
+ }
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user screenshot) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'one deeply nested parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath) }
+ let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas))
+
+ subject
+ end
+ end
+
+ context 'two deeply nested parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath, 'user[friend][ananas]' => uploaded_filepath2) }
+ let(:params) do
+ {
+ 'user' => {
+ 'avatar' => {
+ 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id)
+ },
+ 'friend' => {
+ 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
+ }
+ }
+ }
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas) },
+ { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user friend ananas) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'three parameters nested at different levels' do
+ include_context 'with three temporary files for multipart'
+
+ let(:rewritten_fields) do
+ rewritten_fields_hash(
+ 'file' => uploaded_filepath,
+ 'user[avatar]' => uploaded_filepath2,
+ 'user[friend][avatar]' => uploaded_filepath3
+ )
+ end
+
+ let(:params) do
+ upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', remote_id: remote_id).merge(
+ 'user' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2),
+ 'friend' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, filename: filename3, remote_id: remote_id3)
+ }
+ }
+ )
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user avatar) },
+ { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w(user friend avatar) }
+ ])
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
new file mode 100644
index 00000000000..94ef41ce5a5
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'access restricted confidential issues' do
+ let(:query) { 'issue' }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:project) { create(:project, :internal) }
+
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
+
+ subject(:objects) do
+ described_class.new(user, query, project: project).objects('issues')
+ end
+
+ context 'when the user is non-member' do
+ let(:user) { create(:user) }
+
+ it 'does not list project confidential issues for non project members' do
+ expect(objects).to contain_exactly(issue)
+ expect(results.limited_issues_count).to eq 1
+ end
+ end
+
+ context 'when the member is guest' do
+ let(:user) do
+ create(:user) { |guest| project.add_guest(guest) }
+ end
+
+ it 'does not list project confidential issues for project members with guest role' do
+ expect(objects).to contain_exactly(issue)
+ expect(results.limited_issues_count).to eq 1
+ end
+ end
+
+ context 'when the user is the author' do
+ let(:user) { author }
+
+ it 'lists project confidential issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1)
+ expect(results.limited_issues_count).to eq 2
+ end
+ end
+
+ context 'when the user is the assignee' do
+ let(:user) { assignee }
+
+ it 'lists project confidential issues for assignee' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_2)
+ expect(results.limited_issues_count).to eq 2
+ end
+ end
+
+ context 'when the user is a developper' do
+ let(:user) do
+ create(:user) { |user| project.add_developer(user) }
+ end
+
+ it 'lists project confidential issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1,
+ security_issue_2)
+ expect(results.limited_issues_count).to eq 3
+ end
+ end
+
+ context 'when the user is admin', :request_store do
+ let(:user) { create(:user, admin: true) }
+
+ it 'lists all project issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1,
+ security_issue_2)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
index 4aeae788114..025f0d5c7ea 100644
--- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
@@ -17,5 +17,9 @@ RSpec.shared_examples 'a repo type' do
it 'finds the repository for the repo type' do
expect(described_class.repository_for(expected_container)).to eq(expected_repository)
end
+
+ it 'returns nil when container is nil' do
+ expect(described_class.repository_for(nil)).to eq(nil)
+ end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
new file mode 100644
index 00000000000..f96ff4b101e
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'search recent items' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:recent_items) { described_class.new(user: user, items_limit: 5) }
+ let(:item) { create_item(content: 'hello world 1', project: project) }
+ let(:project) { create(:project, :public) }
+
+ describe '#log_view', :clean_gitlab_redis_shared_state do
+ it 'adds the item to the recent items' do
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to eq([item])
+ end
+
+ it 'removes an item when it exceeds the size items_limit' do
+ (1..6).each do |i|
+ recent_items.log_view(create_item(content: "item #{i}", project: project))
+ end
+
+ results = recent_items.search('item')
+
+ expect(results.map(&:title)).to contain_exactly('item 6', 'item 5', 'item 4', 'item 3', 'item 2')
+ end
+
+ it 'expires the items after expires_after' do
+ recent_items = described_class.new(user: user, expires_after: 0)
+
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to be_empty
+ end
+
+ it 'does not include results logged for another user' do
+ another_user = create(:user)
+ another_item = create_item(content: 'hello world 2', project: project)
+ described_class.new(user: another_user).log_view(another_item)
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to eq([item])
+ end
+ end
+
+ describe '#search', :clean_gitlab_redis_shared_state do
+ let(:item1) { create_item(content: "matching item 1", project: project) }
+ let(:item2) { create_item(content: "matching item 2", project: project) }
+ let(:item3) { create_item(content: "matching item 3", project: project) }
+ let(:non_matching_item) { create_item(content: "different item", project: project) }
+ let!(:non_viewed_item) { create_item(content: "matching but not viewed item", project: project) }
+
+ before do
+ recent_items.log_view(item1)
+ recent_items.log_view(item2)
+ recent_items.log_view(item3)
+ recent_items.log_view(non_matching_item)
+ end
+
+ it 'matches partial text in the item title' do
+ expect(recent_items.search('matching')).to contain_exactly(item1, item2, item3)
+ end
+
+ it 'returns results sorted by recently viewed' do
+ recent_items.log_view(item2)
+
+ expect(recent_items.search('matching')).to eq([item2, item3, item1])
+ end
+
+ it 'does not leak items you no longer have access to' do
+ private_project = create(:project, :public, namespace: create(:group))
+ private_item = create_item(content: 'matching item title', project: private_project)
+
+ recent_items.log_view(private_item)
+
+ private_project.update!(visibility_level: Project::PRIVATE)
+
+ expect(recent_items.search('matching')).not_to include(private_item)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb
new file mode 100644
index 00000000000..e80ec516407
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by state' do
+ context 'state not provided' do
+ let(:filters) { {} }
+
+ it 'returns opened and closed results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'all state' do
+ let(:filters) { { state: 'all' } }
+
+ it 'returns opened and closed results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'closed state' do
+ let(:filters) { { state: 'closed' } }
+
+ it 'returns only closed results', :aggregate_failures do
+ expect(results.objects(scope)).not_to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'opened state' do
+ let(:filters) { { state: 'opened' } }
+
+ it 'returns only opened results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).not_to include closed_result
+ end
+ end
+
+ context 'unsupported state' do
+ let(:filters) { { state: 'hello' } }
+
+ it 'returns only opened results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
new file mode 100644
index 00000000000..73beef06855
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'SQL set operator' do |operator_keyword|
+ operator_keyword = operator_keyword.upcase
+
+ let(:relation_1) { User.where(email: 'alice@example.com').select(:id) }
+ let(:relation_2) { User.where(email: 'bob@example.com').select(:id) }
+
+ def to_sql(relation)
+ relation.reorder(nil).to_sql
+ end
+
+ describe '.operator_keyword' do
+ it { expect(described_class.operator_keyword).to eq operator_keyword }
+ end
+
+ describe '#to_sql' do
+ it "returns a String joining relations together using a #{operator_keyword}" do
+ set_operator = described_class.new([relation_1, relation_2])
+
+ expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})")
+ end
+
+ it 'skips Model.none segements' do
+ empty_relation = User.none
+ set_operator = described_class.new([empty_relation, relation_1, relation_2])
+
+ expect {User.where("users.id IN (#{set_operator.to_sql})").to_a}.not_to raise_error
+ expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})")
+ end
+
+ it "uses #{operator_keyword} ALL when removing duplicates is disabled" do
+ set_operator = described_class
+ .new([relation_1, relation_2], remove_duplicates: false)
+
+ expect(set_operator.to_sql).to include("#{operator_keyword} ALL")
+ end
+
+ it 'returns `NULL` if all relations are empty' do
+ empty_relation = User.none
+ set_operator = described_class.new([empty_relation, empty_relation])
+
+ expect(set_operator.to_sql).to eq('NULL')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb
new file mode 100644
index 00000000000..4e35e388b23
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'an incident management tracked event' do |event|
+ describe ".track_event", :clean_gitlab_redis_shared_state do
+ let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
+ let(:start_time) { 1.minute.ago }
+ let(:end_time) { 1.minute.from_now }
+
+ it "tracks the event using redis" do
+ # Allow other subsequent calls
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(current_user.id, event.to_s)
+ .and_call_original
+
+ expect { subject }
+ .to change { counter.unique_events(event_names: event.to_s, start_date: start_time, end_date: end_time) }
+ .by 1
+ end
+ end
+end
+
+RSpec.shared_examples 'does not track incident management event' do |event|
+ it 'does not track the event', :clean_gitlab_redis_shared_state do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event)
+ .with(anything, event.to_s)
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
index 6611a168c04..0ee24dd93d7 100644
--- a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
@@ -85,7 +85,7 @@ RSpec.shared_examples 'chat slash commands service' do
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
subject do
- described_class.create(project: project, properties: { token: 'token' })
+ described_class.create!(project: project, properties: { token: 'token' })
end
it 'triggers the command' do
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
index 394253fb699..ac8022a4726 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'cluster application helm specs' do |application_name|
- let(:application) { create(application_name) }
+ let(:application) { create(application_name) } # rubocop:disable Rails/SaveBang
describe '#uninstall_command' do
subject { application.uninstall_command }
diff --git a/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb
new file mode 100644
index 00000000000..6b208c0024d
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'from set operator' do |sql_klass|
+ from_set_operator_concern = described_class
+ operator_keyword = sql_klass.operator_keyword
+ operator_method = "from_#{sql_klass.operator_keyword.downcase}"
+
+ describe "##{operator_method}" do
+ let(:model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+
+ include from_set_operator_concern
+ end
+ end
+
+ it "selects from the results of the #{operator_keyword}" do
+ query = model.public_send(operator_method, [model.where(id: 1), model.where(id: 2)])
+
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\n#{operator_keyword}\n\(SELECT.+\)\) users/m)
+ end
+
+ it 'supports the use of a custom alias for the sub query' do
+ query = model.public_send(operator_method,
+ [model.where(id: 1), model.where(id: 2)],
+ alias_as: 'kittens'
+ )
+
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\n#{operator_keyword}\n\(SELECT.+\)\) kittens/m)
+ end
+
+ it 'supports keeping duplicate rows' do
+ query = model.public_send(operator_method,
+ [model.where(id: 1), model.where(id: 2)],
+ remove_duplicates: false
+ )
+
+ expect(query.to_sql)
+ .to match(/FROM \(\(SELECT.+\)\n#{operator_keyword} ALL\n\(SELECT.+\)\) users/m)
+ 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 d21823661f8..07d687147bc 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -8,26 +8,29 @@ RSpec.shared_examples 'includes Limitable concern' do
context 'without plan limits configured' do
it 'can create new models' do
- expect { subject.save }.to change { described_class.count }
+ expect { subject.save! }.to change { described_class.count }
end
end
context 'with plan limits configured' do
before do
- plan_limits.update(subject.class.limit_name => 1)
+ plan_limits.update!(subject.class.limit_name => 1)
end
it 'can create new models' do
- expect { subject.save }.to change { described_class.count }
+ expect { subject.save! }.to change { described_class.count }
end
context 'with an existing model' do
before do
- subject.dup.save
+ subject.dup.save!
end
it 'cannot create new models exceeding the plan limits' do
- expect { subject.save }.not_to change { described_class.count }
+ expect do
+ expect { subject.save! }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ .not_to change { described_class.count }
expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded")
end
end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index 15ca1f56bd0..d199bae4170 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -102,7 +102,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:timebox) { create(timebox_type, *timebox_args, group: group) }
before do
- project.update(group: group)
+ project.update!(group: group)
end
it "does not accept the same title in a group twice" do
diff --git a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
index 8c3e073193c..64390ccdc25 100644
--- a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'raises an error' do
allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil)
- expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError,
+ expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError,
"Failed to find diff line for: #{diff_file_from_repository.file_path}, "\
"old_line: #{position.old_line}"\
", new_line: #{position.new_line}")
@@ -25,11 +25,11 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'fallback to fetch file from repository' do
expect_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository)
- subject.save
+ subject.save!
end
it 'creates a diff note file' do
- subject.save
+ subject.save!
expect(subject.reload.note_diff_file).to be_present
end
@@ -40,7 +40,7 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'raises an error' do
allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(nil)
- expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file')
+ expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file')
end
end
end
diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
index b0cdc77a378..759b22f794e 100644
--- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
@@ -18,8 +18,6 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
)
end
- subject { build(factory_on_commit, commit_id: commit_id, position: position) }
-
context 'position diff refs matches commit diff refs' do
it 'is valid' do
expect(subject).to be_valid
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 9bf157212d3..7ede6f0d8d4 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -25,21 +25,21 @@ RSpec.shared_examples 'inherited access level as a member of entity' do
it 'is allowed to change to be a developer of the entity' do
entity.add_maintainer(user)
- expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
+ expect { member.update!(access_level: Gitlab::Access::DEVELOPER) }
.to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
end
it 'is not allowed to change to be a guest of the entity' do
entity.add_maintainer(user)
- expect { member.update(access_level: Gitlab::Access::GUEST) }
+ expect { member.update(access_level: Gitlab::Access::GUEST) } # rubocop:disable Rails/SaveBang
.not_to change { member.reload.access_level }
end
it "shows an error if the member can't be updated" do
entity.add_maintainer(user)
- member.update(access_level: Gitlab::Access::REPORTER)
+ expect { member.update!(access_level: Gitlab::Access::REPORTER) }.to raise_error(ActiveRecord::RecordInvalid)
expect(member.errors.full_messages).to eq(["Access level should be greater than or equal to Developer inherited membership from group #{parent_entity.name}"])
end
@@ -51,7 +51,7 @@ RSpec.shared_examples 'inherited access level as a member of entity' do
non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
- expect { non_member.update(access_level: Gitlab::Access::GUEST) }
+ expect { non_member.update!(access_level: Gitlab::Access::GUEST) }
.to change { non_member.reload.access_level }
end
end
@@ -60,7 +60,7 @@ end
RSpec.shared_examples '#valid_level_roles' do |entity_name|
let(:member_user) { create(:user) }
let(:group) { create(:group) }
- let(:entity) { create(entity_name) }
+ let(:entity) { create(entity_name) } # rubocop:disable Rails/SaveBang
let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
let(:presenter) { described_class.new(entity_member, current_user: member_user) }
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
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 050d710f1de..04af3935d15 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -13,7 +13,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
it "sends email to user" do
expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
- member.save
+ member.save!
end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index dda5fa37b26..94c52bdaaa6 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -162,7 +162,7 @@ RSpec.shared_examples 'an editable mentionable' do
end
it 'creates new cross-reference notes when the mentionable text is edited' do
- subject.save
+ subject.save!
subject.create_cross_references!
new_text = <<-MSG.strip_heredoc
@@ -270,7 +270,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
let!(:mentionable) { note.noteable }
before do
- note.update(note: note_desc)
+ note.update!(note: note_desc)
note.store_mentions!
add_member(user)
end
@@ -292,7 +292,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
let_it_be(:note_desc) { "#{mentioned_user.to_reference} and #{group.to_reference(full: true)} and @all" }
before do
- note.update(note: note_desc)
+ note.update!(note: note_desc)
note.store_mentions!
add_member(user)
end
@@ -305,7 +305,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << non_existing_record_id
}
- user_mention.update(mention_ids)
+ user_mention.update!(mention_ids)
end
it 'filters out inexistent mentions' do
@@ -328,7 +328,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
}
- user_mention.update(mention_ids)
+ user_mention.update!(mention_ids)
add_member(mega_user)
private_project.add_developer(mega_user)
diff --git a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
index 7bbc0c5a364..7701ab42007 100644
--- a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
@@ -53,7 +53,7 @@ RSpec.shared_examples 'latest successful build for sha or ref' do
let(:build_name) { pending_build.name }
before do
- pipeline.update(status: 'pending')
+ pipeline.update!(status: 'pending')
end
it 'returns empty relation' do
diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
index e4668926d74..d1437244082 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -1,9 +1,25 @@
# frozen_string_literal: true
+# Notes for implementing classes:
+#
+# The following let bindings should be defined:
+# - `factory`: A symbol naming a factory to use to create items
+# - `default_params`: A HashMap of factory parameters to pass to the factory.
+#
+# The `default_params` should include the relative parent, so that any item
+# created with these parameters passed to the `factory` will be considered in
+# the same set of items relative to each other.
+#
+# For the purposes of efficiency, it is a good idea to bind the parent in
+# `let_it_be`, so that it is re-used across examples, but be careful that it
+# does not have any other children - it should only be used within this set of
+# shared examples.
RSpec.shared_examples 'a class that supports relative positioning' do
let(:item1) { create_item }
let(:item2) { create_item }
- let(:new_item) { create_item }
+ let(:new_item) { create_item(relative_position: nil) }
+
+ let(:set_size) { RelativePositioning.mover.context(item1).scoped_items.count }
def create_item(params = {})
create(factory, params.merge(default_params))
@@ -17,6 +33,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '.move_nulls_to_end' do
let(:item3) { create_item }
+ let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
it 'moves items with null relative_position to the end' do
item1.update!(relative_position: 1000)
@@ -28,10 +45,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
expect(item1.relative_position).to be(1000)
- expect(item1.prev_relative_position).to be_nil
- expect(item1.next_relative_position).to eq(item2.relative_position)
- expect(item2.next_relative_position).to eq(item3.relative_position)
- expect(item3.next_relative_position).to be_nil
+
+ expect(sibling_query.where(relative_position: nil)).not_to exist
+ expect(sibling_query.reorder(:relative_position, :id)).to eq([item1, item2, item3])
end
it 'preserves relative position' do
@@ -70,6 +86,37 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
end
+ it 'manages to move nulls to the end even if there is not enough space' do
+ run = run_at_end(20).to_a
+ bunch_a = create_items_with_positions(run[0..18])
+ bunch_b = create_items_with_positions([run.last])
+
+ nils = create_items_with_positions([nil] * 4)
+ described_class.move_nulls_to_end(nils)
+
+ items = [*bunch_a, *bunch_b, *nils]
+ items.each(&:reset)
+
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(items.reverse.sort_by(&:relative_position)).to eq(items)
+ end
+
+ it 'manages to move nulls to the end, stacking if we cannot create enough space' do
+ run = run_at_end(40).to_a
+ bunch = create_items_with_positions(run.select(&:even?))
+
+ nils = create_items_with_positions([nil] * 20)
+ described_class.move_nulls_to_end(nils)
+
+ items = [*bunch, *nils]
+ items.each(&:reset)
+
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch)
+ expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils)
+ expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min)
+ end
+
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
@@ -89,6 +136,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '.move_nulls_to_start' do
let(:item3) { create_item }
+ let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
it 'moves items with null relative_position to the start' do
item1.update!(relative_position: nil)
@@ -100,10 +148,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.map(&:reload)
expect(items.sort_by(&:relative_position)).to eq(items)
- expect(item1.prev_relative_position).to eq nil
- expect(item1.next_relative_position).to eq item2.relative_position
- expect(item2.next_relative_position).to eq item3.relative_position
- expect(item3.next_relative_position).to eq nil
+ expect(sibling_query.where(relative_position: nil)).not_to exist
+ expect(sibling_query.reorder(:relative_position, :id)).to eq(items)
expect(item3.relative_position).to be(1000)
end
@@ -130,193 +176,36 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(described_class.move_nulls_to_start([item1])).to be(0)
expect(item1.reload.relative_position).to be(1)
end
- end
-
- describe '#max_relative_position' do
- it 'returns maximum position' do
- expect(item1.max_relative_position).to eq item2.relative_position
- end
- end
-
- describe '#prev_relative_position' do
- it 'returns previous position if there is an item above' do
- item1.update!(relative_position: 5)
- item2.update!(relative_position: 15)
-
- expect(item2.prev_relative_position).to eq item1.relative_position
- end
-
- it 'returns nil if there is no item above' do
- expect(item1.prev_relative_position).to eq nil
- end
- end
-
- describe '#next_relative_position' do
- it 'returns next position if there is an item below' do
- item1.update!(relative_position: 5)
- item2.update!(relative_position: 15)
-
- expect(item1.next_relative_position).to eq item2.relative_position
- end
-
- it 'returns nil if there is no item below' do
- expect(item2.next_relative_position).to eq nil
- end
- end
-
- describe '#find_next_gap_before' do
- context 'there is no gap' do
- let(:items) { create_items_with_positions(run_at_start) }
-
- it 'returns nil' do
- items.each do |item|
- expect(item.send(:find_next_gap_before)).to be_nil
- end
- end
- end
-
- context 'there is a sequence ending at MAX_POSITION' do
- let(:items) { create_items_with_positions(run_at_end) }
-
- let(:gaps) do
- items.map { |item| item.send(:find_next_gap_before) }
- end
-
- it 'can find the gap at the start for any item in the sequence' do
- gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION }
-
- expect(gaps).to all(eq(gap))
- end
-
- it 'respects lower bounds' do
- gap = { start: items.first.relative_position, end: 10 }
- new_item.update!(relative_position: 10)
-
- expect(gaps).to all(eq(gap))
- end
- end
-
- specify do
- item1.update!(relative_position: 5)
-
- (0..10).each do |pos|
- item2.update!(relative_position: pos)
-
- gap = item2.send(:find_next_gap_before)
-
- expect(gap[:start]).to be <= item2.relative_position
- expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
- expect(gap[:start]).to be_valid_position
- expect(gap[:end]).to be_valid_position
- end
- end
-
- it 'deals with there not being any items to the left' do
- create_items_with_positions([1, 2, 3])
- new_item.update!(relative_position: 0)
-
- expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION)
- end
-
- it 'finds the next gap to the left, skipping adjacent values' do
- create_items_with_positions([1, 9, 10])
- new_item.update!(relative_position: 11)
-
- expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1)
- end
-
- it 'finds the next gap to the left' do
- create_items_with_positions([2, 10])
-
- new_item.update!(relative_position: 15)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10)
-
- new_item.update!(relative_position: 11)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2)
-
- new_item.update!(relative_position: 9)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2)
-
- new_item.update!(relative_position: 5)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2)
- end
- end
-
- describe '#find_next_gap_after' do
- context 'there is no gap' do
- let(:items) { create_items_with_positions(run_at_end) }
- it 'returns nil' do
- items.each do |item|
- expect(item.send(:find_next_gap_after)).to be_nil
- end
- end
- end
-
- context 'there is a sequence starting at MIN_POSITION' do
- let(:items) { create_items_with_positions(run_at_start) }
-
- let(:gaps) do
- items.map { |item| item.send(:find_next_gap_after) }
- end
-
- it 'can find the gap at the end for any item in the sequence' do
- gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION }
-
- expect(gaps).to all(eq(gap))
- end
+ it 'manages to move nulls to the start even if there is not enough space' do
+ run = run_at_start(20).to_a
+ bunch_a = create_items_with_positions([run.first])
+ bunch_b = create_items_with_positions(run[2..])
- it 'respects upper bounds' do
- gap = { start: items.last.relative_position, end: 10 }
- new_item.update!(relative_position: 10)
+ nils = create_items_with_positions([nil, nil, nil, nil])
+ described_class.move_nulls_to_start(nils)
- expect(gaps).to all(eq(gap))
- end
- end
-
- specify do
- item1.update!(relative_position: 5)
-
- (0..10).each do |pos|
- item2.update!(relative_position: pos)
-
- gap = item2.send(:find_next_gap_after)
-
- expect(gap[:start]).to be >= item2.relative_position
- expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
- expect(gap[:start]).to be_valid_position
- expect(gap[:end]).to be_valid_position
- end
- end
-
- it 'deals with there not being any items to the right' do
- create_items_with_positions([1, 2, 3])
- new_item.update!(relative_position: 5)
-
- expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION)
- end
-
- it 'finds the next gap to the right, skipping adjacent values' do
- create_items_with_positions([1, 2, 10])
- new_item.update!(relative_position: 0)
+ items = [*nils, *bunch_a, *bunch_b]
+ items.each(&:reset)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(items.reverse.sort_by(&:relative_position)).to eq(items)
end
- it 'finds the next gap to the right' do
- create_items_with_positions([2, 10])
+ it 'manages to move nulls to the end, stacking if we cannot create enough space' do
+ run = run_at_start(40).to_a
+ bunch = create_items_with_positions(run.select(&:even?))
- new_item.update!(relative_position: 0)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2)
+ nils = create_items_with_positions([nil].cycle.take(20))
+ described_class.move_nulls_to_start(nils)
- new_item.update!(relative_position: 1)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
-
- new_item.update!(relative_position: 3)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10)
+ items = [*nils, *bunch]
+ items.each(&:reset)
- new_item.update!(relative_position: 5)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10)
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch)
+ expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils)
+ expect(bunch.map(&:relative_position)).to all(be > nils.map(&:relative_position).max)
end
end
@@ -384,36 +273,39 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
context 'leap-frogging to the left' do
+ let(:item3) { create(factory, default_params) }
+ let(:start) { RelativePositioning::START_POSITION }
+
before do
- start = RelativePositioning::START_POSITION
item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0)
item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1)
item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2)
end
- let(:item3) { create(factory, default_params) }
+ def leap_frog
+ a, b = [item1.reset, item2.reset].sort_by(&:relative_position)
- def leap_frog(steps)
- a = item1
- b = item2
-
- steps.times do |i|
- a.move_before(b)
- a.save!
- a, b = b, a
- end
+ b.move_before(a)
+ b.save!
end
- it 'can leap-frog STEPS - 1 times before needing to rebalance' do
- # This is less efficient than going right, due to the flooring of
- # integer division
- expect { leap_frog(RelativePositioning::STEPS - 1) }
- .not_to change { item3.reload.relative_position }
+ it 'can leap-frog STEPS times before needing to rebalance' do
+ expect { RelativePositioning::STEPS.times { leap_frog } }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reload.relative_position }.by(be < 0)
+ .and change { item2.reload.relative_position }.by(be < 0)
+
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(be < 0)
end
- it 'rebalances after leap-frogging STEPS times' do
- expect { leap_frog(RelativePositioning::STEPS) }
- .to change { item3.reload.relative_position }
+ context 'there is no space to the left after moving STEPS times' do
+ let(:start) { RelativePositioning::MIN_POSITION + (2 * RelativePositioning::IDEAL_DISTANCE) }
+
+ it 'rebalances to the right' do
+ expect { RelativePositioning::STEPS.succ.times { leap_frog } }
+ .not_to change { item3.reload.relative_position }
+ end
end
end
end
@@ -476,25 +368,25 @@ RSpec.shared_examples 'a class that supports relative positioning' do
let(:item3) { create(factory, default_params) }
- def leap_frog(steps)
- a = item1
- b = item2
+ def leap_frog
+ a, b = [item1.reset, item2.reset].sort_by(&:relative_position)
- steps.times do |i|
- a.move_after(b)
- a.save!
- a, b = b, a
- end
+ a.move_after(b)
+ a.save!
end
- it 'can leap-frog STEPS times before needing to rebalance' do
- expect { leap_frog(RelativePositioning::STEPS) }
- .not_to change { item3.reload.relative_position }
- end
+ it 'rebalances after STEPS jumps' do
+ RelativePositioning::STEPS.pred.times do
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reset.relative_position }.by(be >= 0)
+ .and change { item2.reset.relative_position }.by(be >= 0)
+ end
- it 'rebalances after leap-frogging STEPS+1 times' do
- expect { leap_frog(RelativePositioning::STEPS + 1) }
- .to change { item3.reload.relative_position }
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reset.relative_position }.by(be < 0)
+ .and change { item2.reset.relative_position }.by(be < 0)
end
end
end
@@ -506,12 +398,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ it 'places items at most IDEAL_DISTANCE from the start when the range is open' do
+ n = set_size
+
+ expect([item1, item2].map(&:relative_position)).to all(be >= (RelativePositioning::START_POSITION - (n * RelativePositioning::IDEAL_DISTANCE)))
+ end
+
it 'moves item to the end' do
new_item.move_to_start
expect(new_item.relative_position).to be < item2.relative_position
end
+ it 'positions the item at MIN_POSITION when there is only one space left' do
+ item2.update!(relative_position: RelativePositioning::MIN_POSITION + 1)
+
+ new_item.move_to_start
+
+ expect(new_item.relative_position).to eq RelativePositioning::MIN_POSITION
+ end
+
it 'rebalances when there is already an item at the MIN_POSITION' do
item2.update!(relative_position: RelativePositioning::MIN_POSITION)
@@ -543,12 +449,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ it 'places items at most IDEAL_DISTANCE from the start when the range is open' do
+ n = set_size
+
+ expect([item1, item2].map(&:relative_position)).to all(be <= (RelativePositioning::START_POSITION + (n * RelativePositioning::IDEAL_DISTANCE)))
+ end
+
it 'moves item to the end' do
new_item.move_to_end
expect(new_item.relative_position).to be > item2.relative_position
end
+ it 'positions the item at MAX_POSITION when there is only one space left' do
+ item2.update!(relative_position: RelativePositioning::MAX_POSITION - 1)
+
+ new_item.move_to_end
+
+ expect(new_item.relative_position).to eq RelativePositioning::MAX_POSITION
+ end
+
it 'rebalances when there is already an item at the MAX_POSITION' do
item2.update!(relative_position: RelativePositioning::MAX_POSITION)
@@ -712,63 +632,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
- describe '#move_sequence_before' do
- it 'moves the whole sequence of items to the middle of the nearest gap' do
- items = create_items_with_positions([90, 100, 101, 102])
-
- items.last.move_sequence_before
- items.last.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([90, 95, 96, 102])
- end
-
- it 'raises an error if there is no space' do
- items = create_items_with_positions(run_at_start)
-
- expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft)
- end
-
- it 'finds a gap if there are unused positions' do
- items = create_items_with_positions([100, 101, 102])
-
- items.last.move_sequence_before
- items.last.save!
-
- positions = items.map { |item| item.reload.relative_position }
-
- expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP
- end
- end
-
- describe '#move_sequence_after' do
- it 'moves the whole sequence of items to the middle of the nearest gap' do
- items = create_items_with_positions([100, 101, 102, 110])
-
- items.first.move_sequence_after
- items.first.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([100, 105, 106, 110])
- end
-
- it 'finds a gap if there are unused positions' do
- items = create_items_with_positions([100, 101, 102])
-
- items.first.move_sequence_after
- items.first.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP
- end
-
- it 'raises an error if there is no space' do
- items = create_items_with_positions(run_at_end)
-
- expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft)
- end
- end
-
def be_valid_position
be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION)
end
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
index a5228c43f6f..a1867e1ce39 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
@@ -164,7 +164,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context "event channels" do
it "uses the right channel for push event" do
- chat_service.update(push_channel: "random")
+ chat_service.update!(push_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -172,7 +172,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for merge request event" do
- chat_service.update(merge_request_channel: "random")
+ chat_service.update!(merge_request_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -180,7 +180,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for issue event" do
- chat_service.update(issue_channel: "random")
+ chat_service.update!(issue_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -191,7 +191,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:issue_service_options) { { title: 'Secret', confidential: true } }
it "uses confidential issue channel" do
- chat_service.update(confidential_issue_channel: 'confidential')
+ chat_service.update!(confidential_issue_channel: 'confidential')
expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
@@ -199,7 +199,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to issue channel' do
- chat_service.update(issue_channel: 'fallback_channel')
+ chat_service.update!(issue_channel: 'fallback_channel')
expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
@@ -208,7 +208,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for wiki event" do
- chat_service.update(wiki_page_channel: "random")
+ chat_service.update!(wiki_page_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -221,7 +221,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel" do
- chat_service.update(note_channel: "random")
+ chat_service.update!(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -236,7 +236,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses confidential channel" do
- chat_service.update(confidential_note_channel: "confidential")
+ chat_service.update!(confidential_note_channel: "confidential")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -246,7 +246,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to note channel' do
- chat_service.update(note_channel: "fallback_channel")
+ chat_service.update!(note_channel: "fallback_channel")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
diff --git a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
index fc4f6053bb9..14b851d2828 100644
--- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
+++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'throttled touch' do
describe '#touch' do
it 'updates the updated_at timestamp' do
- Timecop.freeze do
+ freeze_time do
subject.touch
expect(subject.updated_at).to be_like_time(Time.zone.now)
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7f0da19996e..557025569b8 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -105,7 +105,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
project.destroy!
end
@@ -113,7 +113,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
project.destroy!
end
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index a881d5f036c..b87f7fe97e1 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -322,8 +322,8 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
- expect(subject).to receive(:update_container_activity)
+ it 'runs after_wiki_activity callbacks' do
+ expect(subject).to receive(:after_wiki_activity)
subject.create_page('Test Page', 'This is content')
end
@@ -363,10 +363,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
update_page
end
@@ -389,10 +389,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
subject.delete_page(page)
end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index f2a4d9919b7..0c930ec1fce 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
it 'deletes remote uploads' do
expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
- expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-1)
end
end
@@ -21,13 +21,13 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: model_object) }
it 'deletes any FileUploader uploads which are not mounted' do
- expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-3)
end
it 'deletes local files' do
expect_any_instance_of(Uploads::Local).to receive(:delete_keys).with(uploads.map(&:absolute_path))
- model_object.destroy
+ model_object.destroy!
end
end
@@ -35,14 +35,14 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: model_object) }
it 'deletes any FileUploader uploads which are not mounted' do
- expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-3)
end
it 'deletes remote files' do
expected_array = array_including(*uploads.map(&:path))
expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(expected_array)
- model_object.destroy
+ model_object.destroy!
end
end
end
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
index ff55bc9a490..39c7c1f2a94 100644
--- a/spec/support/shared_examples/path_extraction_shared_examples.rb
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -146,20 +146,6 @@ RSpec.shared_examples 'extracts refs' do
expect(extract_ref('release/app/doc/README.md')).to eq(['release/app', 'doc/README.md'])
end
-
- context 'when the extracts_path_optimization feature flag is disabled' do
- before do
- stub_feature_flags(extracts_path_optimization: false)
- end
-
- it 'always fetches all ref names' do
- expect(self).to receive(:ref_names).and_call_original
- expect(container.repository).not_to receive(:branch_names_include?)
- expect(container.repository).not_to receive(:tag_names_include?)
-
- expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md'])
- end
- end
end
context 'when the repository has ambiguous refs' do
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index d8476f5dcc2..d05e5eb9120 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -59,8 +59,7 @@ RSpec.shared_examples 'project policies as anonymous' do
let(:project) { create(:project, :public, namespace: group) }
let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
let(:anonymous_permissions) { guest_permissions - user_permissions }
-
- subject { described_class.new(nil, project) }
+ let(:current_user) { anonymous }
before do
create(:group_member, :invited, group: group)
@@ -78,9 +77,8 @@ RSpec.shared_examples 'project policies as anonymous' do
end
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(nil, project) }
+ let(:project) { private_project }
+ let(:current_user) { anonymous }
it { is_expected.to be_banned }
end
@@ -109,10 +107,10 @@ RSpec.shared_examples 'deploy token does not get confused with user' do
end
RSpec.shared_examples 'project policies as guest' do
- subject { described_class.new(guest, project) }
-
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { private_project }
+ let(:current_user) { guest }
+
let(:reporter_public_build_permissions) do
reporter_permissions - [:read_build, :read_pipeline]
end
@@ -167,9 +165,8 @@ end
RSpec.shared_examples 'project policies as reporter' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(reporter, project) }
+ let(:project) { private_project }
+ let(:current_user) { reporter }
it do
expect_allowed(*guest_permissions)
@@ -192,9 +189,8 @@ end
RSpec.shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(developer, project) }
+ let(:project) { private_project }
+ let(:current_user) { developer }
it do
expect_allowed(*guest_permissions)
@@ -217,9 +213,8 @@ end
RSpec.shared_examples 'project policies as maintainer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(maintainer, project) }
+ let(:project) { private_project }
+ let(:current_user) { maintainer }
it do
expect_allowed(*guest_permissions)
@@ -242,9 +237,8 @@ end
RSpec.shared_examples 'project policies as owner' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(owner, project) }
+ let(:project) { private_project }
+ let(:current_user) { owner }
it do
expect_allowed(*guest_permissions)
@@ -267,9 +261,8 @@ end
RSpec.shared_examples 'project policies as admin with admin mode' do
context 'abilities for non-public projects', :enable_admin_mode do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(admin, project) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it do
expect_allowed(*guest_permissions)
@@ -316,9 +309,8 @@ end
RSpec.shared_examples 'project policies as admin without admin mode' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(admin, project) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it { is_expected.to be_banned }
diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
index 9bfd1e6faa0..e94d29febfb 100644
--- a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-# Shared examples to that test code that creates AwardEmoji also mark Todos
-# as done.
+# Shared examples to test that the code that creates AwardEmoji also marks
+# ToDos as done.
#
# The examples expect these to be defined in the calling spec:
# - `subject` the callable code that executes the creation of an AwardEmoji
# - `user`
# - `project`
+#
RSpec.shared_examples 'creating award emojis marks Todos as done' do
using RSpec::Parameterized::TableSyntax
@@ -22,7 +23,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do
with_them do
let(:project) { awardable.project }
- let(:awardable) { create(type) }
+ let(:awardable) { create(type) } # rubocop:disable Rails/SaveBang
let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
specify do
diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb
index 20b0f4f0dd2..0096aab55e3 100644
--- a/spec/support/shared_examples/requests/api/boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb
@@ -169,7 +169,7 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals
before do
if board_parent.try(:namespace)
- board_parent.update(namespace: owner.namespace)
+ board_parent.update!(namespace: owner.namespace)
else
board.resource_parent.add_owner(owner)
end
diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
index 09743c20fba..5c122b4b5d6 100644
--- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
@@ -16,8 +16,11 @@ RSpec.shared_examples 'Composer package index' do |user_type, status, add_member
subject
expect(response).to have_gitlab_http_status(status)
- expect(response).to match_response_schema('public_api/v4/packages/composer/index')
- expect(json_response).to eq presenter.root
+
+ if status == :success
+ expect(response).to match_response_schema('public_api/v4/packages/composer/index')
+ expect(json_response).to eq presenter.root
+ end
end
end
end
@@ -87,13 +90,22 @@ RSpec.shared_examples 'process Composer api request' do |user_type, status, add_
end
end
-RSpec.shared_context 'Composer auth headers' do |user_role, user_token|
+RSpec.shared_context 'Composer auth headers' do |user_role, user_token, auth_method = :token|
let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ let(:headers) do
+ if user_role == :anonymous
+ {}
+ elsif auth_method == :token
+ { 'Private-Token' => token }
+ else
+ basic_auth_header(user.username, token)
+ end
+ end
end
-RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token|
- include_context 'Composer auth headers', user_role, user_token do
+RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token, auth_method|
+ include_context 'Composer auth headers', user_role, user_token, auth_method do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
new file mode 100644
index 00000000000..c56290a0aa9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -0,0 +1,843 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'conan ping endpoint' do
+ it 'responds with 401 Unauthorized when no token provided' do
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 200 OK when valid token is provided' do
+ jwt = build_jwt(personal_access_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid job token is provided' do
+ jwt = build_jwt_from_job(job)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid deploy token is provided' do
+ jwt = build_jwt_from_deploy_token(deploy_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 401 Unauthorized when invalid access token ID is provided' do
+ jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid user is provided' do
+ jwt = build_jwt(personal_access_token, user_id: 12345)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
+ jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid JWT is provided' do
+ get api(url), headers: build_token_auth_header('invalid-jwt')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'packages feature disabled' do
+ it 'responds with 404 Not Found' do
+ stub_packages_setting(enabled: false)
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan search endpoint' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+
+ get api(url), headers: headers, params: params
+ end
+
+ subject { json_response['results'] }
+
+ context 'returns packages with a matching name' do
+ let(:params) { { q: package.conan_recipe } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'returns packages using a * wildcard' do
+ let(:params) { { q: "#{package.name[0, 3]}*" } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'does not return non-matching packages' do
+ let(:params) { { q: "foo" } }
+
+ it { is_expected.to be_blank }
+ end
+end
+
+RSpec.shared_examples 'conan authenticate endpoint' do
+ subject { get api(url), headers: headers }
+
+ context 'when using invalid token' do
+ let(:auth_token) { 'invalid_token' }
+
+ it 'responds with 401' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when valid JWT access token is provided' do
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'token has valid validity time' do
+ freeze_time do
+ subject
+
+ payload = JSONWebToken::HMACToken.decode(
+ response.body, jwt_secret).first
+ expect(payload['access_token']).to eq(personal_access_token.id)
+ expect(payload['user_id']).to eq(personal_access_token.user_id)
+
+ duration = payload['exp'] - payload['iat']
+ expect(duration).to eq(1.hour)
+ end
+ end
+ end
+
+ context 'with valid job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with valid deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan check_credentials endpoint' do
+ it 'responds with a 200 OK with PAT' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'with job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'responds with a 401 Unauthorized when an invalid token is used' do
+ get api(url), headers: build_token_auth_header('invalid-token')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+end
+
+RSpec.shared_examples 'rejects invalid recipe' do
+ context 'with invalid recipe path' do
+ let(:recipe_path) { '../../foo++../..' }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name|
+ let(:file_name) { invalid_file_name }
+
+ context 'with invalid file_name' do
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects recipe for invalid project' do
+ context 'with invalid project' do
+ let(:recipe_path) { 'aa/bb/cc/dd' }
+ let(:project_id) { 9999 }
+
+ it_behaves_like 'not found request'
+ end
+end
+
+RSpec.shared_examples 'empty recipe for not found package' do
+ context 'with invalid recipe url' do
+ let(:recipe_path) do
+ 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ end
+
+ it 'returns not found' do
+ allow(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(
+ nil,
+ user,
+ project,
+ any_args
+ ).and_return(presenter)
+ allow(presenter).to receive(:recipe_snapshot) { {} }
+ allow(presenter).to receive(:package_snapshot) { {} }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq("{}")
+ end
+ end
+end
+
+RSpec.shared_examples 'not selecting a package with the wrong type' do
+ context 'with a nuget package with same name and version' do
+ let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) }
+ let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" }
+
+ it 'calls the presenter with a nil package' do
+ expect(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(nil, user, project, any_args)
+
+ subject
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the recipe files' do
+ expected_response = {
+ 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ allow(presenter).to receive(:recipe_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'package download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the package files' do
+ expected_response = {
+ 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ allow(presenter).to receive(:package_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'rejects invalid upload_url params' do
+ context 'with unaccepted json format' do
+ let(:params) { %w[foo bar] }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'successful response when using Unicorn' do
+ context 'on Unicorn', :unicorn do
+ it 'returns successfully' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of files with their md5 hashes' do
+ expected_response = {
+ 'conanfile.py' => 'md5hash1',
+ 'conanmanifest.txt' => 'md5hash2'
+ }
+
+ allow(presenter).to receive(:recipe_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'package snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of md5 values for the files' do
+ expected_response = {
+ 'conaninfo.txt' => "md5hash1",
+ 'conanmanifest.txt' => "md5hash2",
+ 'conan_package.tgz' => "md5hash3"
+ }
+
+ allow(presenter).to receive(:package_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'recipe download_urls'
+end
+
+RSpec.shared_examples 'package download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'package download_urls'
+end
+
+RSpec.shared_examples 'recipe upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conanfile.py': 24,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ subject
+
+ expected_response = {
+ 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with conan_sources and conan_export files' do
+ let(:params) do
+ { 'conan_sources.tgz': 345,
+ 'conan_export.tgz': 234,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'returns upload urls for the additional files' do
+ subject
+
+ expected_response = {
+ 'conan_sources.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
+ 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+
+ context 'with an invalid file' do
+ let(:params) do
+ { 'invalid_file.txt': 10,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'does not return the invalid file as an upload_url' do
+ subject
+
+ expected_response = {
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'package upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'conanmanifest.txt': 123,
+ 'conan_package.tgz': 523 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with invalid files' do
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'invalid_file.txt': 10 }
+ end
+
+ it 'returns upload urls only for the valid requested files' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'delete package endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it_behaves_like 'rejects invalid recipe'
+
+ it 'returns unauthorized for users without valid permission' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'with delete permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package'
+
+ it 'deletes a package' do
+ expect { subject }.to change { Packages::Package.count }.from(2).to(1)
+ end
+ end
+end
+
+RSpec.shared_examples 'denies download with no token' do
+ context 'with no private token' do
+ let(:headers) { {} }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+end
+
+RSpec.shared_examples 'a public project with packages' do
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'an internal project with packages' do
+ before do
+ project.team.truncate
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'a private project with packages' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it 'denies download when not enough permissions' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+end
+
+RSpec.shared_examples 'not found request' do
+ it 'returns not found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+end
+
+RSpec.shared_examples 'recipe file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+end
+
+RSpec.shared_examples 'package file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+
+ context 'tracking the conan_package.tgz download' do
+ let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package'
+ end
+end
+
+RSpec.shared_examples 'project not found by recipe' do
+ let(:recipe_path) { 'not/package/for/project' }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'project not found by project id' do
+ let(:project_id) { 99999 }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'workhorse authorize endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'workhorse authorization'
+end
+
+RSpec.shared_examples 'workhorse recipe file upload endpoint' do
+ let(:file_name) { 'conanfile.py' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'uploads a package file'
+end
+
+RSpec.shared_examples 'workhorse package file upload endpoint' do
+ let(:file_name) { 'conaninfo.txt' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
+ it_behaves_like 'uploads a package file'
+
+ context 'tracking the conan_package.tgz upload' do
+ let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package'
+ end
+end
+
+RSpec.shared_examples 'uploads a package file' do
+ context 'file size above maximum limit' do
+ before do
+ params['file.size'] = project.actual_limits.conan_max_file_size + 1
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with object storage disabled' do
+ context 'without a file from workhorse' do
+ let(:params) { { file: nil } }
+
+ it 'rejects the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with a file' do
+ it_behaves_like 'package workhorse uploads'
+ end
+
+ context 'without a token' do
+ it 'rejects request without a token' do
+ headers_with_token.delete('HTTP_AUTHORIZATION')
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when params from workhorse are correct' do
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ end
+
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => remote_id
+ }
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => file_name
+ }
+ end
+
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ expect(package_file.file.read).to eq('content')
+ end
+ end
+ end
+
+ it_behaves_like 'background upload schedules a file migration'
+ end
+end
+
+RSpec.shared_examples 'workhorse authorization' do
+ it 'authorizes posting package with a valid token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'rejects request without a valid token' do
+ headers_with_token['HTTP_AUTHORIZATION'] = 'foo'
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'rejects request without a valid permission' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'rejects requests that bypassed gitlab-workhorse' do
+ headers_with_token.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it 'responds with status 200, location of package remote store and object details' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response).not_to have_key('TempPath')
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).not_to have_key('MultipartUpload')
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index 8cbf11b6de1..f31cbcfdec1 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
- let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' }
- let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
+ let!(:custom_attribute1) { attributable.custom_attributes.create! key: 'foo', value: 'foo' }
+ let!(:custom_attribute2) { attributable.custom_attributes.create! key: 'bar', value: 'bar' }
describe "GET /#{attributable_name} with custom attributes filter" do
before do
@@ -14,8 +14,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
- expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id
+ expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id)
end
end
@@ -40,7 +39,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
end
end
@@ -50,16 +49,15 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
- expect(json_response.second).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
get api("/#{attributable_name}", admin), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
attributable_response = json_response.find { |r| r['id'] == attributable.id }
other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id }
@@ -132,7 +130,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
context 'with an authorized user' do
- it'returns a single custom attribute' do
+ it 'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
index 48824a4b0d2..62dbac3fd4d 100644
--- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
@@ -8,3 +8,42 @@ RSpec.shared_examples 'when the snippet is not found' do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
+
+RSpec.shared_examples 'snippet edit usage data counters' do
+ context 'when user is sessionless' do
+ it 'does not track usage data actions' do
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when user is not sessionless' do
+ before do
+ session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
+ session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
+ end
+
+ cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ end
+
+ it 'tracks usage data actions', :clean_gitlab_redis_shared_state do
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation)
+ end
+
+ context 'when mutation result raises an error' do
+ it 'does not track usage data actions' do
+ mutation_vars[:title] = nil
+
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index fcdc594f258..6aac51a5903 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -175,7 +175,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with object storage enabled' do
let(:tmp_object) do
- fog_connection.directories.new(key: 'packages').files.create(
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 6f4a0236b66..c9a33701161 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -41,3 +41,88 @@ RSpec.shared_examples 'deploy token for package uploads' do
end
end
end
+
+RSpec.shared_examples 'does not cause n^2 queries' do
+ it 'avoids N^2 database queries' do
+ # we create a package to set the baseline for expected queries from 1 package
+ create(
+ :npm_package,
+ name: "@#{project.root_namespace.path}/my-package",
+ project: project,
+ version: "0.0.1"
+ )
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api(url)
+ end
+
+ 5.times do |n|
+ create(
+ :npm_package,
+ name: "@#{project.root_namespace.path}/my-package",
+ project: project,
+ version: "#{n}.0.0"
+ )
+ end
+
+ expect do
+ get api(url)
+ end.not_to exceed_query_limit(control)
+ end
+end
+
+RSpec.shared_examples 'job token for package GET requests' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+end
+
+RSpec.shared_examples 'job token for package uploads' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ 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 4954151b93b..715c494840e 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
@@ -58,7 +58,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member
context 'with object storage enabled' do
let(:tmp_object) do
- fog_connection.directories.new(key: 'packages').files.create(
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)
diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
index cfbb84dd099..051367fbe96 100644
--- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
@@ -77,3 +77,142 @@ RSpec.shared_examples 'raw snippet files' do
end
end
end
+
+RSpec.shared_examples 'snippet file updates' do
+ let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
+ let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
+ let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
+ let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
+ let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
+ let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
+ let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
+
+ context 'with various snippet file changes' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:is_multi_file, :file_name, :content, :files, :status) do
+ true | nil | nil | [create_action] | :success
+ true | nil | nil | [update_action] | :success
+ true | nil | nil | [move_action] | :success
+ true | nil | nil | [delete_action] | :success
+ true | nil | nil | [create_action, update_action] | :success
+ true | 'foo.txt' | 'bar' | [create_action] | :bad_request
+ true | 'foo.txt' | 'bar' | nil | :bad_request
+ true | nil | nil | nil | :bad_request
+ true | 'foo.txt' | nil | [create_action] | :bad_request
+ true | nil | 'bar' | [create_action] | :bad_request
+ true | '' | nil | [create_action] | :bad_request
+ true | nil | '' | [create_action] | :bad_request
+ true | nil | nil | [bad_file_path] | :bad_request
+ true | nil | nil | [bad_previous_path] | :bad_request
+ true | nil | nil | [invalid_move] | :unprocessable_entity
+
+ false | 'foo.txt' | 'bar' | nil | :success
+ false | 'foo.txt' | nil | nil | :success
+ false | nil | 'bar' | nil | :success
+ false | 'foo.txt' | 'bar' | [create_action] | :bad_request
+ false | nil | nil | nil | :bad_request
+ false | nil | '' | nil | :bad_request
+ false | nil | nil | [bad_file_path] | :bad_request
+ false | nil | nil | [bad_previous_path] | :bad_request
+ end
+
+ with_them do
+ before do
+ allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
+ end
+
+ it 'has the correct response' do
+ update_params = {}.tap do |params|
+ params[:files] = files if files
+ params[:file_name] = file_name if file_name
+ params[:content] = content if content
+ end
+
+ update_snippet(params: update_params)
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ context 'when save fails due to a repository commit error' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
+ end
+
+ update_snippet(params: { files: [create_action] })
+ end
+
+ it 'returns a bad request response' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'snippet non-file updates' do
+ it 'updates a snippet non-file attributes' do
+ new_description = 'New description'
+ new_title = 'New title'
+ new_visibility = 'internal'
+
+ update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
+
+ snippet.reload
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(snippet.description).to eq(new_description)
+ expect(snippet.visibility).to eq(new_visibility)
+ expect(snippet.title).to eq(new_title)
+ end
+ end
+end
+
+RSpec.shared_examples 'snippet individual non-file updates' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:attribute, :updated_value) do
+ :description | 'new description'
+ :title | 'new title'
+ :visibility | 'private'
+ end
+
+ with_them do
+ it 'updates the attribute' do
+ params = { attribute => updated_value }
+
+ expect { update_snippet(params: params) }
+ .to change { snippet.reload.send(attribute) }.to(updated_value)
+ end
+ end
+end
+
+RSpec.shared_examples 'invalid snippet updates' do
+ it 'returns 404 for invalid snippet id' do
+ update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ update_snippet
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 if content is blank' do
+ update_snippet(params: { content: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 if title is blank' do
+ update_snippet(params: { title: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
+ end
+end
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index a17163328f4..84ef7723b9b 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -2,6 +2,10 @@
RSpec.shared_examples 'update with repository actions' do
context 'when the repository exists' do
+ before do
+ allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false)
+ end
+
it 'commits the changes to the repository' do
existing_blob = snippet.blobs.first
new_file_name = existing_blob.path + '_new'
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
index 1ef08de31a9..7608f1c7f8a 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
@@ -57,16 +57,6 @@ RSpec.shared_examples 'diff file entity' do
expect(subject).to include(:highlighted_diff_lines)
end
end
-
- context 'when the `single_mr_diff_view` feature is disabled' do
- before do
- stub_feature_flags(single_mr_diff_view: false)
- end
-
- it 'contains both kinds of diffs' do
- expect(subject).to include(:highlighted_diff_lines, :parallel_diff_lines)
- end
- end
end
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 a1354a8099b..1ae74979b7a 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -39,3 +39,41 @@ RSpec.shared_examples 'adds an alert management alert event' do
subject
end
end
+
+RSpec.shared_examples 'processes incident issues' do
+ let(:create_incident_service) { spy }
+
+ before do
+ allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services)
+ end
+
+ it 'processes issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .to receive(:perform_async)
+ .with(nil, nil, kind_of(Integer))
+ .once
+
+ Sidekiq::Testing.inline! do
+ expect(subject).to be_success
+ end
+ end
+end
+
+RSpec.shared_examples 'does not process incident issues' do
+ it 'does not process issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .not_to receive(:perform_async)
+
+ expect(subject).to be_success
+ end
+end
+
+RSpec.shared_examples 'does not process incident issues due to error' do |http_status:|
+ it 'does not process issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .not_to receive(:perform_async)
+
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(http_status)
+ end
+end
diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
index 20856b05de6..5b95a5753a1 100644
--- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text|
before do
issuable.assign_attributes(update_params)
- issuable.save
+ issuable.save!
end
it 'creates 1 system note with the correct content' do
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
new file mode 100644
index 00000000000..d6e79931df5
--- /dev/null
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - issue (required)
+#
+# Usage:
+#
+# it_behaves_like 'incident issue' do
+# let(:issue) { ... }
+# end
+#
+# include_examples 'incident issue'
+RSpec.shared_examples 'incident issue' do
+ let(:label_properties) { attributes_for(:label, :incident) }
+
+ it 'has incident as issue type' do
+ expect(issue.issue_type).to eq('incident')
+ end
+
+ it 'has exactly one incident label' do
+ expect(issue.labels).to be_one do |label|
+ label.slice(*label_properties.keys).symbolize_keys == label_properties
+ end
+ end
+end
+
+# This shared_example requires the following variables:
+# - issue (required)
+#
+# Usage:
+#
+# it_behaves_like 'not an incident issue' do
+# let(:issue) { ... }
+# end
+#
+# include_examples 'not an incident issue'
+RSpec.shared_examples 'not an incident issue' do
+ let(:label_properties) { attributes_for(:label, :incident) }
+
+ it 'has not incident as issue type' do
+ expect(issue.issue_type).not_to eq('incident')
+ end
+
+ it 'has not an incident label' do
+ expect(issue.labels).not_to include(have_attributes(label_properties))
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb
index 9eb66e33513..47c7a1e7356 100644
--- a/spec/support/shared_examples/services/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_shared_examples.rb
@@ -8,37 +8,6 @@ RSpec.shared_examples 'cache counters invalidator' do
end
end
-RSpec.shared_examples 'system notes for milestones' do
- def update_issuable(opts)
- issuable = try(:issue) || try(:merge_request)
- described_class.new(project, user, opts).execute(issuable)
- end
-
- context 'group milestones' do
- let(:group) { create(:group) }
- let(:group_milestone) { create(:milestone, group: group) }
-
- before do
- project.update(namespace: group)
- create(:group_member, group: group, user: user)
- end
-
- it 'creates a system note' do
- expect do
- update_issuable(milestone: group_milestone)
- end.to change { Note.system.count }.by(1)
- end
- end
-
- context 'project milestones' do
- it 'creates a system note' do
- expect do
- update_issuable(milestone: create(:milestone, project: project))
- end.to change { Note.system.count }.by(1)
- end
- end
-end
-
RSpec.shared_examples 'updating a single task' do
def update_issuable(opts)
issuable = try(:issue) || try(:merge_request)
diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb
new file mode 100644
index 00000000000..a7032640217
--- /dev/null
+++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reviewer_ids filter' do
+ context 'filter_reviewer' do
+ let(:opts) { super().merge(reviewer_ids_param) }
+
+ context 'without reviewer_ids' do
+ let(:reviewer_ids_param) { {} }
+
+ it 'contains no reviewer_ids' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+
+ context 'with reviewer_ids' do
+ let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.id, reviewer3.id] } }
+
+ let(:reviewer1) { create(:user) }
+ let(:reviewer2) { create(:user) }
+ let(:reviewer3) { create(:user) }
+
+ context 'when the current user can admin the merge_request' do
+ context 'when merge_request_reviewer feature is enabled' do
+ before do
+ stub_feature_flags(merge_request_reviewer: true)
+ end
+
+ context 'with reviewers who can read the merge_request' do
+ before do
+ project.add_developer(reviewer1)
+ project.add_developer(reviewer2)
+ end
+
+ it 'contains reviewers who can read the merge_request' do
+ expect(execute.reviewers).to contain_exactly(reviewer1, reviewer2)
+ end
+ end
+ end
+
+ context 'when merge_request_reviewer feature is disabled' do
+ before do
+ stub_feature_flags(merge_request_reviewer: false)
+ end
+
+ it 'contains no reviewers' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+ end
+
+ context 'when the current_user cannot admin the merge_request' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains no reviewers' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 45a4c2bb151..7fd59c3d963 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -14,6 +14,14 @@ RSpec.shared_examples 'assigns build to package' do
end
end
+RSpec.shared_examples 'assigns the package creator' do
+ it 'assigns the package creator' do
+ subject
+
+ expect(package.creator).to eq user
+ end
+end
+
RSpec.shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do
before do
@@ -161,6 +169,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package4) { create(:nuget_package, project: project) }
let_it_be(:package5) { create(:pypi_package, project: project) }
let_it_be(:package6) { create(:composer_package, project: project) }
+ let_it_be(:package7) { create(:generic_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 51a4a8b1cd9..4a08c0d4365 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -40,3 +40,20 @@ RSpec.shared_examples 'snippets spam check is performed' do
end
end
end
+
+shared_examples 'invalid params error response' do
+ before do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:valid_params?).and_return false
+ end
+ end
+
+ it 'responds to errors appropriately' do
+ response = subject
+
+ aggregate_failures do
+ expect(response).to be_error
+ expect(response.http_status).to eq 422
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
index db1b50fdf3c..ffdd0c36cfc 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
- let(:container) { create(container_type) }
+ let(:container) { create(container_type) } # rubocop:disable Rails/SaveBang
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
@@ -32,9 +32,19 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
)
end
- it 'does not increment the delete count if the deletion failed' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
+ context 'when the deletion fails' do
+ before do
+ expect(page).to receive(:delete).and_return(false)
+ end
+
+ it 'returns an error response' do
+ response = service.execute(page)
+ expect(response).to be_error
+ end
- expect { service.execute(nil) }.not_to change { counter.read(:delete) }
+ it 'does not increment the delete count if the deletion failed' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+ expect { service.execute(page) }.not_to change { counter.read(:delete) }
+ end
end
end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index 0191a6dfbc9..fd10dd4367e 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -19,8 +19,10 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
it 'updates the wiki page' do
- updated_page = service.execute(page)
+ response = service.execute(page)
+ updated_page = response.payload[:page]
+ expect(response).to be_success
expect(updated_page).to be_valid
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
@@ -81,7 +83,11 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
end
it 'reports the error' do
- expect(service.execute(page)).to be_invalid
+ response = service.execute(page)
+ page = response.payload[:page]
+
+ expect(response).to be_error
+ expect(page).to be_invalid
.and have_attributes(errors: be_present)
end
end
diff --git a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
index f143cbc7165..5a9a3dfc2d2 100644
--- a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
@@ -63,7 +63,7 @@ RSpec.shared_examples 'uploads migration worker' do
if success > 0
it 'outputs the reports' do
- expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+ expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
perform(uploads)
end
@@ -71,7 +71,7 @@ RSpec.shared_examples 'uploads migration worker' do
if failures > 0
it 'outputs upload failures' do
- expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
perform(uploads)
end