diff options
Diffstat (limited to 'spec/support')
69 files changed, 1168 insertions, 377 deletions
diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml index d05812a64eb..d6e74349069 100644 --- a/spec/support/database/cross-database-modification-allowlist.yml +++ b/spec/support/database/cross-database-modification-allowlist.yml @@ -1,90 +1,31 @@ -- "./ee/spec/controllers/projects/settings/access_tokens_controller_spec.rb" -- "./ee/spec/lib/gitlab/ci/templates/Jobs/dast_default_branch_gitlab_ci_yaml_spec.rb" - "./ee/spec/mailers/notify_spec.rb" -- "./ee/spec/models/ci/bridge_spec.rb" -- "./ee/spec/models/ci/build_spec.rb" -- "./ee/spec/models/ci/minutes/additional_pack_spec.rb" -- "./ee/spec/models/ee/ci/job_artifact_spec.rb" - "./ee/spec/models/group_member_spec.rb" -- "./ee/spec/replicators/geo/pipeline_artifact_replicator_spec.rb" - "./ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb" -- "./ee/spec/services/ci/destroy_pipeline_service_spec.rb" - "./ee/spec/services/ci/retry_build_service_spec.rb" -- "./ee/spec/services/ci/subscribe_bridge_service_spec.rb" -- "./ee/spec/services/deployments/auto_rollback_service_spec.rb" -- "./ee/spec/services/ee/ci/job_artifacts/destroy_all_expired_service_spec.rb" -- "./ee/spec/services/ee/users/destroy_service_spec.rb" -- "./ee/spec/services/projects/transfer_service_spec.rb" -- "./ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb" - "./spec/controllers/abuse_reports_controller_spec.rb" -- "./spec/controllers/admin/spam_logs_controller_spec.rb" -- "./spec/controllers/admin/users_controller_spec.rb" - "./spec/controllers/omniauth_callbacks_controller_spec.rb" - "./spec/controllers/projects/issues_controller_spec.rb" -- "./spec/controllers/projects/pipelines_controller_spec.rb" -- "./spec/controllers/projects/settings/access_tokens_controller_spec.rb" - "./spec/features/issues/issue_detail_spec.rb" - "./spec/features/projects/pipelines/pipeline_spec.rb" - "./spec/features/signed_commits_spec.rb" - "./spec/helpers/issuables_helper_spec.rb" - "./spec/lib/gitlab/auth_spec.rb" - "./spec/lib/gitlab/ci/pipeline/chain/create_spec.rb" -- "./spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb" -- "./spec/lib/gitlab/ci/pipeline/seed/build_spec.rb" -- "./spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb" -- "./spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb" -- "./spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb" -- "./spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb" -- "./spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb" - "./spec/lib/gitlab/email/handler/create_issue_handler_spec.rb" - "./spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb" - "./spec/lib/gitlab/email/handler/create_note_handler_spec.rb" - "./spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb" -- "./spec/lib/peek/views/active_record_spec.rb" -- "./spec/models/ci/build_need_spec.rb" - "./spec/models/ci/build_trace_chunk_spec.rb" -- "./spec/models/ci/group_variable_spec.rb" - "./spec/models/ci/job_artifact_spec.rb" -- "./spec/models/ci/job_variable_spec.rb" -- "./spec/models/ci/pipeline_spec.rb" - "./spec/models/ci/runner_spec.rb" -- "./spec/models/ci/variable_spec.rb" - "./spec/models/clusters/applications/runner_spec.rb" -- "./spec/models/commit_status_spec.rb" -- "./spec/models/concerns/batch_destroy_dependent_associations_spec.rb" -- "./spec/models/concerns/bulk_insertable_associations_spec.rb" -- "./spec/models/concerns/has_environment_scope_spec.rb" -- "./spec/models/concerns/token_authenticatable_spec.rb" - "./spec/models/design_management/version_spec.rb" - "./spec/models/hooks/system_hook_spec.rb" - "./spec/models/members/project_member_spec.rb" -- "./spec/models/spam_log_spec.rb" - "./spec/models/user_spec.rb" - "./spec/models/user_status_spec.rb" -- "./spec/requests/api/ci/pipeline_schedules_spec.rb" -- "./spec/requests/api/ci/pipelines_spec.rb" -- "./spec/requests/api/commit_statuses_spec.rb" - "./spec/requests/api/commits_spec.rb" -- "./spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb" -- "./spec/requests/api/resource_access_tokens_spec.rb" -- "./spec/requests/api/users_spec.rb" -- "./spec/services/ci/create_pipeline_service/environment_spec.rb" -- "./spec/services/ci/create_pipeline_service_spec.rb" -- "./spec/services/ci/destroy_pipeline_service_spec.rb" -- "./spec/services/ci/ensure_stage_service_spec.rb" -- "./spec/services/ci/expire_pipeline_cache_service_spec.rb" -- "./spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb" -- "./spec/services/ci/job_artifacts/destroy_associations_service_spec.rb" -- "./spec/services/ci/pipeline_bridge_status_service_spec.rb" -- "./spec/services/ci/pipelines/add_job_service_spec.rb" - "./spec/services/ci/retry_build_service_spec.rb" -- "./spec/services/groups/transfer_service_spec.rb" -- "./spec/services/projects/destroy_service_spec.rb" - "./spec/services/projects/overwrite_project_service_spec.rb" -- "./spec/services/projects/transfer_service_spec.rb" -- "./spec/services/resource_access_tokens/revoke_service_spec.rb" -- "./spec/services/users/destroy_service_spec.rb" -- "./spec/services/users/reject_service_spec.rb" - "./spec/workers/merge_requests/create_pipeline_worker_spec.rb" -- "./spec/workers/remove_expired_members_worker_spec.rb" - "./spec/workers/repository_cleanup_worker_spec.rb" diff --git a/spec/support/database/multiple_databases.rb b/spec/support/database/multiple_databases.rb index 9e72ea589e3..94857b47127 100644 --- a/spec/support/database/multiple_databases.rb +++ b/spec/support/database/multiple_databases.rb @@ -6,6 +6,10 @@ module Database skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci) end + def skip_if_multiple_databases_are_setup + skip 'Skipping because multiple databases are set up' if Gitlab::Database.has_config?(:ci) + end + def reconfigure_db_connection(name: nil, config_hash: {}, model: ActiveRecord::Base, config_model: nil) db_config = (config_model || model).connection_db_config @@ -46,6 +50,26 @@ module Database new_handler&.clear_all_connections! end # rubocop:enable Database/MultipleDatabases + + def with_added_ci_connection + if Gitlab::Database.has_config?(:ci) + # No need to add a ci: connection if we already have one + yield + else + with_reestablished_active_record_base(reconnect: true) do + reconfigure_db_connection( + name: :ci, + model: Ci::ApplicationRecord, + config_model: ActiveRecord::Base + ) + + yield + + # Cleanup connection_specification_name for Ci::ApplicationRecord + Ci::ApplicationRecord.remove_connection + end + end + end end module ActiveRecordBaseEstablishConnection @@ -69,18 +93,9 @@ RSpec.configure do |config| end end - config.around(:each, :mocked_ci_connection) do |example| - with_reestablished_active_record_base(reconnect: true) do - reconfigure_db_connection( - name: :ci, - model: Ci::ApplicationRecord, - config_model: ActiveRecord::Base - ) - + config.around(:each, :add_ci_connection) do |example| + with_added_ci_connection do example.run - - # Cleanup connection_specification_name for Ci::ApplicationRecord - Ci::ApplicationRecord.remove_connection end end end diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb index e69374fbc70..42c69a26788 100644 --- a/spec/support/database/prevent_cross_joins.rb +++ b/spec/support/database/prevent_cross_joins.rb @@ -31,9 +31,13 @@ module Database # See https://gitlab.com/gitlab-org/gitlab/-/issues/339396 return if sql.include?("DISABLE TRIGGER") || sql.include?("ENABLE TRIGGER") - # PgQuery might fail in some cases due to limited nesting: - # https://github.com/pganalyze/pg_query/issues/209 - tables = PgQuery.parse(sql).tables + tables = begin + PgQuery.parse(sql).tables + rescue PgQuery::ParseError + # PgQuery might fail in some cases due to limited nesting: + # https://github.com/pganalyze/pg_query/issues/209 + return + end schemas = ::Gitlab::Database::GitlabSchema.table_schemas(tables) diff --git a/spec/support/database/query_analyzer.rb b/spec/support/database/query_analyzer.rb index 85fa55f81ef..6d6627d54b9 100644 --- a/spec/support/database/query_analyzer.rb +++ b/spec/support/database/query_analyzer.rb @@ -4,11 +4,15 @@ # can be disabled selectively RSpec.configure do |config| - config.around do |example| + config.before do |example| if example.metadata.fetch(:query_analyzers, true) - ::Gitlab::Database::QueryAnalyzer.instance.within { example.run } - else - example.run + ::Gitlab::Database::QueryAnalyzer.instance.begin! + end + end + + config.after do |example| + if example.metadata.fetch(:query_analyzers, true) + ::Gitlab::Database::QueryAnalyzer.instance.end! end end end diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb index 30a064d8705..0c211af695d 100644 --- a/spec/support/flaky_tests.rb +++ b/spec/support/flaky_tests.rb @@ -11,7 +11,7 @@ RSpec.configure do |config| raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty? raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']) - RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data["example_id"] } + RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data.to_h[:example_id] } rescue => e # rubocop:disable Style/RescueStandardError puts e [] diff --git a/spec/support/frontend_fixtures.rb b/spec/support/frontend_fixtures.rb new file mode 100644 index 00000000000..5587d9059dd --- /dev/null +++ b/spec/support/frontend_fixtures.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +return unless ENV['CI'] +return unless ENV['GENERATE_FRONTEND_FIXTURES_MAPPING'] == 'true' + +RSpec.configure do |config| + config.before(:suite) do + $fixtures_mapping = Hash.new { |h, k| h[k] = [] } # rubocop:disable Style/GlobalVars + end + + config.after(:suite) do + next unless ENV['FRONTEND_FIXTURES_MAPPING_PATH'] + + File.write(ENV['FRONTEND_FIXTURES_MAPPING_PATH'], $fixtures_mapping.to_json) # rubocop:disable Style/GlobalVars + end +end diff --git a/spec/support/graphql/fake_query_type.rb b/spec/support/graphql/fake_query_type.rb index ffd851a6e6a..18cf2cf3e82 100644 --- a/spec/support/graphql/fake_query_type.rb +++ b/spec/support/graphql/fake_query_type.rb @@ -1,15 +1,22 @@ # frozen_string_literal: true +require 'graphql' module Graphql - class FakeQueryType < Types::BaseObject + class FakeQueryType < ::GraphQL::Schema::Object graphql_name 'FakeQuery' field :hello_world, String, null: true do argument :message, String, required: false end + field :breaking_field, String, null: true + def hello_world(message: "world") "Hello #{message}!" end + + def breaking_field + raise "This field is supposed to break" + end end end diff --git a/spec/support/graphql/field_inspection.rb b/spec/support/graphql/field_inspection.rb index f39ba751141..e5fe37ec555 100644 --- a/spec/support/graphql/field_inspection.rb +++ b/spec/support/graphql/field_inspection.rb @@ -22,7 +22,7 @@ module Graphql @type ||= begin field_type = @field.type.respond_to?(:to_graphql) ? @field.type.to_graphql : @field.type - # The type could be nested. For example `[GraphQL::STRING_TYPE]`: + # The type could be nested. For example `[GraphQL::Types::String]`: # - List # - String! # - String diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index d3cc7367b6e..fd85071cca3 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -19,13 +19,15 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) + def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil) full_path = "/api/#{version}#{path}" if oauth_access_token query_string = "access_token=#{oauth_access_token.token}" elsif personal_access_token query_string = "private_token=#{personal_access_token.token}" + elsif job_token + query_string = "job_token=#{job_token}" elsif user personal_access_token = create(:personal_access_token, user: user) query_string = "private_token=#{personal_access_token.token}" diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 3502558b2c2..11040562b49 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -5,7 +5,7 @@ module Spec module Helpers module Features module InviteMembersModalHelper - def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false) + def invite_member(name, role: 'Guest', expires_at: nil) click_on 'Invite members' page.within '[data-testid="invite-members-modal"]' do @@ -14,7 +14,6 @@ module Spec wait_for_requests click_button name choose_options(role, expires_at) - choose_area_of_focus if area_of_focus click_button 'Invite' @@ -44,13 +43,6 @@ module Spec fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at end - - def choose_area_of_focus - page.within '[data-testid="area-of-focus-checks"]' do - check 'Contribute to the codebase' - check 'Collaborate on open issues and merge requests' - end - end end end end diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb index 8a329c2f9dd..923051a2e04 100644 --- a/spec/support/helpers/gitaly_setup.rb +++ b/spec/support/helpers/gitaly_setup.rb @@ -18,8 +18,12 @@ module GitalySetup Logger.new($stdout, level: level, formatter: ->(_, _, _, msg) { msg }) end + def expand_path(path) + File.expand_path(path, File.join(__dir__, '../../..')) + end + def tmp_tests_gitaly_dir - File.expand_path('../../../tmp/tests/gitaly', __dir__) + expand_path('tmp/tests/gitaly') end def tmp_tests_gitaly_bin_dir @@ -27,11 +31,11 @@ module GitalySetup end def tmp_tests_gitlab_shell_dir - File.expand_path('../../../tmp/tests/gitlab-shell', __dir__) + expand_path('tmp/tests/gitlab-shell') end def rails_gitlab_shell_secret - File.expand_path('../../../.gitlab_shell_secret', __dir__) + expand_path('.gitlab_shell_secret') end def gemfile @@ -48,7 +52,7 @@ module GitalySetup def env { - 'HOME' => File.expand_path('tmp/tests'), + 'HOME' => expand_path('tmp/tests'), 'GEM_PATH' => Gem.path.join(':'), 'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'), 'BUNDLE_INSTALL_FLAGS' => nil, @@ -67,7 +71,7 @@ module GitalySetup system('bundle config set --local retry 3', chdir: gemfile_dir) if ENV['CI'] - bundle_path = File.expand_path('../../../vendor/gitaly-ruby', __dir__) + bundle_path = expand_path('vendor/gitaly-ruby') system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir) end end @@ -154,7 +158,7 @@ module GitalySetup LOGGER.debug "Checking gitaly-ruby bundle...\n" out = ENV['CI'] ? $stdout : '/dev/null' - abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: File.dirname(gemfile)) + abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir) end def read_socket_path(service) diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index 81e669aab57..7e78fd86de3 100644 --- a/spec/support/helpers/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb @@ -138,7 +138,7 @@ module GpgHelpers end def primary_keyid - fingerprint[-16..-1] + fingerprint[-16..] end def fingerprint @@ -281,7 +281,7 @@ module GpgHelpers end def primary_keyid2 - fingerprint2[-16..-1] + fingerprint2[-16..] end def fingerprint2 @@ -374,7 +374,7 @@ module GpgHelpers end def primary_keyid - fingerprint[-16..-1] + fingerprint[-16..] end def fingerprint @@ -776,7 +776,7 @@ module GpgHelpers end def primary_keyid - fingerprint[-16..-1] + fingerprint[-16..] end def fingerprint diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 1f0c9b658dc..8b7d1c753d5 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -515,8 +515,13 @@ module GraphqlHelpers # Allows for array indexing, like this # ['project', 'boards', 'edges', 0, 'node', 'lists'] keys.reduce(data) do |memo, key| - if memo.is_a?(Array) - key.is_a?(Integer) ? memo[key] : memo.flat_map { |e| Array.wrap(e[key]) } + if memo.is_a?(Array) && key.is_a?(Integer) + memo[key] + elsif memo.is_a?(Array) + memo.compact.flat_map do |e| + x = e[key] + x.nil? ? [x] : Array.wrap(x) + end else memo&.dig(key) end diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index fb909008f12..84cd0181533 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -13,6 +13,12 @@ module JavaScriptFixturesHelpers included do |base| base.around do |example| + # Don't actually run the example when we're only interested in the `test file -> JSON frontend fixture` mapping + if ENV['GENERATE_FRONTEND_FIXTURES_MAPPING'] == 'true' + $fixtures_mapping[example.metadata[:file_path].delete_prefix('./')] << File.join(fixture_root_path, example.description) # rubocop:disable Style/GlobalVars + next + end + # pick an arbitrary date from the past, so tests are not time dependent # Also see spec/frontend/__helpers__/fake_date/jest.js Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run } diff --git a/spec/support/helpers/memory_usage_helper.rb b/spec/support/helpers/memory_usage_helper.rb index aa7b3bae83a..02d1935921f 100644 --- a/spec/support/helpers/memory_usage_helper.rb +++ b/spec/support/helpers/memory_usage_helper.rb @@ -23,7 +23,7 @@ module MemoryUsageHelper output, status = Gitlab::Popen.popen(%w(free -m)) abort "`free -m` return code is #{status}: #{output}" unless status == 0 - result = output.split("\n")[1].split(" ")[1..-1] + result = output.split("\n")[1].split(" ")[1..] attrs = %i(m_total m_used m_free m_shared m_buffers_cache m_available).freeze attrs.zip(result).to_h diff --git a/spec/support/helpers/migrations_helpers/work_item_types_helper.rb b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb new file mode 100644 index 00000000000..59b1f1b1305 --- /dev/null +++ b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module MigrationHelpers + module WorkItemTypesHelper + DEFAULT_WORK_ITEM_TYPES = { + issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 }, + incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 }, + test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, + requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, + task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 } + }.freeze + + def reset_work_item_types + work_item_types_table.delete_all + + DEFAULT_WORK_ITEM_TYPES.each do |type, attributes| + work_item_types_table.create!(base_type: attributes[:enum_value], **attributes.slice(:name, :icon_name)) + end + end + + private + + def work_item_types_table + table(:work_item_types) + end + end +end diff --git a/spec/support/helpers/modal_helpers.rb b/spec/support/helpers/modal_helpers.rb new file mode 100644 index 00000000000..a1f03cc0da5 --- /dev/null +++ b/spec/support/helpers/modal_helpers.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module ModalHelpers + def within_modal + page.within('[role="dialog"]') do + yield + end + end + + def accept_gl_confirm(text = nil, button_text: 'OK') + yield if block_given? + + within_modal do + unless text.nil? + expect(page).to have_content(text) + end + + click_button button_text + end + end + end + end + end +end diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index c2ec82155cd..6fa69cbd6ad 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -19,6 +19,17 @@ module NavbarStructureHelper hash[:nav_sub_items].insert(index + 1, new_sub_nav_item_name) end + def insert_before_sub_nav_item(after_sub_nav_item_name, within:, new_sub_nav_item_name:) + expect(structure).to include(a_hash_including(nav_item: within)) + hash = structure.find { |h| h[:nav_item] == within if h } + + expect(hash).to have_key(:nav_sub_items) + expect(hash[:nav_sub_items]).to include(after_sub_nav_item_name) + + index = hash[:nav_sub_items].find_index(after_sub_nav_item_name) + hash[:nav_sub_items].insert(index, new_sub_nav_item_name) + end + def insert_package_nav(within) insert_after_nav_item( within, diff --git a/spec/support/helpers/session_helpers.rb b/spec/support/helpers/session_helpers.rb index 4ef099a393e..236585296e5 100644 --- a/spec/support/helpers/session_helpers.rb +++ b/spec/support/helpers/session_helpers.rb @@ -17,10 +17,10 @@ module SessionHelpers end def get_session_keys - Gitlab::Redis::SharedState.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a } + Gitlab::Redis::Sessions.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a } end def get_ttl(key) - Gitlab::Redis::SharedState.with { |redis| redis.ttl(key) } + Gitlab::Redis::Sessions.with { |redis| redis.ttl(key) } end end diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb index 553739b5d30..c8b194919ed 100644 --- a/spec/support/helpers/snowplow_helpers.rb +++ b/spec/support/helpers/snowplow_helpers.rb @@ -48,11 +48,15 @@ module SnowplowHelpers # ) def expect_snowplow_event(category:, action:, context: nil, **kwargs) if context - kwargs[:context] = [] - context.each do |c| - expect(SnowplowTracker::SelfDescribingJson).to have_received(:new) - .with(c[:schema], c[:data]).at_least(:once) - kwargs[:context] << an_instance_of(SnowplowTracker::SelfDescribingJson) + if context.is_a?(Array) + kwargs[:context] = [] + context.each do |c| + expect(SnowplowTracker::SelfDescribingJson).to have_received(:new) + .with(c[:schema], c[:data]).at_least(:once) + kwargs[:context] << an_instance_of(SnowplowTracker::SelfDescribingJson) + end + else + kwargs[:context] = context end end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index ef3c39c83c2..ae031f58bd4 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -93,7 +93,7 @@ module StubGitlabCalls def stub_commonmark_sourcepos_disabled render_options = - if Feature.enabled?(:use_cmark_renderer) + if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C else Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index acbc15f7b62..d36bc4e3cb4 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -53,7 +53,7 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '2b5ef814', + 'add-ipython-files' => '532c837', 'add-pdf-file' => 'e774ebd', 'squash-large-files' => '54cec52', 'add-pdf-text-binary' => '79faa7b', @@ -594,6 +594,8 @@ module TestEnv # Not a git SHA, so return early return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID + return false unless Dir.exist?(component_folder) + sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder) return false if exit_status != 0 diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index d3833a1e8e8..1057639beec 100644 --- a/spec/support/matchers/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb @@ -9,7 +9,7 @@ RSpec::Matchers.define :be_background_migration_with_arguments do |arguments| end RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected| - define_method :matches? do |migration| + match(notify_expectation_failures: true) do |migration| expect(migration).to be_background_migration_with_arguments(expected) BackgroundMigrationWorker.jobs.any? do |job| @@ -26,7 +26,7 @@ RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected| end RSpec::Matchers.define :be_scheduled_migration do |*expected| - define_method :matches? do |migration| + match(notify_expectation_failures: true) do |migration| expect(migration).to be_background_migration_with_arguments(expected) BackgroundMigrationWorker.jobs.any? do |job| @@ -41,7 +41,7 @@ RSpec::Matchers.define :be_scheduled_migration do |*expected| end RSpec::Matchers.define :be_scheduled_migration_with_multiple_args do |*expected| - define_method :matches? do |migration| + match(notify_expectation_failures: true) do |migration| expect(migration).to be_background_migration_with_arguments(expected) BackgroundMigrationWorker.jobs.any? do |job| diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb index f27d873eb31..90c15dea1f8 100644 --- a/spec/support/redis/redis_helpers.rb +++ b/spec/support/redis/redis_helpers.rb @@ -32,4 +32,11 @@ module RedisHelpers def redis_sessions_cleanup! Gitlab::Redis::Sessions.with(&:flushdb) end + + # Usage: reset cached instance config + def redis_clear_raw_config!(instance_class) + instance_class.remove_instance_variable(:@_raw_config) + rescue NameError + # raised if @_raw_config was not set; ignore + end end diff --git a/spec/support/redis/redis_new_instance_shared_examples.rb b/spec/support/redis/redis_new_instance_shared_examples.rb index e9b1e3e4da1..943fe0f11ba 100644 --- a/spec/support/redis/redis_new_instance_shared_examples.rb +++ b/spec/support/redis/redis_new_instance_shared_examples.rb @@ -8,10 +8,16 @@ RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_cl let(:fallback_config_file) { nil } before do + redis_clear_raw_config!(fallback_class) + allow(fallback_class).to receive(:config_file_name).and_return(fallback_config_file) end - include_examples "redis_shared_examples" + after do + redis_clear_raw_config!(fallback_class) + end + + it_behaves_like "redis_shared_examples" describe '.config_file_name' do subject { described_class.config_file_name } diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index 72b3a72f9d4..d4c8682ec71 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -20,11 +20,11 @@ RSpec.shared_examples "redis_shared_examples" do before do allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s) - clear_raw_config + redis_clear_raw_config!(described_class) end after do - clear_raw_config + redis_clear_raw_config!(described_class) end describe '.config_file_name' do @@ -93,18 +93,23 @@ RSpec.shared_examples "redis_shared_examples" do subject { described_class.new(rails_env).store } shared_examples 'redis store' do + let(:redis_store) { ::Redis::Store } + let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" } + it 'instantiates Redis::Store' do - is_expected.to be_a(::Redis::Store) - expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database}") + is_expected.to be_a(redis_store) + + expect(subject.to_s).to eq(redis_store_to_s) end context 'with the namespace' do let(:namespace) { 'namespace_name' } + let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}" } subject { described_class.new(rails_env).store(namespace: namespace) } it "uses specified namespace" do - expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}") + expect(subject.to_s).to eq(redis_store_to_s) end end end @@ -394,12 +399,6 @@ RSpec.shared_examples "redis_shared_examples" do end end - def clear_raw_config - described_class.remove_instance_variable(:@_raw_config) - rescue NameError - # raised if @_raw_config was not set; ignore - end - def clear_pool described_class.remove_instance_variable(:@pool) rescue NameError diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 00b9aac7bf4..b4a25fd121d 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -15,7 +15,10 @@ require 'rubocop' require 'rubocop/rspec/support' RSpec.configure do |config| - config.mock_with :rspec + config.mock_with :rspec do |mocks| + mocks.verify_doubled_constant_names = true + end + config.raise_errors_for_deprecations! config.include StubConfiguration diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb index 07012914a4d..6414a4d1eb3 100644 --- a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb @@ -28,7 +28,7 @@ RSpec.shared_context 'project service activation' do end def click_test_integration - click_link('Test settings') + click_button('Test settings') end def click_test_then_save_integration(expect_test_to_fail: true) diff --git a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb new file mode 100644 index 00000000000..d0915bbf158 --- /dev/null +++ b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works. +RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_yml_file_path| + include ApiHelpers + include WikiHelpers + + let_it_be(:user) { create(:user, username: 'gfm_user') } + + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :public, :repository, group: group) } + + let_it_be(:label) { create(:label, project: project, title: 'bug') } + let_it_be(:milestone) { create(:milestone, project: project, title: '1.1') } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) } + + let_it_be(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) } + + before(:all) do + group.add_owner(user) + project.add_maintainer(user) + end + + before do + sign_in(user) + end + + markdown_examples = begin + yaml = File.read(markdown_yml_file_path) + YAML.safe_load(yaml, symbolize_names: true, aliases: true) + end + + it "examples must be unique and alphabetized by name", :unlimited_max_formatted_output_length do + names = markdown_examples.map { |example| example[:name] } + expect(names).to eq(names.sort.uniq) + end + + if focused_markdown_examples_string = ENV['FOCUSED_MARKDOWN_EXAMPLES'] + focused_markdown_examples = focused_markdown_examples_string.split(',').map(&:strip) || [] + markdown_examples.reject! {|markdown_example| !focused_markdown_examples.include?(markdown_example.fetch(:name)) } + end + + markdown_examples.each do |markdown_example| + name = markdown_example.fetch(:name) + api_context = markdown_example[:api_context] + + if api_context && !name.end_with?("_for_#{api_context}") + raise "Name must have suffix of '_for_#{api_context}' to the api_context" + end + + context "for #{name}#{api_context ? " (api_context: #{api_context})" : ''}" do + let(:pending_reason) do + pending_value = markdown_example.fetch(:pending, nil) + get_pending_reason(pending_value) + end + + let(:example_markdown) { markdown_example.fetch(:markdown) } + let(:example_html) { markdown_example.fetch(:html) } + let(:substitutions) { markdown_example.fetch(:substitutions, {}) } + + it "verifies conversion of GFM to HTML", :unlimited_max_formatted_output_length do + pending pending_reason if pending_reason + + normalized_example_html = normalize_html(example_html, substitutions) + + api_url = get_url_for_api_context(api_context) + + post api_url, params: { text: example_markdown, gfm: true } + expect(response).to be_successful + response_body = Gitlab::Json.parse(response.body) + # Some requests have the HTML in the `html` key, others in the `body` key. + response_html = response_body['body'] ? response_body.fetch('body') : response_body.fetch('html') + normalized_response_html = normalize_html(response_html, substitutions) + + expect(normalized_response_html).to eq(normalized_example_html) + end + + def get_pending_reason(pending_value) + return false unless pending_value + + return pending_value if pending_value.is_a?(String) + + pending_value[:backend] || false + end + + def normalize_html(html, substitutions) + normalized_html = html.dup + # Note: having the top level `substitutions` data structure be a hash of arrays + # allows us to compose multiple substitutions via YAML anchors (YAML anchors + # pointing to arrays can't be combined) + substitutions.each_value do |substitution_entry| + substitution_entry.each do |substitution| + regex = substitution.fetch(:regex) + replacement = substitution.fetch(:replacement) + normalized_html.gsub!(%r{#{regex}}, replacement) + end + end + + normalized_html + end + end + end + + def supported_api_contexts + %w(project group project_wiki) + end + + def get_url_for_api_context(api_context) + case api_context + when 'project' + "/#{project.full_path}/preview_markdown" + when 'group' + "/groups/#{group.full_path}/preview_markdown" + when 'project_wiki' + "/#{project.full_path}/-/wikis/#{project_wiki_page.slug}/preview_markdown" + when nil + api "/markdown" + else + raise "Error: 'context' extension was '#{api_context}'. It must be one of: #{supported_api_contexts.join(',')}" + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index bcc6abdc308..085f1f13c2c 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -5,7 +5,7 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Security & Compliance'), nav_sub_items: [ - (_('Audit Events') if Gitlab.ee?), + (_('Audit events') if Gitlab.ee?), _('Configuration') ] } @@ -94,11 +94,11 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Analytics'), nav_sub_items: [ + _('Value stream'), _('CI/CD'), (_('Code review') if Gitlab.ee?), (_('Merge request') if Gitlab.ee?), - _('Repository'), - _('Value stream') + _('Repository') ] }, { @@ -165,7 +165,7 @@ RSpec.shared_context 'group navbar structure' do { nav_item: _('Security & Compliance'), nav_sub_items: [ - _('Audit Events') + _('Audit events') ] } end @@ -190,7 +190,8 @@ RSpec.shared_context 'group navbar structure' do [ _('List'), _('Board'), - _('Milestones') + _('Milestones'), + (_('Iterations') if Gitlab.ee?) ] end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index b432aa24bb8..ad6462dc367 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -48,6 +48,7 @@ RSpec.shared_context 'GroupPolicy context' do destroy_package create_projects read_cluster create_cluster update_cluster admin_cluster add_cluster + admin_group_runners ] end diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb index e8cc666605b..06800f7cded 100644 --- a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb +++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb @@ -9,16 +9,18 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) } - context 'successfully imports wiki for an entity' do - subject { described_class.new(context) } + subject { described_class.new(context) } - before do - allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| - allow(extractor).to receive(:extract).and_return(extracted_data) - end + before do + allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(extracted_data) end + end + context 'when wiki exists' do it 'imports new wiki into destination project' do + expect(subject).to receive(:source_wiki_exists?).and_return(true) + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git" expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0 @@ -27,5 +29,16 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do subject.run end end + + context 'when wiki does not exist' do + it 'does not import wiki' do + expect(subject).to receive(:source_wiki_exists?).and_return(false) + + expect(parent.wiki).not_to receive(:ensure_repository) + expect(parent.wiki.repository).not_to receive(:ensure_repository) + + expect { subject.run }.not_to raise_error + end + end end end diff --git a/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb b/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb new file mode 100644 index 00000000000..a72ce320e90 --- /dev/null +++ b/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'pipelines are created without N+1 SQL queries' do + before do + # warm up + stub_ci_pipeline_yaml_file(config1) + execute_service + end + + it 'avoids N+1 queries', :aggregate_failures, :request_store, :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + stub_ci_pipeline_yaml_file(config1) + + pipeline = execute_service.payload + + expect(pipeline).to be_created_successfully + end + + expect do + stub_ci_pipeline_yaml_file(config2) + + pipeline = execute_service.payload + + expect(pipeline).to be_created_successfully + end.not_to exceed_all_query_limit(control).with_threshold(accepted_n_plus_ones) + end +end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 0ffa32dec9e..46fc2cbdc9b 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -58,11 +58,12 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET new' do end RSpec.shared_examples 'a GitHub-ish import controller: GET status' do + let(:repo_fake) { Struct.new(:id, :login, :full_name, :name, :owner, keyword_init: true) } let(:new_import_url) { public_send("new_import_#{provider}_url") } let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } - let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } + let(:repo) { repo_fake.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } + let(:org) { double('org', login: 'company') } + let(:org_repo) { repo_fake.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } before do assign_session_token(provider) @@ -72,7 +73,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') group = create(:group) group.add_owner(user) - stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [OpenStruct.new(objects: [repo, org_repo])].to_enum) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [double('client', objects: [repo, org_repo])].to_enum) get :status, format: :json @@ -125,7 +126,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end context 'when filtering' do - let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } + let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } let(:group) { create(:group) } let(:repos) { [repo, repo_2, org_repo] } @@ -133,7 +134,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do before do group.add_owner(user) client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) - allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + allow(client).to receive(:each_page).and_return([double('client', objects: repos)].to_enum) # GitHub controller has filtering done using GitHub Search API stub_feature_flags(remove_legacy_github_client: false) end @@ -172,7 +173,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do repos = [build(:project, name: 2, path: 'test')] client = stub_client(repos: repos) - allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + allow(client).to receive(:each_page).and_return([double('client', objects: repos)].to_enum) end it 'does not raise an error' do @@ -189,13 +190,14 @@ end RSpec.shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } let(:provider_username) { user.username } - let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_user) { double('user', login: provider_username) } let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") } let(:provider_repo) do - OpenStruct.new( + double( + 'provider', name: 'vim', full_name: "#{provider_username}/vim", - owner: OpenStruct.new(login: provider_username) + owner: double('owner', login: provider_username) ) end @@ -265,10 +267,9 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do end context "when the repository owner is not the provider user" do - let(:other_username) { "someone_else" } + let(:provider_username) { "someone_else" } before do - provider_repo.owner = OpenStruct.new(login: other_username) assign_session_token(provider) end @@ -277,8 +278,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is owned by the GitLab user" do before do - user.username = other_username - user.save! + user.update!(username: provider_username) end it "takes the existing namespace" do @@ -292,7 +292,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is not owned by the GitLab user" do it "creates a project using user's namespace" do - create(:user, username: other_username) + create(:user, username: provider_username) expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index 00a0fb7e4c5..3a7588a5cc9 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -50,7 +50,8 @@ RSpec.shared_examples Repositories::GitHttpController do context 'with authorized user' do before do - request.headers.merge! auth_env(user.username, user.password, nil) + password = user.try(:password) || user.try(:token) + request.headers.merge! auth_env(user.username, password, nil) end it 'returns 200' do @@ -71,9 +72,10 @@ RSpec.shared_examples Repositories::GitHttpController do it 'adds user info to the logs' do get :info_refs, params: params - expect(log_data).to include('username' => user.username, - 'user_id' => user.id, - 'meta.user' => user.username) + user_log_data = { 'username' => user.username, 'user_id' => user.id } + user_log_data['meta.user'] = user.username if user.is_a?(User) + + expect(log_data).to include(user_log_data) 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 30914e61df0..ac7680f7ddb 100644 --- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -6,15 +6,23 @@ RSpec.shared_examples 'tracking unique visits' do |method| let(:request_params) { {} } it 'tracks unique visit if the format is HTML' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .to receive(:track_event).with(target_id, values: kind_of(String)) + ids = target_id.instance_of?(String) ? [target_id] : target_id + + ids.each do |id| + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(id, values: kind_of(String)) + end get method, params: request_params, format: :html end it 'tracks unique visit if DNT is not enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .to receive(:track_event).with(target_id, values: kind_of(String)) + ids = target_id.instance_of?(String) ? [target_id] : target_id + + ids.each do |id| + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(id, values: kind_of(String)) + end stub_do_not_track('0') 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 30710e43357..1cb52c07069 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -299,7 +299,7 @@ RSpec.shared_examples 'wiki controller actions' do expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true') expect(response.cache_control[:public]).to be(false) - expect(response.headers['Cache-Control']).to eq('private, no-store') + expect(response.headers['Cache-Control']).to eq('max-age=60, private') end end end diff --git a/spec/support/shared_examples/csp.rb b/spec/support/shared_examples/csp.rb index c4a8c7df898..9143d0f4720 100644 --- a/spec/support/shared_examples/csp.rb +++ b/spec/support/shared_examples/csp.rb @@ -28,7 +28,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "appends to #{rule_name}" do - is_expected.to eql("#{rule_name} #{default_csp_values} #{whitelisted_url}") + is_expected.to eql("#{rule_name} #{default_csp_values} #{allowlisted_url}") end end @@ -46,7 +46,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "uses default-src values in #{rule_name}" do - is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{whitelisted_url}") + is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{allowlisted_url}") end end @@ -64,7 +64,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "uses default-src values in #{rule_name}" do - is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{whitelisted_url}") + is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{allowlisted_url}") end end diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb index 81653220b4c..e3ea36633d1 100644 --- a/spec/support/shared_examples/features/page_description_shared_examples.rb +++ b/spec/support/shared_examples/features/page_description_shared_examples.rb @@ -7,3 +7,13 @@ RSpec.shared_examples 'page meta description' do |expected_description| end end end + +RSpec.shared_examples 'default brand title page meta description' do + include AppearancesHelper + + it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do + %w(name='description' property='og:description' property='twitter:description').each do |selector| + expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false) + end + end +end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb new file mode 100644 index 00000000000..345dfbce423 --- /dev/null +++ b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'date sidebar widget' do + context 'editing due date' do + let(:due_date_value) { find('[data-testid="sidebar-due-date"] [data-testid="sidebar-date-value"]') } + + around do |example| + freeze_time { example.run } + end + + it 'displays "None" when there is no due date' do + expect(due_date_value.text).to have_content 'None' + end + + it 'updates due date' do + page.within('[data-testid="sidebar-due-date"]') do + today = Date.today.day + + click_button 'Edit' + + click_button today.to_s + + wait_for_requests + + expect(page).to have_content(today.to_s(:medium)) + expect(due_date_value.text).to have_content Time.current.strftime('%b %-d, %Y') + end + end + end +end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb new file mode 100644 index 00000000000..da730240e8e --- /dev/null +++ b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'milestone sidebar widget' do + context 'editing milestone' do + let_it_be(:milestone_expired) { create(:milestone, project: project, title: 'Foo - expired', due_date: 5.days.ago) } + let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } + + let(:milestone_widget) { find('[data-testid="sidebar-milestones"]') } + + before do + within(milestone_widget) do + click_button 'Edit' + end + + wait_for_all_requests + end + + it 'shows milestones list in the dropdown' do + # 5 milestones + "No milestone" = 6 items + expect(milestone_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 6) + end + + it 'shows expired milestone at the bottom of the list and milestone due earliest at the top of the list', :aggregate_failures do + within(milestone_widget, '.gl-new-dropdown-contents') do + expect(page.find('li:last-child')).to have_content milestone_expired.title + + [milestone3, milestone2, milestone1, milestone_no_duedate].each_with_index do |m, i| + expect(page.all('li.gl-new-dropdown-item')[i + 1]).to have_content m.title + end + end + end + + it 'adds a milestone' do + within(milestone_widget) do + click_button milestone1.title + + wait_for_requests + + page.within('[data-testid="select-milestone"]') do + expect(page).to have_content(milestone1.title) + end + end + end + + it 'removes a milestone' do + within(milestone_widget) do + click_button "No milestone" + + wait_for_requests + + page.within('[data-testid="select-milestone"]') do + expect(page).not_to have_content(milestone1.title) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index d509d124de0..615f568420e 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -5,6 +5,7 @@ RSpec.shared_examples 'issue boards sidebar' do before do first_card.click + wait_for_requests end it 'shows sidebar when clicking issue' do @@ -41,6 +42,14 @@ RSpec.shared_examples 'issue boards sidebar' do end end + context 'editing issue milestone', :js do + it_behaves_like 'milestone sidebar widget' + end + + context 'editing issue due date', :js do + it_behaves_like 'date sidebar widget' + end + context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index bd1a67f3bb5..c402333107c 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_examples 'paginated snippets' do |remote: false| end RSpec.shared_examples 'tabs with counts' do - let(:tabs) { page.all('.snippet-scope-menu li') } + let(:tabs) { page.all('.js-snippets-nav-tabs li') } it 'shows a tab for All snippets and count' do tab = tabs[0] diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 7ced8508a31..a456b76b324 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -138,11 +138,26 @@ RSpec.shared_examples 'User updates wiki page' do end context 'when using the content editor' do - before do - click_button 'Use the new editor' + context 'with feature flag on' do + before do + click_button 'Edit rich text' + end + + it_behaves_like 'edits content using the content editor' end - it_behaves_like 'edits content using the content editor' + context 'with feature flag off' do + before do + stub_feature_flags(wiki_switch_between_content_editor_raw_markdown: false) + visit(wiki_path(wiki)) + + click_link('Edit') + + click_button 'Use the new editor' + end + + it_behaves_like 'edits content using the content editor' + end end end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index 96df5a5f972..eec911f3b6f 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -161,7 +161,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('updated home') expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') expect(page).to have_content('some link') @@ -174,7 +174,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('updated home') expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') expect(page).to have_content('some link') @@ -188,7 +188,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('created page: home') expect(page).to have_content('Showing 1 changed file with 4 additions and 0 deletions') expect(page).to have_content('Look at this') 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 deleted file mode 100644 index 7707e79386c..00000000000 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class_name| - it 'migrates resource mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize - - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(1) - - user_mention = user_mentions.last - expect(user_mention.mentioned_users_ids.sort).to eq(mentioned_users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that performing the same job twice does not fail and does not change counts - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class_name| - it 'migrates mentions from note' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - - # there are 5 notes for each noteable_type, but two do not have mentions and - # another one's noteable_id points to an inexistent resource - expect(notes.where(noteable_type: resource_class_name).count).to eq 5 - expect(user_mentions.count).to eq 0 - - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(2) - - # check that the user_mention for regular note is created - user_mention = user_mentions.first - expect(Note.find(user_mention.note_id).system).to be false - expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that the user_mention for system note is created - user_mention = user_mentions.second - expect(Note.find(user_mention.note_id).system).to be true - expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that performing the same job twice does not fail and does not change counts - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| - before do - stub_const("#{described_class.name}::BATCH_SIZE", 1) - end - - it 'schedules background migrations' do - Sidekiq::Testing.fake! do - freeze_time do - resource_count = is_for_notes ? Note.count : resource_class.count - expect(resource_count).to eq 5 - - migrate! - - migration = described_class::MIGRATION - join = described_class::JOIN - conditions = described_class::QUERY_CONDITIONS - delay = described_class::DELAY - - expect(migration).to be_scheduled_delayed_migration(1 * delay, resource_class.name, join, conditions, is_for_notes, resource1.id, resource1.id) - expect(migration).to be_scheduled_delayed_migration(2 * delay, resource_class.name, join, conditions, is_for_notes, resource2.id, resource2.id) - expect(migration).to be_scheduled_delayed_migration(3 * delay, resource_class.name, join, conditions, is_for_notes, resource3.id, resource3.id) - expect(BackgroundMigrationWorker.jobs.size).to eq 3 - end - end - end -end - -RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class_name| - it 'does not migrate mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize - - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class_name| - it 'does not migrate mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index bd8bdd70ce5..bce889b454d 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples_for 'value stream analytics event' do it { expect(described_class.identifier).to be_a_kind_of(Symbol) } it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } it { expect(instance).to respond_to(:timestamp_projection) } - it { expect(instance).to respond_to(:markdown_description) } + it { expect(instance).to respond_to(:html_description) } it { expect(instance.column_list).to be_a_kind_of(Array) } describe '#apply_query_customization' do diff --git a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb index 41d3d76b66b..03344584361 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes, additional_attributes = []| - let(:prohibited_attributes) { %i[remote_url my_attributes my_ids token my_id test] } + let(:prohibited_attributes) { %w[remote_url my_attributes my_ids token my_id test] } let(:import_export_config) { Gitlab::ImportExport::Config.new.to_h } let(:project_relation_factory) { Gitlab::ImportExport::Project::RelationFactory } @@ -8,7 +8,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib let(:relation_hash) { (permitted_attributes + prohibited_attributes).map(&:to_s).zip([]).to_h } let(:relation_name) { project_relation_factory.overrides[relation_sym]&.to_sym || relation_sym } let(:relation_class) { project_relation_factory.relation_class(relation_name) } - let(:excluded_keys) { import_export_config.dig(:excluded_keys, relation_sym) || [] } + let(:excluded_keys) { (import_export_config.dig(:excluded_attributes, relation_sym) || []).map(&:to_s) } let(:cleaned_hash) do Gitlab::ImportExport::AttributeCleaner.new( @@ -18,7 +18,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib ).clean end - let(:permitted_hash) { subject.permit(relation_sym, relation_hash) } + let(:permitted_hash) { subject.permit(relation_sym, relation_hash).transform_keys { |k| k.to_s } } if described_class.new.permitted_attributes_defined?(relation_sym) it 'contains only attributes that are defined as permitted in the import/export config' do @@ -26,11 +26,11 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib end it 'does not contain attributes that would be cleaned with AttributeCleaner' do - expect(cleaned_hash.keys + additional_attributes.to_a).to include(*permitted_hash.keys) + expect(cleaned_hash.keys + additional_attributes.to_a.map(&:to_s)).to include(*permitted_hash.keys) end it 'does not contain prohibited attributes that are not related to given relation' do - expect(permitted_hash.keys).not_to include(*prohibited_attributes.map(&:to_s)) + expect(permitted_hash.keys).not_to include(*prohibited_attributes) end else it 'is disabled' do diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb new file mode 100644 index 00000000000..046c70bf779 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default| + context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do + before do + stub_feature_flags(use_primary_and_secondary_stores => true) + end + + it 'multi store is enabled' do + expect(subject.use_primary_and_secondary_stores?).to be true + end + end + + context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do + before do + stub_feature_flags(use_primary_and_secondary_stores => false) + end + + it 'multi store is disabled' do + expect(subject.use_primary_and_secondary_stores?).to be false + end + end + + context "with feature flag :#{use_primary_store_as_default} is enabled" do + before do + stub_feature_flags(use_primary_store_as_default => true) + end + + it 'primary store is enabled' do + expect(subject.use_primary_store_as_default?).to be true + end + end + + context "with feature flag :#{use_primary_store_as_default} is disabled" do + before do + stub_feature_flags(use_primary_store_as_default => false) + end + + it 'primary store is disabled' do + expect(subject.use_primary_store_as_default?).to be false + end + end +end diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb index 7ccd9533811..8f3a93de509 100644 --- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb +++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb @@ -4,17 +4,12 @@ RSpec.shared_examples 'it has loose foreign keys' do let(:factory_name) { nil } let(:table_name) { described_class.table_name } let(:connection) { described_class.connection } - - it 'includes the LooseForeignKey module' do - expect(described_class.ancestors).to include(LooseForeignKey) - end - - it 'responds to #loose_foreign_key_definitions' do - expect(described_class).to respond_to(:loose_foreign_key_definitions) - end + let(:fully_qualified_table_name) { "#{connection.current_schema}.#{table_name}" } + let(:deleted_records) { LooseForeignKeys::DeletedRecord.where(fully_qualified_table_name: fully_qualified_table_name) } it 'has at least one loose foreign key definition' do - expect(described_class.loose_foreign_key_definitions.size).to be > 0 + definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table_name] + expect(definitions.size).to be > 0 end it 'has the deletion trigger present' do @@ -32,9 +27,11 @@ RSpec.shared_examples 'it has loose foreign keys' do it 'records record deletions' do model = create(factory_name) # rubocop: disable Rails/SaveBang - model.destroy! - deleted_record = LooseForeignKeys::DeletedRecord.find_by(fully_qualified_table_name: "#{connection.current_schema}.#{table_name}", primary_key_value: model.id) + # using delete to avoid cross-database modification errors when associations with dependent option are present + model.delete + + deleted_record = deleted_records.find_by(primary_key_value: model.id) expect(deleted_record).not_to be_nil end @@ -42,11 +39,36 @@ RSpec.shared_examples 'it has loose foreign keys' do it 'cleans up record deletions' do model = create(factory_name) # rubocop: disable Rails/SaveBang - expect { model.destroy! }.to change { LooseForeignKeys::DeletedRecord.count }.by(1) + expect { model.delete }.to change { deleted_records.count }.by(1) LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute - expect(LooseForeignKeys::DeletedRecord.status_pending.count).to be(0) - expect(LooseForeignKeys::DeletedRecord.status_processed.count).to be(1) + expect(deleted_records.status_pending.count).to be(0) + expect(deleted_records.status_processed.count).to be(1) + end +end + +RSpec.shared_examples 'cleanup by a loose foreign key' do + let(:foreign_key_definition) do + foreign_keys_for_parent = Gitlab::Database::LooseForeignKeys.definitions_by_table[parent.class.table_name] + foreign_keys_for_parent.find { |definition| definition.from_table == model.class.table_name } + end + + def find_model + model.class.find_by(id: model.id) + end + + it 'deletes the model' do + parent.delete + + expect(find_model).to be_present + + LooseForeignKeys::ProcessDeletedRecordsService.new(connection: model.connection).execute + + if foreign_key_definition.on_delete.eql?(:async_delete) + expect(find_model).not_to be_present + else + expect(find_model[foreign_key_definition.column]).to eq(nil) + end end end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index e1f7a9030e2..20ed380fb18 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -161,6 +161,12 @@ RSpec.shared_examples 'it should not have Gmail Actions links' do end end +RSpec.shared_examples 'it should show Gmail Actions Join now link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text('Join now') } +end + RSpec.shared_examples 'it should show Gmail Actions View Issue link' do it_behaves_like 'it should have Gmail Actions links' diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index c06083ba952..6e8c340582a 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| - let(:db_config_name) { ::Gitlab::Database.db_config_names.first } + let(:db_config_name) do + db_config_name = ::Gitlab::Database.db_config_names.first + db_config_name += "_replica" if db_role == :secondary + db_config_name + end let(:expected_payload_defaults) do result = {} @@ -39,15 +43,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_primary_cached_count: record_cached_query ? 1 : 0, - "db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, db_primary_count: record_query ? 1 : 0, - "db_primary_#{db_config_name}_count": record_query ? 1 : 0, + "db_#{db_config_name}_count": record_query ? 1 : 0, db_primary_duration_s: record_query ? 0.002 : 0.0, - "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, + "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, db_primary_wal_count: record_wal_query ? 1 : 0, - "db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - "db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 }) elsif db_role == :replica transform_hash(expected_payload_defaults, { @@ -55,15 +59,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_replica_cached_count: record_cached_query ? 1 : 0, - "db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, db_replica_count: record_query ? 1 : 0, - "db_replica_#{db_config_name}_count": record_query ? 1 : 0, + "db_#{db_config_name}_count": record_query ? 1 : 0, db_replica_duration_s: record_query ? 0.002 : 0.0, - "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, + "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, db_replica_wal_count: record_wal_query ? 1 : 0, - "db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - "db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 }) else transform_hash(expected_payload_defaults, { @@ -71,15 +75,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_primary_cached_count: 0, - "db_primary_#{db_config_name}_cached_count": 0, + "db_#{db_config_name}_cached_count": 0, db_primary_count: 0, - "db_primary_#{db_config_name}_count": 0, + "db_#{db_config_name}_count": 0, db_primary_duration_s: 0.0, - "db_primary_#{db_config_name}_duration_s": 0.0, + "db_#{db_config_name}_duration_s": 0.0, db_primary_wal_count: 0, - "db_primary_#{db_config_name}_wal_count": 0, + "db_#{db_config_name}_wal_count": 0, db_primary_wal_cached_count: 0, - "db_primary_#{db_config_name}_wal_cached_count": 0 + "db_#{db_config_name}_wal_cached_count": 0 }) end @@ -105,7 +109,11 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| end RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role| - let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.retrieve_connection) } + let(:db_config_name) do + db_config_name = ::Gitlab::Database.db_config_names.first + db_config_name += "_replica" if db_role == :secondary + db_config_name + end it 'increments only db counters' do if record_query diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 03f565e0aac..fe85daa7235 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -80,15 +80,22 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| it 'calls InternalId.generate_next and sets internal id attribute' do iid = rand(1..1000) - expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid) + # Need to do this before evaluating instance otherwise it gets set + # already in factory + allow(InternalId).to receive(:generate_next).and_return(iid) + subject expect(read_internal_id).to eq(iid) + + expect(InternalId).to have_received(:generate_next).with(instance, scope_attrs, usage, any_args) end it 'does not overwrite an existing internal id' do write_internal_id(4711) - expect { subject }.not_to change { read_internal_id } + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect { subject }.not_to change { read_internal_id } + end end context 'when the instance has an internal ID set' do @@ -101,6 +108,7 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| .to receive(:track_greatest) .with(instance, scope_attrs, usage, internal_id, any_args) .and_return(internal_id) + subject end end @@ -110,7 +118,11 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| context 'when the internal id has been changed' do context 'when the internal id is automatically set' do it 'clears it on the instance' do - expect_iid_to_be_set_and_rollback + write_internal_id(nil) + + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect_iid_to_be_set_and_rollback + end expect(read_internal_id).to be_nil end @@ -120,7 +132,9 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| it 'does not clear it on the instance' do write_internal_id(100) - expect_iid_to_be_set_and_rollback + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect_iid_to_be_set_and_rollback + end expect(read_internal_id).not_to be_nil end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 72659dd5f3b..e6b270c6188 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -71,7 +71,7 @@ RSpec.shared_examples "chat integration" do |integration_name| it "does not call #{integration_name} API" do result = subject.execute(sample_data) - expect(result).to be(false) + expect(result).to be_falsy expect(WebMock).not_to have_requested(:post, webhook_url) end end @@ -113,7 +113,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with protected branch" do before do - create(:protected_branch, project: project, name: "a-protected-branch") + create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch") end let(:sample_data) do @@ -309,7 +309,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with protected branch" do before do - create(:protected_branch, project: project, name: "a-protected-branch") + create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch") end let(:sample_data) do @@ -355,5 +355,11 @@ RSpec.shared_examples "chat integration" do |integration_name| end end end + + context 'deployment events' do + let(:sample_data) { Gitlab::DataBuilder::Deployment.build(create(:deployment), Time.now) } + + it_behaves_like "untriggered #{integration_name} integration" + end end end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 2d4c0b60f2b..ad15f82be5e 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -305,7 +305,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch' do before do - create(:protected_branch, project: project, name: 'a-protected-branch') + create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end let(:data) do @@ -347,7 +347,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch with protected branches defined using wildcards' do before do - create(:protected_branch, project: project, name: '*-stable') + create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end let(:data) do @@ -560,7 +560,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch' do before do - create(:protected_branch, project: project, name: 'a-protected-branch') + create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end let(:pipeline) do @@ -590,7 +590,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch with protected branches defined usin wildcards' do before do - create(:protected_branch, project: project, name: '*-stable') + create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end let(:pipeline) do diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index a2909c66e22..d5d137922eb 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -301,10 +301,6 @@ RSpec.shared_examples_for "member creation" do end context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - before do - stub_experiments(invite_members_for_task: true) - end - it 'creates a member_task with the correct attributes', :aggregate_failures do task_project = source.is_a?(Group) ? create(:project, group: source) : source described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute @@ -397,10 +393,6 @@ RSpec.shared_examples_for "bulk member creation" do end context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - before do - stub_experiments(invite_members_for_task: true) - end - it 'creates a member_task with the correct attributes', :aggregate_failures do task_project = source.is_a?(Group) ? create(:project, group: source) : source members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id) diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index f08ee820463..23026167b19 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') } let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') } let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) } - let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) } + let_it_be_with_refind(:component_file_with_file_type_sources) { create("debian_#{container_type}_component_file", :sources, component: component1_1) } let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) } subject { component_file_with_architecture } @@ -43,8 +43,8 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) } end - context 'with :source file_type' do - subject { component_file_with_file_type_source } + context 'with :sources file_type' do + subject { component_file_with_file_type_sources } it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional } end @@ -66,8 +66,8 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to validate_presence_of(:architecture) } end - context 'with :source file_type' do - subject { component_file_with_file_type_source } + context 'with :sources file_type' do + subject { component_file_with_file_type_sources } it { is_expected.to validate_absence_of(:architecture) } end @@ -135,10 +135,10 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| end describe '.with_file_type' do - subject { described_class.with_file_type(:source) } + subject { described_class.with_file_type(:sources) } it do - expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) + expect(subject.to_a).to contain_exactly(component_file_with_file_type_sources) end end @@ -214,9 +214,9 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| end context 'with a Source file_type' do - subject { component_file_with_file_type_source.relative_path } + subject { component_file_with_file_type_sources.relative_path } - it { is_expected.to eq("#{component1_1.name}/source/Source") } + it { is_expected.to eq("#{component1_1.name}/source/Sources") } end context 'with a DI Packages file_type' do diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index ac6a843663f..73e22b97abc 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -205,6 +205,58 @@ RSpec.shared_examples 'namespace traversal' do end end + shared_examples '#ancestors_upto' do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + + it 'returns all ancestors when no namespace is given' do + expect(child2.ancestors_upto).to contain_exactly(child, parent) + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(child2.ancestors_upto(parent)).to contain_exactly(child) + end + + context 'with asc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :asc)).to eq([child, parent]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child]) + end + end + + describe '#recursive_self_and_ancestor_ids' do + it 'is equivalent to ancestors_upto' do + recursive_result = child2.recursive_ancestors_upto(parent) + linear_result = child2.ancestors_upto(parent) + expect(linear_result).to match_array recursive_result + end + + it 'makes a recursive query' do + expect { child2.recursive_ancestors_upto.try(:load) }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + + describe '#ancestors_upto' do + context 'with use_traversal_ids_for_ancestors_upto enabled' do + include_examples '#ancestors_upto' + end + + context 'with use_traversal_ids_for_ancestors_upto disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors_upto: false) + end + + include_examples '#ancestors_upto' + end + end + describe '#descendants' do let!(:another_group) { create(:group) } let!(:another_group_nested) { create(:group, parent: another_group) } diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 4c09c1c2a3b..3d52ed30c62 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -213,6 +213,12 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) } end + + context 'with offset and limit' do + subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants } + + it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } + end end describe '.self_and_descendants' do @@ -242,6 +248,19 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) } end + + context 'with offset and limit' do + subject do + described_class + .where(id: [group_1, group_2]) + .limit(1) + .offset(1) + .self_and_descendant_ids + .pluck(:id) + end + + it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) } + end end describe '.self_and_descendant_ids' do 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 e45be21f152..9f4fdcf7ba1 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 @@ -173,3 +173,65 @@ RSpec.shared_examples 'rejects Composer access with unknown project id' do end end end + +RSpec.shared_examples 'Composer access with deploy tokens' do + shared_examples 'a deploy token for Composer GET requests' do + context 'with deploy token headers' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + before do + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :not_found + end + end + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_group } + + it_behaves_like 'a deploy token for Composer GET requests' + end + + context 'project deploy token' do + let(:deploy_token) { deploy_token_for_project } + + it_behaves_like 'a deploy token for Composer GET requests' + end +end + +RSpec.shared_examples 'Composer publish with deploy tokens' do + shared_examples 'a deploy token for Composer publish requests' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_group } + + it_behaves_like 'a deploy token for Composer publish requests' + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_project } + + it_behaves_like 'a deploy token for Composer publish requests' + end +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 index 20606ae942d..71f3a0235be 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -178,6 +178,54 @@ RSpec.shared_examples 'rejects invalid recipe' do end end +RSpec.shared_examples 'handling empty values for username and channel' do + using RSpec::Parameterized::TableSyntax + + let(:recipe_path) { "#{package.name}/#{package.version}/#{package_username}/#{channel}" } + + where(:username, :channel, :status) do + 'username' | 'channel' | :ok + 'username' | '_' | :bad_request + '_' | 'channel' | :bad_request_or_not_found + '_' | '_' | :ok_or_not_found + end + + with_them do + let(:package_username) do + if username == 'username' + package.conan_metadatum.package_username + else + username + end + end + + before do + project.add_maintainer(user) # avoid any permission issue + end + + it 'returns the correct status code' do |example| + project_level = example.full_description.include?('api/v4/projects') + + expected_status = case status + when :ok_or_not_found + project_level ? :ok : :not_found + when :bad_request_or_not_found + project_level ? :bad_request : :not_found + else + status + end + + if expected_status == :ok + package.conan_metadatum.update!(package_username: package_username, package_channel: channel) + end + + subject + + expect(response).to have_gitlab_http_status(expected_status) + end + end +end + RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name| let(:file_name) { invalid_file_name } @@ -300,6 +348,7 @@ RSpec.shared_examples 'recipe snapshot endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' + it_behaves_like 'handling empty values for username and channel' context 'with existing package' do it 'returns a hash of files with their md5 hashes' do @@ -324,6 +373,7 @@ RSpec.shared_examples 'package snapshot endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' + it_behaves_like 'handling empty values for username and channel' context 'with existing package' do it 'returns a hash of md5 values for the files' do @@ -344,12 +394,14 @@ 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' + it_behaves_like 'handling empty values for username and channel' 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' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'recipe upload_urls endpoint' do @@ -362,6 +414,7 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'handling empty values for username and channel' it 'returns a set of upload urls for the files requested' do subject @@ -423,6 +476,7 @@ RSpec.shared_examples 'package upload_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'handling empty values for username and channel' it 'returns a set of upload urls for the files requested' do expected_response = { @@ -458,6 +512,7 @@ RSpec.shared_examples 'delete package endpoint' do let(:recipe_path) { package.conan_recipe_path } it_behaves_like 'rejects invalid recipe' + it_behaves_like 'handling empty values for username and channel' it 'returns unauthorized for users without valid permission' do subject @@ -568,12 +623,14 @@ 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' + it_behaves_like 'handling empty values for username and channel' 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' + it_behaves_like 'handling empty values for username and channel' context 'tracking the conan_package.tgz download' do let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } @@ -598,6 +655,7 @@ 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' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'workhorse recipe file upload endpoint' do @@ -619,6 +677,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'workhorse package file upload endpoint' do @@ -640,6 +699,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' + it_behaves_like 'handling empty values for username and channel' context 'tracking the conan_package.tgz upload' do let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } 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 62dbac3fd4d..8bffd1f71e9 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 @@ -18,19 +18,19 @@ RSpec.shared_examples 'snippet edit usage data counters' do end end - context 'when user is not sessionless' do + context 'when user is not sessionless', :clean_gitlab_redis_sessions 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| + Gitlab::Redis::Sessions.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 + it 'tracks usage data actions', :clean_gitlab_redis_sessions do expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action) post_graphql_mutation(mutation) diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 367c6d4fa3a..882c79cb03f 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -55,7 +55,7 @@ RSpec.shared_examples 'group and project packages query' do end it 'deals with metadata' do - expect(target_shas).to contain_exactly(composer_metadatum.target_sha) + expect(target_shas.compact).to contain_exactly(composer_metadatum.target_sha) end it 'returns the count of the packages' do diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb index 673d7741017..c5e5803c0a7 100644 --- a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -28,4 +28,34 @@ RSpec.shared_examples 'issuable participants endpoint' do expect(response).to have_gitlab_http_status(:not_found) end + + context 'with a confidential note' do + let!(:note) do + create( + :note, + :confidential, + project: project, + noteable: entity, + author: create(:user) + ) + end + + it 'returns a full list of participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user) + + expect(response).to have_gitlab_http_status(:ok) + participant_ids = json_response.map { |el| el['id'] } + expect(participant_ids).to match_array([entity.author_id, note.author_id]) + end + + context 'when user cannot see a confidential note' do + it 'returns a limited list of participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", create(:user)) + + expect(response).to have_gitlab_http_status(:ok) + participant_ids = json_response.map { |el| el['id'] } + expect(participant_ids).to match_array([entity.author_id]) + end + end + end end diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 19677e92001..8d6d85732be 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -41,19 +41,6 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| # query count can slightly change between the examples so we're using a custom threshold expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) end - - context 'with packages_npm_abbreviated_metadata disabled' do - before do - stub_feature_flags(packages_npm_abbreviated_metadata: false) - end - - it 'calls the presenter without including metadata' do - expect(::Packages::Npm::PackagePresenter) - .to receive(:new).with(anything, anything, include_metadata: false).and_call_original - - subject - end - end end shared_examples 'reject metadata request' do |status:| 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 878cbc10a24..6568d51b90e 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 @@ -391,7 +391,7 @@ RSpec.shared_examples 'rejects nuget access with invalid target id' do context 'with a target id with invalid integers' do using RSpec::Parameterized::TableSyntax - let(:target) { OpenStruct.new(id: id) } + let(:target) { double(id: id) } where(:id, :status) do '/../' | :bad_request @@ -411,7 +411,7 @@ end RSpec.shared_examples 'rejects nuget access with unknown target id' do context 'with an unknown target' do - let(:target) { OpenStruct.new(id: 1234567890) } + let(:target) { double(id: 1234567890) } context 'as anonymous' do it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized 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 06c51add438..aff086d1ba3 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 @@ -346,7 +346,8 @@ RSpec.shared_examples 'a pypi user namespace endpoint' do end with_them do - let_it_be_with_reload(:group) { create(:namespace) } + # only groups are supported, so this "group" is actually the wrong namespace type + let_it_be_with_reload(:group) { create(:user_namespace) } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } before do diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb index c979fdc2bb0..7fd20fc3909 100644 --- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb +++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb @@ -126,7 +126,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do SHA256: #{package_files[4].file_sha256} EOF - expected_main_source_content = <<~EOF + expected_main_sources_content = <<~EOF Package: #{package.name} Binary: sample-dev, libsample0, sample-udeb Version: #{package.version} @@ -158,7 +158,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content) check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil) - check_component_file(current_time.round, 'main', :source, nil, expected_main_source_content) + check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content) check_component_file(current_time.round, 'contrib', :packages, 'all', nil) check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) @@ -168,7 +168,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil) check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil) - check_component_file(current_time.round, 'contrib', :source, nil, nil) + check_component_file(current_time.round, 'contrib', :sources, nil, nil) main_amd64_size = expected_main_amd64_content.length main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content) @@ -182,9 +182,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do main_amd64_di_md5sum = Digest::MD5.hexdigest(expected_main_amd64_di_content) main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content) - main_source_size = expected_main_source_content.length - main_source_md5sum = Digest::MD5.hexdigest(expected_main_source_content) - main_source_sha256 = Digest::SHA256.hexdigest(expected_main_source_content) + main_sources_size = expected_main_sources_content.length + main_sources_md5sum = Digest::MD5.hexdigest(expected_main_sources_content) + main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) expected_release_content = <<~EOF Codename: unstable @@ -199,14 +199,14 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages - d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Source + d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Sources d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-all/Packages #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages #{main_amd64_di_md5sum} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-arm64/Packages - #{main_source_md5sum} #{main_source_size} main/source/Source + #{main_sources_md5sum} #{main_sources_size} main/source/Sources SHA256: #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages @@ -214,14 +214,14 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-arm64/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Source + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages #{main_amd64_di_sha256} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages - #{main_source_sha256} #{main_source_size} main/source/Source + #{main_sources_sha256} #{main_sources_size} main/source/Sources EOF check_release_files(expected_release_content) diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb new file mode 100644 index 00000000000..0d3e158d358 --- /dev/null +++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it runs background migration jobs' do |tracking_database, metric_name| + describe 'defining the job attributes' do + it 'defines the data_consistency as always' do + expect(described_class.get_data_consistency).to eq(:always) + end + + it 'defines the retry count in sidekiq_options' do + expect(described_class.sidekiq_options['retry']).to eq(3) + end + + it 'defines the feature_category as database' do + expect(described_class.get_feature_category).to eq(:database) + end + + it 'defines the urgency as throttled' do + expect(described_class.get_urgency).to eq(:throttled) + end + + it 'defines the loggable_arguments' do + expect(described_class.loggable_arguments).to match_array([0, 1]) + end + end + + describe '.tracking_database' do + it 'does not raise an error' do + expect { described_class.tracking_database }.not_to raise_error + end + + it 'overrides the method to return the tracking database' do + expect(described_class.tracking_database).to eq(tracking_database) + end + end + + describe '.unhealthy_metric_name' do + it 'does not raise an error' do + expect { described_class.unhealthy_metric_name }.not_to raise_error + end + + it 'overrides the method to return the unhealthy metric name' do + expect(described_class.unhealthy_metric_name).to eq(metric_name) + end + end + + describe '.minimum_interval' do + it 'returns 2 minutes' do + expect(described_class.minimum_interval).to eq(2.minutes.to_i) + end + end + + describe '#perform' do + let(:worker) { described_class.new } + + before do + allow(worker).to receive(:jid).and_return(1) + allow(worker).to receive(:always_perform?).and_return(false) + + allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false) + end + + it 'performs jobs using the coordinator for the worker' do + expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator| + allow(coordinator).to receive(:with_shared_connection).and_yield + + expect(coordinator.worker_class).to eq(described_class) + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + end + + worker.perform('Foo', [10, 20]) + end + + context 'when lease can be obtained' do + let(:coordinator) { double('job coordinator') } + + before do + allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database) + .with(tracking_database) + .and_return(coordinator) + + allow(coordinator).to receive(:with_shared_connection).and_yield + end + + it 'sets up the shared connection before checking replication' do + expect(coordinator).to receive(:with_shared_connection).and_yield.ordered + expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered + + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) + end + + it 'performs a background migration' do + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) + end + + context 'when lease_attempts is 1' do + it 'performs a background migration' do + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20], 1) + end + end + + it 'can run scheduled job and retried job concurrently' do + expect(coordinator) + .to receive(:perform) + .with('Foo', [10, 20]) + .exactly(2).time + + worker.perform('Foo', [10, 20]) + worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1) + end + + it 'sets the class that will be executed as the caller_id' do + expect(coordinator).to receive(:perform) do + expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo') + end + + worker.perform('Foo', [10, 20]) + end + end + + context 'when lease not obtained (migration of same class was performed recently)' do + let(:timeout) { described_class.minimum_interval } + let(:lease_key) { "#{described_class.name}:Foo" } + let(:coordinator) { double('job coordinator') } + + before do + allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database) + .with(tracking_database) + .and_return(coordinator) + + allow(coordinator).to receive(:with_shared_connection).and_yield + + expect(coordinator).not_to receive(:perform) + + Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain + end + + it 'reschedules the migration and decrements the lease_attempts' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 4) + + worker.perform('Foo', [10, 20], 5) + end + + context 'when lease_attempts is 1' do + let(:lease_key) { "#{described_class.name}:Foo:retried" } + + it 'reschedules the migration and decrements the lease_attempts' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 0) + + worker.perform('Foo', [10, 20], 1) + end + end + + context 'when lease_attempts is 0' do + let(:lease_key) { "#{described_class.name}:Foo:retried" } + + it 'gives up performing the migration' do + expect(described_class).not_to receive(:perform_in) + expect(Sidekiq.logger).to receive(:warn).with( + class: 'Foo', + message: 'Job could not get an exclusive lease after several tries. Giving up.', + job_id: 1) + + worker.perform('Foo', [10, 20], 0) + end + end + end + + context 'when database is not healthy' do + before do + expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true) + end + + it 'reschedules a migration if the database is not healthy' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 4) + + worker.perform('Foo', [10, 20]) + end + + it 'increments the unhealthy counter' do + counter = Gitlab::Metrics.counter(metric_name, 'msg') + + expect(described_class).to receive(:perform_in) + + expect { worker.perform('Foo', [10, 20]) }.to change { counter.get }.by(1) + end + + context 'when lease_attempts is 0' do + it 'gives up performing the migration' do + expect(described_class).not_to receive(:perform_in) + expect(Sidekiq.logger).to receive(:warn).with( + class: 'Foo', + message: 'Database was unhealthy after several tries. Giving up.', + job_id: 1) + + worker.perform('Foo', [10, 20], 0) + end + end + end + end +end |