diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-20 14:18:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-20 14:18:08 +0300 |
commit | 5afcbe03ead9ada87621888a31a62652b10a7e4f (patch) | |
tree | 9918b67a0d0f0bafa6542e839a8be37adf73102d /spec/support | |
parent | c97c0201564848c1f53226fe19d71fdcc472f7d0 (diff) |
Add latest changes from gitlab-org/gitlab@16-4-stable-eev16.4.0-rc42
Diffstat (limited to 'spec/support')
105 files changed, 2318 insertions, 753 deletions
diff --git a/spec/support/before_all_adapter.rb b/spec/support/before_all_adapter.rb index f4946ff271f..35846fcecb8 100644 --- a/spec/support/before_all_adapter.rb +++ b/spec/support/before_all_adapter.rb @@ -25,20 +25,9 @@ module TestProfBeforeAllAdapter end end - # This class is required so we can disable transactions on migration specs - module NoTransactionAdapter - def self.begin_transaction; end - - def self.rollback_transaction; end - end - def self.default_adapter MultipleDatabaseAdapter end - - def self.no_transaction_adapter - NoTransactionAdapter - end end TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.default_adapter diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 392743fda4a..65abbe12621 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -54,11 +54,7 @@ Capybara.register_server :puma_via_workhorse do |app, port, host, **options| # In cases of multiple installations of chromedriver, prioritize the version installed by SeleniumManager # selenium-manager doesn't work with Linux arm64 yet: # https://github.com/SeleniumHQ/selenium/issues/11357 - if RUBY_PLATFORM.include?('x86_64-linux') || - # Rosetta is required on macOS because the selenium-manager - # binaries (https://github.com/SeleniumHQ/selenium/tree/trunk/common/manager/macos) - # are only compiled for macOS x86. - (RUBY_PLATFORM.include?('darwin') && system('/usr/bin/pgrep -q oahd')) + if RUBY_PLATFORM.include?('x86_64-linux') || RUBY_PLATFORM.include?('darwin') chrome_options = Selenium::WebDriver::Chrome::Options.chrome chromedriver_path = File.dirname(Selenium::WebDriver::SeleniumManager.driver_path(chrome_options)) ENV['PATH'] = "#{chromedriver_path}:#{ENV['PATH']}" # rubocop:disable RSpec/EnvAssignment diff --git a/spec/support/capybara_wait_for_all_requests.rb b/spec/support/capybara_wait_for_all_requests.rb index 36b63619b08..6f272474cf6 100644 --- a/spec/support/capybara_wait_for_all_requests.rb +++ b/spec/support/capybara_wait_for_all_requests.rb @@ -9,9 +9,11 @@ module Capybara include CapybaraHelpers include WaitForRequests - def visit(visit_uri) + def visit(visit_uri, &block) super + yield if block + wait_for_all_requests end end @@ -24,24 +26,26 @@ module Capybara include CapybaraHelpers include WaitForRequests - module WaitForAllRequestsAfterClickButton + module WaitForRequestsAfterClickButton def click_button(locator = nil, **options) super - wait_for_all_requests + wait_for_requests end end - module WaitForAllRequestsAfterClickLink - def click_link(locator = nil, **options) + module WaitForRequestsAfterClickLink + def click_link(locator = nil, **options, &block) super - wait_for_all_requests + yield if block + + wait_for_requests end end - prepend WaitForAllRequestsAfterClickButton - prepend WaitForAllRequestsAfterClickLink + prepend WaitForRequestsAfterClickButton + prepend WaitForRequestsAfterClickLink end end end diff --git a/spec/support/database/auto_explain.rb b/spec/support/database/auto_explain.rb index 108d88e37b9..799457034a1 100644 --- a/spec/support/database/auto_explain.rb +++ b/spec/support/database/auto_explain.rb @@ -115,11 +115,16 @@ module AutoExplain private def record_auto_explain?(connection) - ENV['CI'] \ - && ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries') \ - && ENV['CI_JOB_NAME_SLUG'] != 'db-migrate-non-superuser' \ - && connection.database_version.to_s[0..1].to_i >= 14 \ - && connection.select_one('SHOW is_superuser')['is_superuser'] == 'on' + return false unless ENV['CI'] + return false if ENV['CI_JOB_NAME_SLUG'] == 'db-migrate-non-superuser' + return false if connection.database_version.to_s[0..1].to_i < 14 + return false if connection.select_one('SHOW is_superuser')['is_superuser'] != 'on' + + # This condition matches the pipeline rules for if-merge-request-labels-record-queries + return true if ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries') + + # This condition matches the pipeline rules for if-default-branch-refs + ENV['CI_COMMIT_REF_NAME'] == ENV['CI_DEFAULT_BRANCH'] && !ENV['CI_MERGE_REQUEST_IID'] end end end diff --git a/spec/support/database/click_house/hooks.rb b/spec/support/database/click_house/hooks.rb index 27abd19dc3f..b970d3daf84 100644 --- a/spec/support/database/click_house/hooks.rb +++ b/spec/support/database/click_house/hooks.rb @@ -4,7 +4,13 @@ class ClickHouseTestRunner def truncate_tables ClickHouse::Client.configuration.databases.each_key do |db| - tables_for(db).each do |table| + # Select tables with at least one row + query = tables_for(db).map do |table| + "(SELECT '#{table}' AS table FROM #{table} LIMIT 1)" + end.join(' UNION ALL ') + + tables_with_data = ClickHouse::Client.select(query, db).pluck('table') + tables_with_data.each do |table| ClickHouse::Client.execute("TRUNCATE TABLE #{table}", db) end end diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb index 77fa7feacd4..02572d011f7 100644 --- a/spec/support/database/prevent_cross_database_modification.rb +++ b/spec/support/database/prevent_cross_database_modification.rb @@ -1,15 +1,53 @@ # frozen_string_literal: true -module PreventCrossDatabaseModificationSpecHelpers - delegate :with_cross_database_modification_prevented, - :allow_cross_database_modification_within_transaction, - to: :'::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification' +module Database + module PreventCrossDatabaseModificationSpecHelpers + delegate :with_cross_database_modification_prevented, + :allow_cross_database_modification_within_transaction, + to: :'::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification' + end + + module AllowCrossDatabaseFactoryBotBuilt + extend ActiveSupport::Concern + + attr_accessor :factory_bot_built + + prepended do + around_create :_test_ignore_table_in_transaction, prepend: true, if: :factory_bot_built? + + def _test_ignore_table_in_transaction(&blk) + Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( + [self.class.table_name], url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130277', &blk + ) + end + end + + def factory_bot_built? + return false unless Rails.env.test? + + !!factory_bot_built + end + + private + + def ignore_cross_database_tables_if_factory_bot(tables, &blk) + return super unless factory_bot_built? + + Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction( + tables, + url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130277', + &blk + ) + end + end end +ActiveRecord::Base.prepend(Database::AllowCrossDatabaseFactoryBotBuilt) + CROSS_DB_MODIFICATION_ALLOW_LIST = Set.new(YAML.load_file(File.join(__dir__, 'cross-database-modification-allowlist.yml'))).freeze RSpec.configure do |config| - config.include(PreventCrossDatabaseModificationSpecHelpers) + config.include(Database::PreventCrossDatabaseModificationSpecHelpers) # By default allow cross-modifications as we want to observe only transactions # within a specific block of execution which is defined be `before(:each)` and `after(:each)` diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 7bd1f0c5dfa..1ffc1cc25fd 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -12,14 +12,4 @@ RSpec.configure do |config| setup_database_cleaner DatabaseCleaner.clean_with(:deletion) end - - config.around(:each, :delete) do |example| - self.class.use_transactional_tests = false - - example.run - - delete_from_all_tables!(except: deletion_except_tables) - - self.class.use_transactional_tests = true - end end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 17b4270fa20..3131a22a20b 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -73,7 +73,10 @@ module DbCleaner end end + disable_ddl_was = Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops) + stub_feature_flags(disallow_database_ddl_feature_flags: false) Gitlab::Database::Partitioning.sync_partitions_ignore_db_error + stub_feature_flags(disallow_database_ddl_feature_flags: disable_ddl_was) puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}" end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb index 6faa2db3330..d30098a5cc0 100644 --- a/spec/support/factory_bot.rb +++ b/spec/support/factory_bot.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true +FactoryBot.define do + after(:build) do |object, _| + next unless object.respond_to?(:factory_bot_built=) + + object.factory_bot_built = true + end + + before(:create) do |object, _| + next unless object.respond_to?(:factory_bot_built=) + + object.factory_bot_built = false + end +end + FactoryBot::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods include StubMethodCalls diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index 5de8e8cdca2..860045c6ce6 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -7,6 +7,7 @@ - Namespaces::FreeUserCap::UsersFinder # Reason: There is no need to have anything else besides the count - Groups::EnvironmentScopesFinder # Reason: There is no need to have anything else besides the simple strucutre with the scope name - Security::RelatedPipelinesFinder # Reason: There is no need to have anything else besides the IDs of pipelines +- Llm::ExtraResourceFinder # Reason: The finder does not deal with DB-backend resource for now. # Temporary excludes (aka TODOs) # For example: @@ -61,11 +62,6 @@ - Security::PipelineVulnerabilitiesFinder - Security::ScanExecutionPoliciesFinder - Security::ScanResultPoliciesFinder -- Security::TrainingProviders::BaseUrlFinder -- Security::TrainingUrlsFinder -- Security::TrainingProviders::KontraUrlFinder -- Security::TrainingProviders::SecureCodeWarriorUrlFinder -- Security::TrainingProviders::SecureFlagUrlFinder - SentryIssueFinder - ServerlessDomainFinder - TagsFinder diff --git a/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml index 583d44c452e..097cbf5b0c8 100644 --- a/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml +++ b/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml @@ -1,3 +1,6 @@ +stages: + - dast + dast: stage: dast image: @@ -7,4 +10,6 @@ dast: allow_failure: true dast_configuration: site_profile: "site_profile_name_included" - scanner_profile: "scanner_profile_name_included"
\ No newline at end of file + scanner_profile: "scanner_profile_name_included" + script: + - echo "Runs DAST!" diff --git a/spec/support/helpers/database/duplicate_indexes.rb b/spec/support/helpers/database/duplicate_indexes.rb new file mode 100644 index 00000000000..0ad8ee1e055 --- /dev/null +++ b/spec/support/helpers/database/duplicate_indexes.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Database + class DuplicateIndexes + attr_accessor :table_name, :indexes + + BTREE_INDEX_STRUCT = Struct.new(:name, :columns, :unique) + + def initialize(table_name, indexes) + @table_name = table_name + @indexes = indexes + end + + def duplicate_indexes + ret = {} + + btree_indexes.each do |btree_index| + matching_indexes = matching_indexes_for(btree_index) + next unless matching_indexes.any? + + ret[btree_index] = matching_indexes + end + + ret + end + + def self.btree_index_struct(index) + BTREE_INDEX_STRUCT.new( + index.name, + Array.wrap(index.columns).map do |column| + # https://apidock.com/rails/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements/indexes + # asc is the default order + column_order = index.orders.is_a?(Symbol) ? index.orders : (index.orders[column] || :asc) + { name: column, order: column_order } + end, + index.unique + ) + end + + private + + def btree_indexes + return @btree_indexes if @btree_indexes + + # We only scan non-conditional btree indexes + @btree_indexes = indexes.select do |index| + index.using == :btree && index.where.nil? && index.opclasses.blank? + end + + @btree_indexes = @btree_indexes.map { |index| self.class.btree_index_struct(index) } + end + + def matching_indexes_for(btree_index) + all_matching_indexes = [] + + # When comparing btree_index with other_index. btree_index is the index that can have more columns + # than the other_index. + (1..btree_index.columns.length).each do |subset_length| + columns = btree_index.columns.first(subset_length) + matching_indexes = btree_indexes.reject { |other_index| other_index == btree_index }.select do |other_index| + other_index.columns == columns + end + + # For now we ignore other indexes that are UNIQUE and have a matching columns subset of + # the btree_index columns, as UNIQUE indexes are still needed to enforce uniqueness + # constraints on subset of the columns. + matching_indexes = matching_indexes.reject do |other_index| + (other_index.unique && (other_index.columns.length < btree_index.columns.length)) + end + + all_matching_indexes += matching_indexes + end + + all_matching_indexes + end + end +end diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml new file mode 100644 index 00000000000..02efdabd70b --- /dev/null +++ b/spec/support/helpers/database/duplicate_indexes.yml @@ -0,0 +1,265 @@ +--- +# It maps table_name to {index1: array_of_duplicate_indexes, index2: array_of_duplicate_indexes, ... } +abuse_reports: + idx_abuse_reports_user_id_status_and_category: + - index_abuse_reports_on_user_id +alert_management_http_integrations: + index_http_integrations_on_project_and_endpoint: + - index_alert_management_http_integrations_on_project_id +analytics_cycle_analytics_group_stages: + index_group_stages_on_group_id_group_value_stream_id_and_name: + - index_analytics_ca_group_stages_on_group_id +approval_project_rules_users: + index_approval_project_rules_users_1: + - index_approval_project_rules_users_on_approval_project_rule_id +approvals: + index_approvals_on_merge_request_id_and_created_at: + - index_approvals_on_merge_request_id +board_group_recent_visits: + index_board_group_recent_visits_on_user_group_and_board: + - index_board_group_recent_visits_on_user_id +board_project_recent_visits: + index_board_project_recent_visits_on_user_project_and_board: + - index_board_project_recent_visits_on_user_id +board_user_preferences: + index_board_user_preferences_on_user_id_and_board_id: + - index_board_user_preferences_on_user_id +boards_epic_board_recent_visits: + index_epic_board_recent_visits_on_user_group_and_board: + - index_boards_epic_board_recent_visits_on_user_id +boards_epic_user_preferences: + index_boards_epic_user_preferences_on_board_user_epic_unique: + - index_boards_epic_user_preferences_on_board_id +bulk_import_batch_trackers: + i_bulk_import_trackers_id_batch_number: + - index_bulk_import_batch_trackers_on_tracker_id +bulk_import_export_batches: + i_bulk_import_export_batches_id_batch_number: + - index_bulk_import_export_batches_on_export_id +ci_job_artifacts: + index_ci_job_artifacts_on_id_project_id_and_created_at: + - index_ci_job_artifacts_on_project_id + index_ci_job_artifacts_on_id_project_id_and_file_type: + - index_ci_job_artifacts_on_project_id + index_ci_job_artifacts_on_project_id_and_id: + - index_ci_job_artifacts_on_project_id +ci_pipeline_artifacts: + index_ci_pipeline_artifacts_on_pipeline_id_and_file_type: + - index_ci_pipeline_artifacts_on_pipeline_id +ci_stages: + index_ci_stages_on_pipeline_id_and_name: + - index_ci_stages_on_pipeline_id + index_ci_stages_on_pipeline_id_and_position: + - index_ci_stages_on_pipeline_id + index_ci_stages_on_pipeline_id_convert_to_bigint_and_name: + - index_ci_stages_on_pipeline_id_convert_to_bigint + index_ci_stages_on_pipeline_id_convert_to_bigint_and_position: + - index_ci_stages_on_pipeline_id_convert_to_bigint +dast_site_tokens: + index_dast_site_token_on_project_id_and_url: + - index_dast_site_tokens_on_project_id +design_management_designs: + index_design_management_designs_on_iid_and_project_id: + - index_design_management_designs_on_project_id +design_management_designs_versions: + design_management_designs_versions_uniqueness: + - index_design_management_designs_versions_on_design_id +error_tracking_errors: + index_et_errors_on_project_id_and_status_and_id: + - index_error_tracking_errors_on_project_id + index_et_errors_on_project_id_and_status_events_count_id_desc: + - index_error_tracking_errors_on_project_id + index_et_errors_on_project_id_and_status_first_seen_at_id_desc: + - index_error_tracking_errors_on_project_id + index_et_errors_on_project_id_and_status_last_seen_at_id_desc: + - index_error_tracking_errors_on_project_id +geo_node_namespace_links: + index_geo_node_namespace_links_on_geo_node_id_and_namespace_id: + - index_geo_node_namespace_links_on_geo_node_id +in_product_marketing_emails: + index_in_product_marketing_emails_on_user_campaign: + - index_in_product_marketing_emails_on_user_id + index_in_product_marketing_emails_on_user_track_series: + - index_in_product_marketing_emails_on_user_id +incident_management_oncall_participants: + index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id: + - index_inc_mgmnt_oncall_participants_on_oncall_user_id +incident_management_oncall_schedules: + index_im_oncall_schedules_on_project_id_and_iid: + - index_incident_management_oncall_schedules_on_project_id +instance_audit_events_streaming_headers: + idx_instance_external_audit_event_destination_id_key_uniq: + - idx_headers_instance_external_audit_event_destination_id +issue_links: + index_issue_links_on_source_id_and_target_id: + - index_issue_links_on_source_id +issues: + index_issues_on_author_id_and_id_and_created_at: + - index_issues_on_author_id +jira_connect_subscriptions: + idx_jira_connect_subscriptions_on_installation_id_namespace_id: + - idx_jira_connect_subscriptions_on_installation_id +list_user_preferences: + index_list_user_preferences_on_user_id_and_list_id: + - index_list_user_preferences_on_user_id +member_tasks: + index_member_tasks_on_member_id_and_project_id: + - index_member_tasks_on_member_id +members: + index_members_on_member_namespace_id_compound: + - index_members_on_member_namespace_id +merge_request_assignees: + index_merge_request_assignees_on_merge_request_id_and_user_id: + - index_merge_request_assignees_on_merge_request_id +merge_request_metrics: + index_mr_metrics_on_target_project_id_merged_at_nulls_last: + - index_merge_request_metrics_on_target_project_id +merge_requests: + index_merge_requests_on_author_id_and_created_at: + - index_merge_requests_on_author_id + index_merge_requests_on_author_id_and_id: + - index_merge_requests_on_author_id + index_merge_requests_on_author_id_and_target_project_id: + - index_merge_requests_on_author_id +ml_candidate_params: + index_ml_candidate_params_on_candidate_id_on_name: + - index_ml_candidate_params_on_candidate_id +ml_candidates: + index_ml_candidates_on_project_id_on_internal_id: + - index_ml_candidates_on_project_id +ml_model_versions: + index_ml_model_versions_on_project_id_and_model_id_and_version: + - index_ml_model_versions_on_project_id +ml_models: + index_ml_models_on_project_id_and_name: + - index_ml_models_on_project_id +p_ci_runner_machine_builds: + index_p_ci_runner_machine_builds_on_runner_machine_id: + - index_ci_runner_machine_builds_on_runner_machine_id +packages_debian_group_distributions: + uniq_pkgs_debian_group_distributions_group_id_and_codename: + - index_packages_debian_group_distributions_on_group_id + uniq_pkgs_debian_group_distributions_group_id_and_suite: + - index_packages_debian_group_distributions_on_group_id +packages_debian_project_distributions: + uniq_pkgs_debian_project_distributions_project_id_and_codename: + - index_packages_debian_project_distributions_on_project_id + uniq_pkgs_debian_project_distributions_project_id_and_suite: + - index_packages_debian_project_distributions_on_project_id +packages_tags: + index_packages_tags_on_package_id_and_updated_at: + - index_packages_tags_on_package_id +pages_domains: + index_pages_domains_on_project_id_and_enabled_until: + - index_pages_domains_on_project_id + index_pages_domains_on_verified_at_and_enabled_until: + - index_pages_domains_on_verified_at +personal_access_tokens: + index_pat_on_user_id_and_expires_at: + - index_personal_access_tokens_on_user_id +pm_affected_packages: + i_affected_packages_unique_for_upsert: + - index_pm_affected_packages_on_pm_advisory_id +pm_package_version_licenses: + i_pm_package_version_licenses_join_ids: + - index_pm_package_version_licenses_on_pm_package_version_id +pm_package_versions: + i_pm_package_versions_on_package_id_and_version: + - index_pm_package_versions_on_pm_package_id +project_compliance_standards_adherence: + u_project_compliance_standards_adherence_for_reporting: + - index_project_compliance_standards_adherence_on_project_id +project_relation_exports: + index_project_export_job_relation: + - index_project_relation_exports_on_project_export_job_id +project_repositories: + index_project_repositories_on_shard_id_and_project_id: + - index_project_repositories_on_shard_id +project_topics: + index_project_topics_on_project_id_and_topic_id: + - index_project_topics_on_project_id +projects: + index_projects_api_path_id_desc: + - index_on_projects_path + index_projects_on_path_and_id: + - index_on_projects_path +protected_environments: + index_protected_environments_on_project_id_and_name: + - index_protected_environments_on_project_id +protected_tags: + index_protected_tags_on_project_id_and_name: + - index_protected_tags_on_project_id +related_epic_links: + index_related_epic_links_on_source_id_and_target_id: + - index_related_epic_links_on_source_id +requirements_management_test_reports: + idx_test_reports_on_issue_id_created_at_and_id: + - index_requirements_management_test_reports_on_issue_id +sbom_component_versions: + index_sbom_component_versions_on_component_id_and_version: + - index_sbom_component_versions_on_component_id +sbom_occurrences: + index_sbom_occurrences_for_input_file_path_search: + - index_sbom_occurrences_on_project_id_component_id + - index_sbom_occurrences_on_project_id + idx_sbom_occurrences_on_project_id_and_source_id: + - index_sbom_occurrences_on_project_id + index_sbom_occurrences_on_project_id_and_id: + - index_sbom_occurrences_on_project_id + index_sbom_occurrences_on_project_id_component_id: + - index_sbom_occurrences_on_project_id + index_sbom_occurrences_on_project_id_and_component_id_and_id: + - index_sbom_occurrences_on_project_id_component_id + - index_sbom_occurrences_on_project_id + index_sbom_occurrences_on_project_id_and_package_manager: + - index_sbom_occurrences_on_project_id +scan_result_policies: + index_scan_result_policies_on_position_in_configuration: + - index_scan_result_policies_on_policy_configuration_id +search_namespace_index_assignments: + index_search_namespace_index_assignments_uniqueness_index_type: + - index_search_namespace_index_assignments_on_namespace_id + index_search_namespace_index_assignments_uniqueness_on_index_id: + - index_search_namespace_index_assignments_on_namespace_id +sprints: + sequence_is_unique_per_iterations_cadence_id: + - index_sprints_iterations_cadence_id +taggings: + taggings_idx: + - index_taggings_on_tag_id +term_agreements: + term_agreements_unique_index: + - index_term_agreements_on_user_id +todos: + index_todos_on_author_id_and_created_at: + - index_todos_on_author_id +user_callouts: + index_user_callouts_on_user_id_and_feature_name: + - index_user_callouts_on_user_id +users: + index_users_on_state_and_user_type: + - index_users_on_state +vulnerabilities: + index_vulnerabilities_project_id_state_severity_default_branch: + - index_vulnerabilities_on_project_id_and_state_and_severity +vulnerability_external_issue_links: + idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue: + - index_vulnerability_external_issue_links_on_vulnerability_id +vulnerability_finding_links: + finding_link_name_url_idx: + - finding_links_on_vulnerability_occurrence_id +vulnerability_finding_signatures: + idx_vuln_signatures_uniqueness_signature_sha: + - index_vulnerability_finding_signatures_on_finding_id +vulnerability_flags: + index_vulnerability_flags_on_unique_columns: + - index_vulnerability_flags_on_vulnerability_occurrence_id +web_hook_logs: + index_web_hook_logs_on_web_hook_id_and_created_at: + - index_web_hook_logs_part_on_web_hook_id +web_hooks: + index_web_hooks_on_project_id_recent_failures: + - index_web_hooks_on_project_id +work_item_hierarchy_restrictions: + index_work_item_hierarchy_restrictions_on_parent_and_child: + - index_work_item_hierarchy_restrictions_on_parent_type_id diff --git a/spec/support/helpers/features/admin_users_helpers.rb b/spec/support/helpers/features/admin_users_helpers.rb index 9a87ccf113a..b4477537a40 100644 --- a/spec/support/helpers/features/admin_users_helpers.rb +++ b/spec/support/helpers/features/admin_users_helpers.rb @@ -4,7 +4,7 @@ module Features module AdminUsersHelpers def click_user_dropdown_toggle(user_id) page.within("[data-testid='user-actions-#{user_id}']") do - find("[data-testid='dropdown-toggle']").click + find("[data-testid='user-actions-dropdown-toggle']").click end end diff --git a/spec/support/helpers/features/highlight_content_helper.rb b/spec/support/helpers/features/highlight_content_helper.rb new file mode 100644 index 00000000000..f55dd213061 --- /dev/null +++ b/spec/support/helpers/features/highlight_content_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# This helper allows you to reliably highlight text within a given Element by +# simulating mouse actions. +# +module Features + module HighlightContentHelper + def highlight_content(node) + height = node.native.rect.height + width = node.native.rect.width + page.driver.browser.action + .move_to(node.native, -(width / 2), -(height / 2)) + .click_and_hold + .move_by(width, height) + .release + .perform + end + end +end diff --git a/spec/support/helpers/features/runners_helpers.rb b/spec/support/helpers/features/runners_helpers.rb index dbd1edade8c..7c3618ee799 100644 --- a/spec/support/helpers/features/runners_helpers.rb +++ b/spec/support/helpers/features/runners_helpers.rb @@ -23,11 +23,13 @@ module Features def input_filtered_search_keys(search_term) focus_filtered_search - page.find(search_bar_selector).find('input').send_keys(search_term) - # blur input - find('body').click + page.within(search_bar_selector) do + send_keys(search_term) + send_keys(:enter) + + click_on 'Search' + end - page.click_on 'Search' wait_for_requests end diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 60638eb06cd..abd5d78e836 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -155,7 +155,7 @@ module FilteredSearchHelpers end def default_placeholder - 'Search or filter results...' + 'Search or filter results…' end def get_filtered_search_placeholder diff --git a/spec/support/helpers/loose_foreign_keys_helper.rb b/spec/support/helpers/loose_foreign_keys_helper.rb new file mode 100644 index 00000000000..c83c60d72ed --- /dev/null +++ b/spec/support/helpers/loose_foreign_keys_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Helper to process deletions of associated records created via loose foreign keys + +module LooseForeignKeysHelper + def process_loose_foreign_key_deletions(record:) + LooseForeignKeys::DeletedRecord.using_connection(record.connection) do + LooseForeignKeys::ProcessDeletedRecordsService.new(connection: record.connection).execute + end + end +end diff --git a/spec/support/helpers/sign_up_helpers.rb b/spec/support/helpers/sign_up_helpers.rb new file mode 100644 index 00000000000..6259467232c --- /dev/null +++ b/spec/support/helpers/sign_up_helpers.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module SignUpHelpers + def fill_in_sign_up_form(new_user, submit_button_text = 'Register') + fill_in 'new_user_first_name', with: new_user.first_name + fill_in 'new_user_last_name', with: new_user.last_name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_password', with: new_user.password + + wait_for_all_requests + + expect_username_to_be_validated + + yield if block_given? + + click_button submit_button_text + end + + private + + def expect_username_to_be_validated + expect(page).to have_selector('[data-testid="new-user-username-field"].gl-field-success-outline') + end +end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index 6d0e97b0a75..c02ffe07159 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -23,13 +23,17 @@ module StubGitlabCalls end def stub_ci_pipeline_yaml_file(ci_yaml_content) - allow_any_instance_of(Gitlab::Ci::ProjectConfig::Repository) - .to receive(:file_in_repository?) - .and_return(ci_yaml_content.present?) + blob = instance_double(Blob, empty?: ci_yaml_content.blank?, data: ci_yaml_content) + allow(blob).to receive(:load_all_data!) allow_any_instance_of(Repository) - .to receive(:gitlab_ci_yml_for) - .and_return(ci_yaml_content) + .to receive(:blob_at) + .and_call_original + + allow_any_instance_of(Repository) + .to receive(:blob_at) + .with(String, '.gitlab-ci.yml') + .and_return(blob) # Ensure we don't hit auto-devops when config not found in repository unless ci_yaml_content diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb index 1dc8b1d4845..aa5c360d953 100644 --- a/spec/support/helpers/x509_helpers.rb +++ b/spec/support/helpers/x509_helpers.rb @@ -173,6 +173,10 @@ module X509Helpers Time.at(1561027326) end + def signed_tag_time + Time.at(1574261780) + end + def signed_tag_signature <<~SIGNATURE -----BEGIN SIGNED MESSAGE----- @@ -337,6 +341,10 @@ module X509Helpers 'r.meier@siemens.com' end + def tag_email + 'dmitriy.zaporozhets@gmail.com' + end + def certificate_issuer 'CN=Siemens Issuing CA EE Auth 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA2,O=Siemens,L=Muenchen,ST=Bayern,C=DE' end @@ -357,4 +365,177 @@ module X509Helpers ['r.meier@siemens.com'] end end + + module User2 + extend self + + def commit + '440bf5b2b499a90d9adcbebe3752f8c6f245a1aa' + end + + def path + 'gitlab-test' + end + + def trust_cert + <<~TRUSTCERTIFICATE + -----BEGIN CERTIFICATE----- + MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw + KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y + MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl + LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C + AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 + 7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS + 0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB + BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp + KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI + zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR + nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP + mygUY7Ii2zbdCdliiow= + -----END CERTIFICATE----- + TRUSTCERTIFICATE + end + + def signed_commit_signature + <<~SIGNATURE + -----BEGIN SIGNED MESSAGE----- + MIIEOQYJKoZIhvcNAQcCoIIEKjCCBCYCAQExDTALBglghkgBZQMEAgEwCwYJKoZI + hvcNAQcBoIIC2jCCAtYwggJdoAMCAQICFC5R9EXk+ljFhyCs4urRxmCuvQNAMAoG + CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln + c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDgxOTE3NTgwNVoXDTIzMDgxOTE4MDgw + NVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBGajWb10Rt36IMxtJmjRDa7 + 5O6YCLhVq9+LNJSAx2M7p6netqW7W+lwym4z1Y1gXLdGHBshrbx/yr6Trhh2TCej + ggF8MIIBeDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD + VR0OBBYEFBttEjGzNppCqA4tlZY4oaxkdmQbMB8GA1UdIwQYMBaAFN/T6c9WJBGW + +ajY6ShVosYuGGQ/MCUGA1UdEQEB/wQbMBmBF2dpdGxhYmdwZ3Rlc3RAZ21haWwu + Y29tMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0 + aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0 + aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt + /4eKcoAvKe6OAAABig7ydOsAAAQDAEgwRgIhAMqJnFLAspeqfbK/gA/7zjceyExq + QN7qDXWKRLS01rTvAiEAp/uBShQb9tVa3P3fYVAMiXydvr5dqCpNiuudZiuYq0Yw + CgYIKoZIzj0EAwMDZwAwZAIwWKXYyP5FvbfhvfLkV0tN887ax1eg7TmF1Tzkugag + cLJ5MzK3xYNcUO/3AxO3H/b8AjBD9DF6R4kFO4cXoqnpsk2FTUeSPiUJ+0x2PDFG + gQZvoMWz7CnwjXml8XDEKNpYoPkxggElMIIBIQIBATBPMDcxFTATBgNVBAoTDHNp + Z3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlAhQuUfRF + 5PpYxYcgrOLq0cZgrr0DQDALBglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJ + KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA4MTkxNzU4MDVaMC8GCSqGSIb3 + DQEJBDEiBCB4B7DeGk22WmBseJzjjRJcQsyYxu0PNDAFXq55uJ7MSzAKBggqhkjO + PQQDAgRHMEUCIQCNegIrK6m1xyGuu4lw06l22VQsmO74/k3H236jCFF+bAIgAX1N + rxBFWnjWboZmAV1NuduTD/YToShK6iRmJ/NpILA= + -----END SIGNED MESSAGE----- + SIGNATURE + end + + def signed_commit_base_data + <<~SIGNEDDATA + tree 7d5ee08cadaa161d731c56a9265feef130143b07 + parent 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 + author Mona Lisa <gitlabgpgtest@gmail.com> 1692467872 +0000 + committer Mona Lisa <gitlabgpgtest@gmail.com> 1692467872 +0000 + + Sigstore Signed Commit + SIGNEDDATA + end + + def signed_commit_time + Time.at(1692467872) + end + + def signed_tag_time + Time.at(1692467872) + end + + def signed_tag_signature + <<~SIGNATURE + -----BEGIN SIGNED MESSAGE----- + MIIEOgYJKoZIhvcNAQcCoIIEKzCCBCcCAQExDTALBglghkgBZQMEAgEwCwYJKoZI + hvcNAQcBoIIC2zCCAtcwggJdoAMCAQICFB5qFHBSNfcJDZecnHK5/tleuX3yMAoG + CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln + c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDgxOTE3NTgzM1oXDTIzMDgxOTE4MDgz + M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKJtbdL88PM8lE21CuyDYlZm + 0xZYCThoXZSGmULrgE5+hfroCIbLswOi5i6TyB8j4CCe0Jxeu94Jn+76SXF+lbej + ggF8MIIBeDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD + VR0OBBYEFBkU3IBENVJYeyK9b56vbGGrjPwYMB8GA1UdIwQYMBaAFN/T6c9WJBGW + +ajY6ShVosYuGGQ/MCUGA1UdEQEB/wQbMBmBF2dpdGxhYmdwZ3Rlc3RAZ21haWwu + Y29tMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0 + aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0 + aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt + /4eKcoAvKe6OAAABig7y4tYAAAQDAEgwRgIhAMUjWh8ayhjWDI3faFah3Du/7IuY + xzbUXaPQnCyUbvwwAiEAwHgWv8fmKMudbVu37Nbq/c1cdnQqDK9Y2UGtlmzaLrYw + CgYIKoZIzj0EAwMDaAAwZQIwZTKZlS4HNJH48km3pxG95JTbldSBhvFlrpIEVRUd + TEK6uGQJmpIm1WYQjbJbiVS8AjEA+2NoAdMuRpa2k13HUfWQEMtzQcxZMMNB7Yux + 9ZIADOlFp701ujtFSZAXgqGL3FYKMYIBJTCCASECAQEwTzA3MRUwEwYDVQQKEwxz + aWdzdG9yZS5kZXYxHjAcBgNVBAMTFXNpZ3N0b3JlLWludGVybWVkaWF0ZQIUHmoU + cFI19wkNl5yccrn+2V65ffIwCwYJYIZIAWUDBAIBoGkwGAYJKoZIhvcNAQkDMQsG + CSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwODE5MTc1ODMzWjAvBgkqhkiG + 9w0BCQQxIgQgwpYCAlbS6KnfgxQD3SATWUbdUssLaBWkHwTkmtCye4wwCgYIKoZI + zj0EAwIERzBFAiB8y5bGhWJvWCHQyma7oF038ZPLzXmsDJyJffJHoAb6XAIhAOW3 + gxuYuJAKP86B1fY0vYCZHF8vU6SZAcE6teSDowwq + -----END SIGNED MESSAGE----- + SIGNATURE + end + + def signed_tag_base_data + <<~SIGNEDDATA + object 440bf5b2b499a90d9adcbebe3752f8c6f245a1aa + type commit + tag v1.1.2 + tagger Mona Lisa <gitlabgpgtest@gmail.com> 1692467901 +0000 + + Sigstore Signed Tag + SIGNEDDATA + end + + def certificate_serial + 264441215000592123389532407734419590292801651520 + end + + def tag_certificate_serial + 173635382582380059990335547381753891120957980146 + end + + def certificate_subject_key_identifier + '1B:6D:12:31:B3:36:9A:42:A8:0E:2D:95:96:38:A1:AC:64:76:64:1B' + end + + def tag_certificate_subject_key_identifier + '19:14:DC:80:44:35:52:58:7B:22:BD:6F:9E:AF:6C:61:AB:8C:FC:18' + end + + def issuer_subject_key_identifier + 'DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F' + end + + def tag_issuer_subject_key_identifier + 'DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F' + end + + def certificate_email + 'gitlabgpgtest@gmail.com' + end + + def tag_email + 'gitlabgpgtest@gmail.com' + end + + def certificate_issuer + 'CN=sigstore-intermediate,O=sigstore.dev' + end + + def tag_certificate_issuer + 'CN=sigstore-intermediate,O=sigstore.dev' + end + + def certificate_subject + '' + end + + def names + ['Mona Lisa'] + end + + def emails + ['gitlabgpgtest@gmail.com'] + end + end end diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb index a3e9c3b8474..beaba84d78c 100644 --- a/spec/support/matchers/pagination_matcher.rb +++ b/spec/support/matchers/pagination_matcher.rb @@ -11,3 +11,13 @@ RSpec::Matchers.define :include_limited_pagination_headers do |expected| expect(actual.headers).to include('X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') end end + +RSpec::Matchers.define :include_keyset_url_params do |expected| + include KeysetPaginationHelpers + + match do |actual| + params_for_next_page = pagination_params_from_next_url(actual) + + expect(params_for_next_page).to include('cursor') + end +end diff --git a/spec/support/migration.rb b/spec/support/migration.rb index b1e75d9c9e2..fc8a4bb12fb 100644 --- a/spec/support/migration.rb +++ b/spec/support/migration.rb @@ -20,21 +20,11 @@ RSpec.configure do |config| Gitlab::CurrentSettings.clear_in_memory_application_settings! end - config.prepend_before(:all, :migration) do - TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.no_transaction_adapter - end - - config.append_after(:all, :migration) do - TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.default_adapter - end - config.append_after(:context, :migration) do recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables end config.around(:each, :migration) do |example| - self.class.use_transactional_tests = false - migration_schema = example.metadata[:migration] migration_schema = :gitlab_main if migration_schema == true base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first @@ -52,10 +42,6 @@ RSpec.configure do |config| else example.run end - - delete_from_all_tables!(except: deletion_except_tables) - - self.class.use_transactional_tests = true end # Each example may call `migrate!`, so we must ensure we are migrated down every time diff --git a/spec/support/multiple_databases.rb b/spec/support/multiple_databases.rb index 616cf00269c..1c556858018 100644 --- a/spec/support/multiple_databases.rb +++ b/spec/support/multiple_databases.rb @@ -7,8 +7,6 @@ RSpec.configure do |config| # at startup, but that generates its own # `Gitlab::Database::Reflection` so the result is not memoized by # callers of `ApplicationRecord.database.version`, such as - # `Gitlab::Database::AsWithMaterialized.materialized_supported?`. - # TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed. [ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version } config.around(:each, :reestablished_active_record_base) do |example| diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb index 576275e9d1d..db5118d6f88 100644 --- a/spec/support/protected_branch_helpers.rb +++ b/spec/support/protected_branch_helpers.rb @@ -34,7 +34,7 @@ module ProtectedBranchHelpers select_input.click wait_for_requests - within('.dropdown.show .dropdown-menu', &block) + within('.dropdown .dropdown-menu.show', &block) # Enhanced select is used in EE, therefore an extra click is needed. select_input.click if select_input['aria-expanded'] == 'true' diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 4479e679d67..7f3aa55fb1d 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -20,10 +20,18 @@ RSpec.configure do |config| config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt') # Makes diffs show entire non-truncated values. - config.before(:each, :unlimited_max_formatted_output_length) do + config.around(:each, :unlimited_max_formatted_output_length) do |example| + old_max_formatted_output_length = RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length + config.expect_with :rspec do |c| c.max_formatted_output_length = nil end + + example.run + + config.expect_with :rspec do |c| + c.max_formatted_output_length = old_max_formatted_output_length + end end unless ENV['CI'] diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index f52f843e56a..298f4006c3b 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -19,7 +19,6 @@ - './ee/spec/controllers/admin/elasticsearch_controller_spec.rb' - './ee/spec/controllers/admin/emails_controller_spec.rb' - './ee/spec/controllers/admin/geo/nodes_controller_spec.rb' -- './ee/spec/controllers/admin/geo/projects_controller_spec.rb' - './ee/spec/controllers/admin/geo/settings_controller_spec.rb' - './ee/spec/controllers/admin/groups_controller_spec.rb' - './ee/spec/controllers/admin/impersonations_controller_spec.rb' @@ -166,7 +165,6 @@ - './ee/spec/controllers/security/vulnerabilities_controller_spec.rb' - './ee/spec/controllers/sitemap_controller_spec.rb' - './ee/spec/controllers/subscriptions_controller_spec.rb' -- './ee/spec/controllers/trial_registrations_controller_spec.rb' - './ee/spec/controllers/users_controller_spec.rb' - './ee/spec/db/production/license_spec.rb' - './ee/spec/elastic_integration/global_search_spec.rb' @@ -461,7 +459,6 @@ - './ee/spec/features/read_only_spec.rb' - './ee/spec/features/registrations/combined_registration_spec.rb' - './ee/spec/features/registrations/one_trust_spec.rb' -- './ee/spec/features/registrations/welcome_spec.rb' - './ee/spec/features/search/elastic/global_search_spec.rb' - './ee/spec/features/search/elastic/group_search_spec.rb' - './ee/spec/features/search/elastic/project_search_spec.rb' @@ -526,7 +523,6 @@ - './ee/spec/finders/epics_finder_spec.rb' - './ee/spec/finders/geo/ci_secure_file_registry_finder_spec.rb' - './ee/spec/finders/geo/container_repository_registry_finder_spec.rb' -- './ee/spec/finders/geo/design_registry_finder_spec.rb' - './ee/spec/finders/geo/group_wiki_repository_registry_finder_spec.rb' - './ee/spec/finders/geo/lfs_object_registry_finder_spec.rb' - './ee/spec/finders/geo/merge_request_diff_registry_finder_spec.rb' @@ -594,7 +590,6 @@ - './ee/spec/graphql/api/vulnerabilities_spec.rb' - './ee/spec/graphql/ee/mutations/boards/issues/issue_move_list_spec.rb' - './ee/spec/graphql/ee/mutations/boards/lists/create_spec.rb' -- './ee/spec/graphql/ee/mutations/ci/project_ci_cd_settings_update_spec.rb' - './ee/spec/graphql/ee/mutations/ci/runner/update_spec.rb' - './ee/spec/graphql/ee/mutations/concerns/mutations/resolves_issuable_spec.rb' - './ee/spec/graphql/ee/resolvers/board_list_issues_resolver_spec.rb' @@ -999,7 +994,6 @@ - './ee/spec/helpers/security_helper_spec.rb' - './ee/spec/helpers/subscriptions_helper_spec.rb' - './ee/spec/helpers/timeboxes_helper_spec.rb' -- './ee/spec/helpers/trial_registrations/reassurances_helper_spec.rb' - './ee/spec/helpers/trial_status_widget_helper_spec.rb' - './ee/spec/helpers/users_helper_spec.rb' - './ee/spec/helpers/vulnerabilities_helper_spec.rb' @@ -1147,7 +1141,6 @@ - './ee/spec/lib/ee/gitlab/group_search_results_spec.rb' - './ee/spec/lib/ee/gitlab/hook_data/group_member_builder_spec.rb' - './ee/spec/lib/ee/gitlab/hook_data/issue_builder_spec.rb' -- './ee/spec/lib/ee/gitlab/hook_data/user_builder_spec.rb' - './ee/spec/lib/ee/gitlab/import_export/after_export_strategies/custom_template_export_import_strategy_spec.rb' - './ee/spec/lib/ee/gitlab/import_export/group/tree_restorer_spec.rb' - './ee/spec/lib/ee/gitlab/import_export/group/tree_saver_spec.rb' @@ -1395,7 +1388,6 @@ - './ee/spec/lib/gitlab/geo/log_cursor/daemon_spec.rb' - './ee/spec/lib/gitlab/geo/log_cursor/event_logs_spec.rb' - './ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb' -- './ee/spec/lib/gitlab/geo/log_cursor/events/design_repository_updated_event_spec.rb' - './ee/spec/lib/gitlab/geo/log_cursor/events/event_spec.rb' - './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_attachments_event_spec.rb' - './ee/spec/lib/gitlab/geo/log_cursor/events/hashed_storage_migrated_event_spec.rb' @@ -1758,7 +1750,6 @@ - './ee/spec/models/geo/ci_secure_file_registry_spec.rb' - './ee/spec/models/geo/container_repository_registry_spec.rb' - './ee/spec/models/geo/deleted_project_spec.rb' -- './ee/spec/models/geo/design_registry_spec.rb' - './ee/spec/models/geo/event_log_spec.rb' - './ee/spec/models/geo/event_log_state_spec.rb' - './ee/spec/models/geo/every_geo_event_spec.rb' @@ -2750,7 +2741,6 @@ - './ee/spec/services/geo/wiki_sync_service_spec.rb' - './ee/spec/services/gitlab_subscriptions/activate_service_spec.rb' - './ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb' -- './ee/spec/services/gitlab_subscriptions/create_hand_raise_lead_service_spec.rb' - './ee/spec/services/gitlab_subscriptions/create_service_spec.rb' - './ee/spec/services/gitlab_subscriptions/create_trial_or_lead_service_spec.rb' - './ee/spec/services/gitlab_subscriptions/fetch_purchase_eligible_namespaces_service_spec.rb' @@ -2828,7 +2818,6 @@ - './ee/spec/services/milestones/destroy_service_spec.rb' - './ee/spec/services/milestones/promote_service_spec.rb' - './ee/spec/services/milestones/update_service_spec.rb' -- './ee/spec/services/namespaces/in_product_marketing_emails_service_spec.rb' - './ee/spec/services/namespaces/storage/email_notification_service_spec.rb' - './ee/spec/services/path_locks/lock_service_spec.rb' - './ee/spec/services/path_locks/unlock_service_spec.rb' @@ -3057,7 +3046,6 @@ - './ee/spec/views/profiles/preferences/show.html.haml_spec.rb' - './ee/spec/views/projects/edit.html.haml_spec.rb' - './ee/spec/views/projects/issues/show.html.haml_spec.rb' -- './ee/spec/views/projects/_merge_request_status_checks_settings.html.haml_spec.rb' - './ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb' - './ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb' - './ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb' @@ -3068,6 +3056,7 @@ - './ee/spec/views/projects/security/discover/show.html.haml_spec.rb' - './ee/spec/views/projects/security/policies/index.html.haml_spec.rb' - './ee/spec/views/projects/security/sast_configuration/show.html.haml_spec.rb' +- './ee/spec/views/projects/settings/merge_requests/_merge_request_status_checks_settings.html.haml_spec.rb' - './ee/spec/views/projects/settings/subscriptions/_index.html.haml_spec.rb' - './ee/spec/views/registrations/groups/new.html.haml_spec.rb' - './ee/spec/views/shared/billings/_billing_plan_actions.html.haml_spec.rb' @@ -3133,7 +3122,6 @@ - './ee/spec/workers/ee/arkose/blocked_users_report_worker_spec.rb' - './ee/spec/workers/ee/ci/build_finished_worker_spec.rb' - './ee/spec/workers/ee/issuable_export_csv_worker_spec.rb' -- './ee/spec/workers/ee/namespaces/in_product_marketing_emails_worker_spec.rb' - './ee/spec/workers/ee/namespaces/root_statistics_worker_spec.rb' - './ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb' - './ee/spec/workers/ee/repository_check/batch_worker_spec.rb' @@ -8579,7 +8567,6 @@ - './spec/requests/groups/crm/contacts_controller_spec.rb' - './spec/requests/groups/crm/organizations_controller_spec.rb' - './spec/requests/groups/deploy_tokens_controller_spec.rb' -- './spec/requests/groups/email_campaigns_controller_spec.rb' - './spec/requests/groups/harbor/artifacts_controller_spec.rb' - './spec/requests/groups/harbor/repositories_controller_spec.rb' - './spec/requests/groups/harbor/tags_controller_spec.rb' @@ -9323,7 +9310,6 @@ - './spec/services/milestones/transfer_service_spec.rb' - './spec/services/milestones/update_service_spec.rb' - './spec/services/namespace_settings/update_service_spec.rb' -- './spec/services/namespaces/in_product_marketing_emails_service_spec.rb' - './spec/services/namespaces/package_settings/update_service_spec.rb' - './spec/services/namespaces/statistics_refresher_service_spec.rb' - './spec/services/notes/build_service_spec.rb' @@ -10126,7 +10112,6 @@ - './spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb' - './spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb' - './spec/workers/migrate_external_diffs_worker_spec.rb' -- './spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb' - './spec/workers/namespaces/process_sync_events_worker_spec.rb' - './spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb' - './spec/workers/namespaces/root_statistics_worker_spec.rb' diff --git a/spec/support/shared_contexts/dependency_proxy_shared_context.rb b/spec/support/shared_contexts/dependency_proxy_shared_context.rb new file mode 100644 index 00000000000..02625722a8c --- /dev/null +++ b/spec/support/shared_contexts/dependency_proxy_shared_context.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with a server running the dependency proxy' do + def run_server(handler) + default_server = Capybara.server + + Capybara.server = Capybara.servers[:puma] + server = Capybara::Server.new(handler) + server.boot + server + ensure + Capybara.server = default_server + end +end diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb index 8b4c1c1e243..3098fc3dc5e 100644 --- a/spec/support/shared_contexts/email_shared_context.rb +++ b/spec/support/shared_contexts/email_shared_context.rb @@ -191,7 +191,7 @@ RSpec.shared_examples 'note handler shared examples' do |forwardable| context 'when the service desk' do let(:project) { create(:project, :public, service_desk_enabled: true) } - let(:support_bot) { User.support_bot } + let(:support_bot) { Users::Internal.support_bot } let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') } let!(:note) { create(:note, project: project, noteable: noteable) } let(:email_raw) { with_quick_actions } diff --git a/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb index 5996fcc6593..befc231f04f 100644 --- a/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb @@ -22,7 +22,7 @@ RSpec.shared_context 'group integration activation' do visit_group_integrations within('#content-body') do - click_link(name) + click_link(name, match: :first) end end end diff --git a/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb index 3b02db994a3..c740917cec4 100644 --- a/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb @@ -18,7 +18,7 @@ RSpec.shared_context 'instance integration activation' do visit_instance_integrations within('#content-body') do - click_link(name) + click_link(name, match: :first) end end end 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 a9b9a5246e6..c3da9435e05 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 @@ -4,7 +4,7 @@ RSpec.shared_context 'project integration activation' do include_context 'with integration activation' let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user) } + let_it_be(:user) { create(:user, :no_super_sidebar) } before do project.add_maintainer(user) @@ -19,7 +19,7 @@ RSpec.shared_context 'project integration activation' do visit_project_integrations within('#content-body') do - click_link(name) + click_link(name, match: :first) end end diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb index ef1c01f72f9..b89b1aabd87 100644 --- a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb @@ -8,5 +8,5 @@ RSpec.shared_context 'UsersFinder#execute filter by project context' do let_it_be(:external_user) { create(:user, :external) } let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) } let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } - let_it_be(:internal_user) { User.alert_bot.tap { |u| u.confirm } } + let_it_be(:internal_user) { Users::Internal.alert_bot.tap { |u| u.confirm } } end diff --git a/spec/support/shared_contexts/lib/gitlab/database/load_balancing/wal_tracking_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/database/load_balancing/wal_tracking_shared_context.rb index cbbd3754108..46d6a1fbac0 100644 --- a/spec/support/shared_contexts/lib/gitlab/database/load_balancing/wal_tracking_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/database/load_balancing/wal_tracking_shared_context.rb @@ -62,7 +62,13 @@ RSpec.shared_context 'when tracking WAL location reference' do def stub_replica_available!(available) ::Gitlab::Database::LoadBalancing.each_load_balancer do |lb| - allow(lb).to receive(:select_up_to_date_host).with(current_location).and_return(available) + result = if available + ::Gitlab::Database::LoadBalancing::LoadBalancer::ANY_CAUGHT_UP + else + ::Gitlab::Database::LoadBalancing::LoadBalancer::NONE_CAUGHT_UP + end + + allow(lb).to receive(:select_up_to_date_host).with(current_location).and_return(result) end end end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 112b90029b8..a09319b4980 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -82,7 +82,6 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Monitor'), nav_sub_items: [ - _('Tracing'), _('Error Tracking'), _('Alerts'), _('Incidents') @@ -165,15 +164,6 @@ RSpec.shared_context 'group navbar structure' do } end - let(:ci_cd_nav_item) do - { - nav_item: _('CI/CD'), - nav_sub_items: [ - s_('Runners|Runners') - ] - } - end - let(:issues_nav_items) do [ _('List'), @@ -207,6 +197,12 @@ RSpec.shared_context 'group navbar structure' do }, (security_and_compliance_nav_item if Gitlab.ee?), { + nav_item: _('CI/CD'), + nav_sub_items: [ + s_('Runners|Runners') + ] + }, + { nav_item: _('Kubernetes'), nav_sub_items: [] }, @@ -231,6 +227,10 @@ RSpec.shared_context 'dashboard navbar structure' do nav_sub_items: [] }, { + nav_item: _('Organizations'), + nav_sub_items: [] + }, + { nav_item: _("Issues"), nav_sub_items: [] }, 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 07a4cbdb534..70b48322efd 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -58,6 +58,7 @@ RSpec.shared_context 'GroupPolicy context' do destroy_upload admin_achievement award_achievement + read_group_runners ] end @@ -73,7 +74,6 @@ RSpec.shared_context 'GroupPolicy context' do create_subgroup read_statistics update_default_branch_protection - read_group_runners register_group_runners read_billing edit_billing diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb index 36103b94542..cf5ac849f63 100644 --- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb @@ -5,7 +5,7 @@ RSpec.shared_context 'npm api setup' do include HttpBasicAuthHelpers let_it_be(:user, reload: true) { create(:user) } - let_it_be(:group) { create(:group, name: 'test-group') } + let_it_be(:group, reload: true) { create(:group, name: 'test-group') } let_it_be(:namespace) { group } let_it_be(:project, reload: true) { create(:project, :public, namespace: namespace) } let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.3') } diff --git a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb index cb7001a9faf..21bba14f3e6 100644 --- a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb +++ b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb @@ -15,16 +15,4 @@ RSpec.shared_examples 'handle subscription based on user access' do expect(subscription).to be_rejected end - - context 'when action_cable_notes is disabled' do - before do - stub_feature_flags(action_cable_notes: false) - end - - it 'rejects the subscription' do - subscribe(subscribe_params) - - expect(subscription).to be_rejected - end - end end diff --git a/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb b/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb new file mode 100644 index 00000000000..77a352a8326 --- /dev/null +++ b/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creating a pipeline with environment keyword' do + context 'with environment' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/$CI_COMMIT_REF_NAME" }, + **base_config + }) + end + + it 'creates the environment', :sidekiq_inline do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).to be_present + expect(result.all_jobs.first.deployment).to be_persisted + expect(result.all_jobs.first.deployment.deployable).to be_a(expected_deployable_class) + end + + it 'sets tags when build job' do + skip unless expected_deployable_class == Ci::Build + + result = execute_service.payload + + expect(result).to be_persisted + expect(result.all_jobs.first.tag_list).to match_array(expected_tag_names) + end + end + + context 'with environment with auto_stop_in' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/$CI_COMMIT_REF_NAME", auto_stop_in: '1 day' }, + **base_config + }) + end + + it 'creates the environment with auto stop in' do + result = execute_service.payload + + expect(result).to be_persisted + expect(result.all_jobs.first.options[:environment][:auto_stop_in]).to eq('1 day') + end + end + + context 'with environment name including persisted variables' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_JOB_ID" }, + **base_config + } + ) + end + + it 'skips persisted variables in environment name' do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/id1/id2")).to be_present + end + end + + context 'when environment with Kubernetes configuration' do + let(:kubernetes_namespace) { 'custom-namespace' } + let(:config) do + YAML.dump( + deploy: { + environment: { + name: "environment-name", + kubernetes: { namespace: kubernetes_namespace } + }, + **base_config + } + ) + end + + it 'stores the requested namespace' do + result = execute_service.payload + job = result.all_jobs.first + + expect(result).to be_persisted + expect(job.options.dig(:environment, :kubernetes, :namespace)).to eq(kubernetes_namespace) + end + end + + context 'when environment with invalid name' do + let(:config) do + YAML.dump(deploy: { environment: { name: 'name,with,commas' }, **base_config }) + end + + it 'does not create an environment' do + expect do + result = execute_service.payload + + expect(result).to be_persisted + end.not_to change { Environment.count } + end + end + + context 'when environment with duplicate names' do + let(:config) do + YAML.dump({ + deploy: { environment: { name: 'production' }, **base_config }, + deploy_2: { environment: { name: 'production' }, **base_config } + }) + end + + it 'creates a pipeline with the environment', :sidekiq_inline do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: 'production')).to be_present + expect(result.all_jobs.first.deployment).to be_persisted + expect(result.all_jobs.first.deployment.deployable).to be_a(expected_deployable_class) + end + end + + context 'when pipeline has a job with environment' do + let(:pipeline) { execute_service.payload } + + context 'when environment name is valid' do + let(:config) do + YAML.dump({ + review_app: { + environment: { + name: 'review/${CI_COMMIT_REF_NAME}', + url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com' + }, + **base_config + } + }) + end + + it 'has a job with environment', :sidekiq_inline do + expect(pipeline.all_jobs.count).to eq(1) + expect(pipeline.all_jobs.first.persisted_environment.name).to eq('review/master') + expect(pipeline.all_jobs.first.deployment.status).to eq(expected_deployment_status) + expect(pipeline.all_jobs.first.status).to eq(expected_job_status) + end + end + + context 'when environment name is invalid' do + let(:config) do + YAML.dump({ + 'job:deploy-to-test-site': { + environment: { + name: '${CI_JOB_NAME}', + url: 'https://$APP_URL' + }, + **base_config + } + }) + end + + it 'has a job without environment' do + expect(pipeline.all_jobs.count).to eq(1) + expect(pipeline.all_jobs.first.persisted_environment).to be_nil + expect(pipeline.all_jobs.first.deployment).to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb index b51a8fa20e2..4f43d38e604 100644 --- a/spec/support/shared_examples/ci/deployable_shared_examples.rb +++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb @@ -96,7 +96,7 @@ RSpec.shared_examples 'a deployable job' do ActiveRecord::QueryRecorder.new { subject } end - index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") } + index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") } index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") } expect(index_for_build).to be < index_for_deployment @@ -259,7 +259,7 @@ RSpec.shared_examples 'a deployable job' do describe '#environment_tier_from_options' do subject { job.environment_tier_from_options } - let(:job) { Ci::Build.new(options: options) } + let(:job) { described_class.new(options: options) } let(:options) { { environment: { deployment_tier: 'production' } } } it { is_expected.to eq('production') } @@ -276,7 +276,7 @@ RSpec.shared_examples 'a deployable job' do let(:options) { { environment: { deployment_tier: 'production' } } } let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } - let(:job) { Ci::Build.new(options: options, environment: 'production', project: project) } + let(:job) { described_class.new(options: options, environment: 'production', project: project) } it { is_expected.to eq('production') } @@ -295,6 +295,52 @@ RSpec.shared_examples 'a deployable job' do end end + describe '#environment_url' do + subject { job.environment_url } + + let!(:job) { create(factory_type, :with_deployment, :deploy_to_production, pipeline: pipeline) } + + it { is_expected.to eq('http://prd.example.com/$CI_JOB_NAME') } + + context 'when options does not include url' do + before do + job.update!(options: { environment: { url: nil } }) + job.persisted_environment.update!(external_url: 'http://prd.example.com/$CI_JOB_NAME') + end + + it 'fetches from the persisted environment' do + expect_any_instance_of(::Environment) do |environment| + expect(environment).to receive(:external_url).once + end + + is_expected.to eq('http://prd.example.com/$CI_JOB_NAME') + end + + context 'when persisted environment is absent' do + before do + job.clear_memoization(:persisted_environment) + job.persisted_environment = nil + end + + it { is_expected.to be_nil } + end + end + end + + describe '#environment_slug' do + subject { job.environment_slug } + + let!(:job) { create(factory_type, :with_deployment, :start_review_app, pipeline: pipeline) } + + it { is_expected.to eq('review-master-8dyme2') } + + context 'when persisted environment is absent' do + let!(:job) { create(factory_type, :start_review_app, pipeline: pipeline) } + + it { is_expected.to be_nil } + end + end + describe 'environment' do describe '#has_environment_keyword?' do subject { job.has_environment_keyword? } @@ -536,10 +582,6 @@ RSpec.shared_examples 'a deployable job' do end describe '#deployment_status' do - before do - allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf - end - context 'when job is a last deployment' do let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) } let(:environment) { create(:environment, name: 'production', project: job.project) } diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb index e61c884cd2b..14d0ac81250 100644 --- a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb +++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb @@ -121,8 +121,7 @@ RSpec.shared_examples 'every metric definition' do let(:ignored_classes) do [ Gitlab::Usage::Metrics::Instrumentations::IssuesWithAlertManagementAlertsMetric, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithSelfManagedPrometheusAlertEvents + Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents ].freeze end diff --git a/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb index 56a5dcb10b2..cb4e68122d9 100644 --- a/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb @@ -43,5 +43,14 @@ RSpec.shared_examples WebHooks::HookLogActions do expect(response).to have_gitlab_http_status(:not_found) end + + it 'redirects back with a warning if the hook log url is outdated' do + web_hook_log.update!(url_hash: 'some_other_value') + + post retry_path, headers: { 'REFERER' => show_path } + + expect(response).to redirect_to(show_path) + expect(flash[:warning]).to eq(_('The hook URL has changed, and this log entry cannot be retried')) + end end end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index a4eb6a839c0..bd9c2582d2f 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -18,23 +18,6 @@ RSpec.shared_examples 'issuable notes filter' do expect(UserPreference.count).to eq(1) end - it 'expires notes e-tag cache for issuable if filter changed' do - notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - - expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache) - - get :discussions, params: params.merge(notes_filter: notes_filter) - end - - it 'does not expires notes e-tag cache for issuable if filter did not change' do - notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - user.set_notes_filter(notes_filter, issuable) - - expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache) - - get :discussions, params: params.merge(notes_filter: notes_filter) - end - it 'does not set notes filter when database is in read-only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] diff --git a/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb b/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb new file mode 100644 index 00000000000..aea552f6ac7 --- /dev/null +++ b/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'lock_on_merge when editing labels' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(enforce_locked_labels_on_merge: false) + visit edit_label_path_unlocked + end + + it 'does not display the checkbox/help text' do + expect(page).not_to have_content(_('Lock label after a merge request is merged')) + expect(page).not_to have_content(label_lock_on_merge_help_text) + end + end + + it 'updates lock_on_merge' do + expect(page).to have_content(_('Lock label after a merge request is merged')) + expect(page).to have_content(label_lock_on_merge_help_text) + + check(_('Lock label after a merge request is merged')) + click_button 'Save changes' + + expect(label_unlocked.reload.lock_on_merge).to be_truthy + end + + it 'checkbox is disabled if lock_on_merge already set' do + visit edit_label_path_locked + + expect(page.find('#label_lock_on_merge')).to be_disabled + end +end + +RSpec.shared_examples 'lock_on_merge when creating labels' do + it 'is not supported when creating a label' do + expect(page).not_to have_content(_('Lock label after a merge request is merged')) + expect(page).not_to have_content(label_lock_on_merge_help_text) + end +end diff --git a/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb new file mode 100644 index 00000000000..aefcdc70082 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Requires a context containing: +# - user +# - params + +RSpec.shared_examples 'search request exceeding rate limit' do + include_examples 'rate limited endpoint', rate_limit_key: :search_rate_limit + + it 'allows user in allow-list to search without applying rate limit', :freeze_time, + :clean_gitlab_redis_rate_limiting do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1) + + stub_application_setting(search_rate_limit_allowlist: [current_user.username]) + + request + request + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb index 3d3b619451d..29d6f202498 100644 --- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'Snowplow event tracking with Redis context' do |overrides it_behaves_like 'Snowplow event tracking', overrides: overrides do let(:context) do key_path = try(:label) || action - [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: key_path).to_context.to_json] + [Gitlab::Usage::MetricDefinition.context_for(key_path).to_context.to_json] end end end diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb index 6c4e98c9989..f50874b6b05 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -14,7 +14,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| end describe "registration" do - let(:user) { create(:user) } + let(:user) { create(:user, :no_super_sidebar) } before do gitlab_sign_in(user) @@ -67,7 +67,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| end describe 'fallback code authentication' do - let(:user) { create(:user) } + let(:user) { create(:user, :no_super_sidebar) } before do # Register and logout diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index fff8ef915eb..3e81f969462 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -244,7 +244,8 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi end end - it 'expands the link, updates the link attributes and text if text is updated' do + it 'expands the link, updates the link attributes and text if text is updated', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419684' do page.within '[data-testid="link-bubble-menu"]' do fill_in 'link-text', with: 'new text' fill_in 'link-href', with: 'https://about.gitlab.com' diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index d410653ca43..58bf461c733 100644 --- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb @@ -4,8 +4,8 @@ RSpec.shared_examples 'project features apply to issuables' do |klass| let(:described_class) { klass } let(:group) { create(:group) } - let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user } - let(:user_outside_group) { create(:user) } + let(:user_in_group) { create(:group_member, :developer, user: create(:user, :no_super_sidebar), group: group ).user } + let(:user_outside_group) { create(:user, :no_super_sidebar) } let(:project) { create(:project, :public, project_args) } diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index fb882ef8a23..fc717fbac20 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -2,6 +2,7 @@ RSpec.shared_examples "protected branches > access control > CE" do let(:no_one) { ProtectedRef::AccessLevel.humanize(::Gitlab::Access::NO_ACCESS) } + let_it_be(:edit_form) { '.js-protected-branch-edit-form' } ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do @@ -30,7 +31,8 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) end - it "allows updating protected branches so that #{access_type_name} can push to them" do + it "allows updating protected branches so that #{access_type_name} can push to them", + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425080' do visit project_protected_branches_path(project) show_add_form @@ -41,18 +43,14 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.count).to eq(1) - within(".protected-branches-list") do - within_select(".js-allowed-to-push") do - click_on(access_type_name) - end - end - + set_allowed_to('push', access_type_name, form: edit_form) wait_for_requests expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) end - it "allows updating protected branches so that #{access_type_name} can merge to them" do + it "allows updating protected branches so that #{access_type_name} can merge to them", + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425080' do visit project_protected_branches_path(project) show_add_form @@ -63,12 +61,7 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.count).to eq(1) - within(".protected-branches-list") do - within_select(".js-allowed-to-merge") do - click_on(access_type_name) - end - end - + set_allowed_to('merge', access_type_name, form: edit_form) wait_for_requests expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb index 703ba5b018a..2b7147fa4b4 100644 --- a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb +++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do find(".js-allowed-to-create").click wait_for_requests - within('[data-testid="allowed-to-create-dropdown"]') do + within('.dropdown-menu') do dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) @@ -53,7 +53,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do find(".js-allowed-to-create").click wait_for_requests - within('[data-testid="allowed-to-create-dropdown"]') do + within('.dropdown-menu') do dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 0c043f48c5f..861c205337a 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -94,6 +94,17 @@ RSpec.shared_examples 'shows runner in list' do end end +RSpec.shared_examples 'shows runner details from list' do + it 'shows runner details page' do + click_link("##{runner.id} (#{runner.short_sha})") + + expect(current_url).to include(runner_page_path) + + expect(page).to have_selector 'h1', text: "##{runner.id} (#{runner.short_sha})" + expect(page).to have_content "#{s_('Runners|Description')} #{runner.description}" + end +end + RSpec.shared_examples 'pauses, resumes and deletes a runner' do include Spec::Support::Helpers::ModalHelpers @@ -191,6 +202,13 @@ RSpec.shared_examples 'shows runner jobs tab' do end end +RSpec.shared_examples 'shows locked field' do + it 'shows locked checkbox with description', :js do + expect(page).to have_selector('input[type="checkbox"][name="locked"]') + expect(page).to have_content(_('Lock to current projects')) + end +end + RSpec.shared_examples 'submits edit runner form' do it 'breadcrumb contains runner id and token' do page.within '[data-testid="breadcrumb-links"]' do diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb index 8ebec19a884..c2d144bef3b 100644 --- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb @@ -86,7 +86,7 @@ RSpec.shared_examples 'labels sidebar widget' do context 'creating a label', :js do before do page.within(labels_widget) do - page.find('[data-testid="create-label-button"]').click + click_button 'Create project label' end end @@ -96,12 +96,11 @@ RSpec.shared_examples 'labels sidebar widget' do end end - it 'creates new label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391240' do + it 'creates new label' do page.within(labels_widget) do fill_in 'Name new label', with: 'wontfix' - page.find('.suggest-colors a', match: :first).click - page.find('button', text: 'Create').click - wait_for_requests + click_link 'Magenta-pink' + click_button 'Create' expect(page).to have_content 'wontfix' end diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index bf870b3ce66..383f81d048f 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'tabs with counts' do end RSpec.shared_examples 'does not show New Snippet button' do - let(:user) { create(:user, :external) } + let(:user) { create(:user, :external, :no_super_sidebar) } specify do sign_in(user) diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb index 0b0c9edcb42..c1057671699 100644 --- a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb @@ -41,15 +41,15 @@ RSpec.shared_examples 'variable list pagination' do |variable_type| it 'sorts variables alphabetically in ASC and DESC order' do page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) - expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key) + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]')).to have_content('test_key_8') end click_button 'Next' wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('test_key_9') end page.within('[data-testid="ci-variable-table"]') do @@ -59,8 +59,8 @@ RSpec.shared_examples 'variable list pagination' do |variable_type| wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') - expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('test_key_9') + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]')).to have_content('test_key_0') end end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 3a91b798bbd..5951d3e781b 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'variable list' do it 'shows a list of variables' do page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key) end end @@ -17,7 +17,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('key') end end @@ -31,8 +31,8 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected')) end end @@ -46,25 +46,25 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked')) end end it 'reveals and hides variables' do page.within('[data-testid="ci-variable-table"]') do - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) expect(page).to have_content('*' * 5) click_button('Reveal value') - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) - expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) + expect(first('.js-ci-variable-row td[data-label="Value"]')).to have_content(variable.value) expect(page).not_to have_content('*' * 5) click_button('Hide value') - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) expect(page).to have_content('*' * 5) end end @@ -98,7 +98,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq('new_key') + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content('new_key') end it 'edits a variable to be unmasked' do @@ -116,8 +116,8 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected')) - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked')) end end @@ -145,7 +145,7 @@ RSpec.shared_examples 'variable list' do end page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Masked')) end end @@ -234,9 +234,9 @@ RSpec.shared_examples 'variable list' do # expect to find 3 rows of variables in alphabetical order expect(page).to have_selector('.js-ci-variable-row', count: 3) rows = all('.js-ci-variable-row') - expect(rows[0].find('td[data-label="Key"]').text).to eq('ckey') - expect(rows[1].find('td[data-label="Key"]').text).to eq('test_key') - expect(rows[2].find('td[data-label="Key"]').text).to eq('zkey') + expect(rows[0].find('td[data-label="Key"]')).to have_content('ckey') + expect(rows[1].find('td[data-label="Key"]')).to have_content('test_key') + expect(rows[2].find('td[data-label="Key"]')).to have_content('zkey') end context 'defaults to the application setting' do diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index d3863c9a675..18e0cfdad00 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -179,6 +179,45 @@ RSpec.shared_examples 'work items assignees' do expect(work_item.reload.assignees).to include(user) end + + it 'successfully assigns the current user by clicking `Assign myself` button' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + end + + it 'successfully removes all users on clear all button click' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + + find('[data-testid="work-item-assignees-input"]').click + find('[data-testid="clear-all-button"]').click + find("body").click + wait_for_requests + + expect(work_item.reload.assignees).not_to include(user) + end + + it 'successfully removes user on clicking badge cross button' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + + within('[data-testid="work-item-assignees-input"]') do + find('[data-testid="close-icon"]').click + end + find("body").click + wait_for_requests + + expect(work_item.reload.assignees).not_to include(user) + end end RSpec.shared_examples 'work items labels' do diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index 19001abcbe2..ed8feebf1f6 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -1406,7 +1406,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1416,7 +1416,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1426,7 +1426,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1436,7 +1436,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end end diff --git a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb index d6d360bb413..a69b56c3d58 100644 --- a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb @@ -6,11 +6,25 @@ RSpec.shared_examples 'updating time estimate' do let(:input_params) { input.merge(extra_params).merge({ timeEstimate: time_estimate }) } + before do + resource.update!(time_estimate: 1800) + end + + context 'when time estimate is not provided' do + let(:input_params) { input.merge(extra_params).except(:timeEstimate) } + + it 'does not update' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } + end + end + context 'when time estimate is not a valid numerical value' do let(:time_estimate) { '-3.5d' } it 'does not update' do - expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } end it 'returns error' do @@ -24,7 +38,8 @@ RSpec.shared_examples 'updating time estimate' do let(:time_estimate) { 'nonsense' } it 'does not update' do - expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } end it 'returns error' do @@ -47,6 +62,7 @@ RSpec.shared_examples 'updating time estimate' do '1h' | 3600 '0h' | 0 '-0h' | 0 + nil | 0 end with_them do diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index b346f35bdc9..2c94f21e144 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -61,14 +61,6 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect(deprecable.deprecation_reason).to eq('This was renamed. Deprecated in 1.10.') end - it 'supports named reasons: alpha' do - deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha }) - - expect(deprecable.deprecation_reason).to eq( - 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.' - ) - end - it 'supports :alpha' do deprecable = subject(alpha: { milestone: '1.10' }) @@ -82,7 +74,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do subject(alpha: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } ) end.to raise_error( ArgumentError, - eq("`experiment` and `deprecated` arguments cannot be passed at the same time") + eq("`alpha` and `deprecated` arguments cannot be passed at the same time") ) end diff --git a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb index 85fcd426e3d..16e25bf96dd 100644 --- a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb @@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor artifacts controller' do |args| get harbor_artifact_url(container, repository_id), headers: json_header end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with anonymous user' do before do diff --git a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb index b35595a10b2..a0d47d1a2d1 100644 --- a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb @@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args| get harbor_repository_url(container) end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with html' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with html' context 'with anonymous user' do before do @@ -121,17 +111,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args| get harbor_repository_url(container), headers: json_header end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with valid params' do context 'with valid page params' do diff --git a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb index 46fea7fdff6..aee728295de 100644 --- a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb @@ -76,17 +76,7 @@ RSpec.shared_examples 'a harbor tags controller' do |args| headers: json_header) end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with anonymous user' do before do diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb deleted file mode 100644 index d4fe45a91a0..00000000000 --- a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'behind AI related feature flags' do |provider_flag| - context "when #{provider_flag} is disabled" do - before do - stub_feature_flags(provider_flag => false) - end - - it 'responds as not found' do - post api(url, current_user), params: input_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when ai_experimentation_api is disabled' do - before do - stub_feature_flags(ai_experimentation_api: false) - end - - it 'responds as not found' do - post api(url, current_user), params: input_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end -end - -RSpec.shared_examples 'delegates AI request to Workhorse' do - it 'responds with Workhorse send-url headers' do - post api(url, current_user), params: input_params - - expect(response.body).to eq('""') - expect(response).to have_gitlab_http_status(:ok) - - send_url_prefix, encoded_data = response.headers['Gitlab-Workhorse-Send-Data'].split(':') - data = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_data)) - - expect(send_url_prefix).to eq('send-url') - expect(data).to eq({ - 'AllowRedirects' => false, - 'Method' => 'POST' - }.merge(expected_params)) - end -end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb new file mode 100644 index 00000000000..3dbe43d822f --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketImport::ObjectImporter do + include AfterNextHelpers + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } + + it 'notifies the waiter' do + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end + + describe '#perform' do + let_it_be(:import_started_project) { create(:project, :import_started) } + + let(:project_id) { project_id } + let(:waiter_key) { 'key' } + + shared_examples 'notifies the waiter' do + specify do + allow_next(worker.importer_class).to receive(:execute) + + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + + worker.perform(project_id, {}, waiter_key) + end + end + + context 'when project does not exist' do + let(:project_id) { non_existing_record_id } + + it_behaves_like 'notifies the waiter' + end + + context 'when project has import started' do + let_it_be(:project) do + create(:project, :import_started, import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + }) + end + + let(:project_id) { project.id } + + it 'calls the importer' do + expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice + expect_next(worker.importer_class, project, kind_of(Hash)).to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + + context 'when the importer raises an ActiveRecord::RecordInvalid error' do + before do + allow_next(worker.importer_class).to receive(:execute).and_raise(ActiveRecord::RecordInvalid) + end + + it 'tracks the error' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + worker.perform(project_id, {}, waiter_key) + end + end + + context 'when the importer raises a StandardError' do + before do + allow_next(worker.importer_class).to receive(:execute).and_raise(StandardError) + end + + it 'tracks the error and raises the error' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + expect { worker.perform(project_id, {}, waiter_key) }.to raise_error(StandardError) + end + end + end + + context 'when project import has been cancelled' do + let_it_be(:project_id) { create(:project, :import_canceled).id } + + it 'does not call the importer' do + expect_next(worker.importer_class).not_to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + end + end + + describe '#importer_class' do + it 'does not raise a NotImplementedError' do + expect(worker.importer_class).not_to be_nil + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb new file mode 100644 index 00000000000..f128aa92a53 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketImport::StageMethods do + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [project.id] } } + + it 'tracks the import failure' do + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: StandardError.new, + fail_import: true + ) + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end + + describe '.perform' do + let(:worker) { described_class.new } + + it 'executes the import' do + expect(worker).to receive(:import).with(project).once + expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice + + worker.perform(project.id) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb index ec2ae0b8a73..4eae8632467 100644 --- a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } it 'notifies the waiter' do - expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) end @@ -23,7 +23,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do specify do allow_next(worker.importer_class).to receive(:execute) - expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything, ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project_id, {}, waiter_key) end diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index 10f58748698..7cfab5c8295 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -35,8 +35,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project) end - it 'calls ::Ci::Build.sticking.unstick_or_continue_sticking' do - expect(::Ci::Build.sticking).to receive(:unstick_or_continue_sticking) + it 'calls ::Ci::Build.sticking.find_caught_up_replica' do + expect(::Ci::Build.sticking).to receive(:find_caught_up_replica) .with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id) .and_call_original @@ -49,8 +49,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false) end - it 'does not call ::Ci::Build.sticking.unstick_or_continue_sticking' do - expect(::Ci::Build.sticking).not_to receive(:unstick_or_continue_sticking) + it 'does not call ::Ci::Build.sticking.find_caught_up_replica' do + expect(::Ci::Build.sticking).not_to receive(:find_caught_up_replica) trace.read { |stream| stream } end diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb index df795723874..b80a51a1fc6 100644 --- a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb @@ -4,40 +4,17 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do describe 'adding MATERIALIZE to the CTE' do let(:options) { {} } - before do - # Clear the cached value before the test - Gitlab::Database::AsWithMaterialized.clear_memoization(:materialized_supported) - end - - context 'when PG version is <12' do - it 'does not add MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('11.1') + it 'adds MATERIALIZE keyword' do + allow(ApplicationRecord.database).to receive(:version).and_return('12.1') - expect(query).to include(expected_query_block_without_materialized) - end + expect(query).to include(expected_query_block_with_materialized) end - context 'when PG version is >=12' do - it 'adds MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('12.1') - - expect(query).to include(expected_query_block_with_materialized) - end + context 'when materialized is disabled' do + let(:options) { { materialized: false } } - context 'when version is higher than 12' do - it 'adds MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('15.1') - - expect(query).to include(expected_query_block_with_materialized) - end - end - - context 'when materialized is disabled' do - let(:options) { { materialized: false } } - - it 'does not add MATERIALIZE keyword' do - expect(query).to include(expected_query_block_without_materialized) - end + it 'does not add MATERIALIZE keyword' do + expect(query).to include(expected_query_block_without_materialized) end end end diff --git a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb new file mode 100644 index 00000000000..0fef5269ab6 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:| + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:import_state) { create(factory, :started, project: project, jid: '123') } + let(:worker) { described_class.new } + let(:next_stage) { :finish } + + describe '#perform', :clean_gitlab_redis_shared_state do + context 'when the project no longer exists' do + it 'does not perform any work' do + expect(worker).not_to receive(:wait_for_jobs) + + worker.perform(non_existing_record_id, { '123' => 2 }, next_stage) + end + end + + context 'when there are remaining jobs' do + it 'reschedules itself' do + expect(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({ '123' => 1 }) + + expect(described_class) + .to receive(:perform_in) + .with(described_class::INTERVAL, project.id, { '123' => 1 }, next_stage) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + context 'when the project import is not running' do + before do + import_state.update_column(:status, :failed) + end + + it 'does not perform any work' do + expect(worker).not_to receive(:wait_for_jobs) + expect(described_class).not_to receive(:perform_in) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + it 'clears the JobWaiter cache' do + expect(Gitlab::JobWaiter).to receive(:delete_key).with('123') + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + end + end + + context 'when there are no remaining jobs' do + before do + allow(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({}) + end + + it 'schedules the next stage' do + next_worker = described_class::STAGES[next_stage] + + expect_next_found_instance_of(import_state.class) do |state| + expect(state).to receive(:refresh_jid_expiration) + end + + expect(next_worker).to receive(:perform_async).with(project.id) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + it 'raises KeyError when the stage name is invalid' do + expect { worker.perform(project.id, { '123' => 2 }, :kittens) } + .to raise_error(KeyError) + end + end + end + + describe '#wait_for_jobs' do + it 'waits for jobs to complete and returns a new pair of keys to wait for' do + waiter1 = instance_double("Gitlab::JobWaiter", jobs_remaining: 1, key: '123') + waiter2 = instance_double("Gitlab::JobWaiter", jobs_remaining: 0, key: '456') + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(2, '123') + .and_return(waiter1) + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(1, '456') + .and_return(waiter2) + + expect(waiter1) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + expect(waiter2) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 }) + + expect(new_waiters).to eq({ '123' => 1 }) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb index c2898513424..025f0d5c7ea 100644 --- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'a repo type' do describe '#repository_for' do it 'finds the repository for the repo type' do - expect(described_class.repository_for(expected_repository_resolver)).to eq(expected_repository) + expect(described_class.repository_for(expected_container)).to eq(expected_repository) end it 'returns nil when container is nil' do diff --git a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb index 6b296d0e78a..ac72b31d5a4 100644 --- a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name| +RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name, migration_name| context 'when filter not provided (all behavior)' do let(:filters) { {} } @@ -28,16 +28,33 @@ RSpec.shared_examples 'search results filtered by archived' do |feature_flag_nam end end - context "when the #{feature_flag_name} feature flag is disabled" do - let(:filters) { {} } + if feature_flag_name.present? + context "when the #{feature_flag_name} feature flag is disabled" do + let(:filters) { {} } + + before do + stub_feature_flags("#{feature_flag_name}": false) + end - before do - stub_feature_flags("#{feature_flag_name}": false) + it 'returns archived and unarchived results' do + expect(results.objects(scope)).to include unarchived_result + expect(results.objects(scope)).to include archived_result + end end + end - it 'returns archived and unarchived results' do - expect(results.objects(scope)).to include unarchived_result - expect(results.objects(scope)).to include archived_result + if migration_name.present? + context "when the #{migration_name} is not completed" do + let(:filters) { {} } + + before do + set_elasticsearch_migration_to(migration_name.to_s, including: false) + end + + it 'returns archived and unarchived results' do + expect(results.objects(scope)).to include unarchived_result + expect(results.objects(scope)).to include archived_result + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb deleted file mode 100644 index 9dc18555340..00000000000 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'tracked issuable events' do - before do - stub_application_setting(usage_ping_enabled: true) - end - - def count_unique(date_from: Date.today.beginning_of_week, date_to: 1.week.from_now) - Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) - end - - specify do - aggregate_failures do - expect(track_action({ author: user1 }.merge(track_params))).to be_truthy - expect(track_action({ author: user1 }.merge(track_params))).to be_truthy - expect(track_action({ author: user2 }.merge(track_params))).to be_truthy - expect(count_unique).to eq(2) - end - end - - it 'does not track edit actions if author is not present' do - expect(track_action({ author: nil }.merge(track_params))).to be_nil - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do - it_behaves_like 'tracked issuable events' - - it 'emits snowplow event' do - track_action({ author: user1 }.merge(track_params)) - - expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params)) - end -end - -RSpec.shared_examples 'tracked issuable internal event for given event params' do - it_behaves_like 'tracked issuable events' - - it_behaves_like 'internal event tracking' do - subject(:track_event) { track_action({ author: user1 }.merge(track_params)) } - - let(:user) { user1 } - let(:namespace) { project&.namespace } - end -end - -RSpec.shared_examples 'tracked issuable internal event with project' do - it_behaves_like 'tracked issuable internal event for given event params' do - let(:track_params) { original_params || { project: project } } - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events with project' do - it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do - let(:context) do - Gitlab::Tracking::ServicePingContext - .new(data_source: :redis_hll, event: event_property) - .to_h - end - - let(:track_params) { original_params || { project: project } } - let(:event_params) { { project: project }.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) } - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events with namespace' do - it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do - let(:context) do - Gitlab::Tracking::ServicePingContext - .new(data_source: :redis_hll, event: event_property) - .to_h - end - - let(:track_params) { { namespace: namespace } } - let(:event_params) { track_params.merge(label: event_label, property: event_property, context: [context]) } - end -end - -RSpec.shared_examples 'does not track with namespace when feature flag is disabled' do |feature_flag| - context "when feature flag #{feature_flag} is disabled" do - it 'does not track action' do - stub_feature_flags(feature_flag => false) - - expect(track_action(author: user1, namespace: namespace)).to be_nil - end - end -end diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb index 0aa98517444..575f48c43e0 100644 --- a/spec/support/shared_examples/lib/menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/menus_shared_examples.rb @@ -62,6 +62,13 @@ RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do end end +RSpec.shared_examples_for 'a panel instantiable by the anonymous user' do + it do + context.instance_variable_set(:@current_user, nil) + expect(described_class.new(context)).to be_a(described_class) + end +end + RSpec.shared_examples_for 'a panel with uniquely identifiable menu items' do let(:menu_items) do subject.instance_variable_get(:@menus) diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb index 5e8aebb4f29..6a43b9b4300 100644 --- a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:| +RSpec.shared_examples 'User profile menu' do | + icon:, active_route:, avatar_shape: 'rect', expect_avatar: false, entity_id: nil, + # A nil title will fall back to user.name. + title: nil +| let_it_be(:current_user) { build(:user) } let_it_be(:user) { build(:user) } @@ -17,13 +21,19 @@ RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:| end it 'renders the correct title' do - expect(subject.title).to eq title + expect(subject.title).to eq(title || user.name) end it 'renders the correct icon' do expect(subject.sprite_icon).to eq icon end + it 'renders the correct avatar' do + expect(subject.avatar).to eq(expect_avatar ? user.avatar_url : nil) + expect(subject.avatar_shape).to eq(avatar_shape) + expect(subject.entity_id).to eq(entity_id) + end + it 'defines correct active route' do expect(subject.active_routes[:path]).to be active_route 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 5f59d43ad19..179bbc8734d 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 @@ -55,6 +55,8 @@ RSpec.shared_examples 'it has loose foreign keys' do end RSpec.shared_examples 'cleanup by a loose foreign key' do + include LooseForeignKeysHelper + 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 } @@ -75,9 +77,7 @@ RSpec.shared_examples 'cleanup by a loose foreign key' do expect(find_model).to be_present - LooseForeignKeys::DeletedRecord.using_connection(parent.connection) do - LooseForeignKeys::ProcessDeletedRecordsService.new(connection: parent.connection).execute - end + process_loose_foreign_key_deletions(record: parent) if foreign_key_definition.on_delete.eql?(:async_delete) expect(find_model).not_to be_present diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index cf1ab7697ab..987060d73b9 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -54,6 +54,14 @@ RSpec.shared_examples 'an email with X-GitLab headers containing IDs' do expect(subject.header["X-GitLab-#{model.class.name}-IID"]).to eq nil end end + + it 'has X-GitLab-*-State header if model has state defined' do + if model.respond_to?(:state) + is_expected.to have_header "X-GitLab-#{model.class.name}-State", model.state.to_s + else + expect(subject.header["X-GitLab-#{model.class.name}-State"]).to eq nil + end + end end RSpec.shared_examples 'an email with X-GitLab headers containing project details' do diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb index fdb31fa5d9d..37c338a7712 100644 --- a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb +++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb @@ -32,3 +32,110 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do end end end + +# Shared examples for testing migration that adds a single widget to a work item type +# +# It expects the following variables +# - `target_type_enum_value`: Int, enum value for the target work item type, typically defined in the migration +# as a constant +# - `target_type`: Symbol, the target type's name +# - `additional_types`: Hash (optional), name of work item types and their corresponding enum value that are defined +# at the time the migration was created but are missing from `base_types`. +# - `widgets_for_type`: Hash, name of the widgets included in the target type with their corresponding enum value +RSpec.shared_examples 'migration that adds a widget to a work item type' do + include MigrationHelpers::WorkItemTypesHelper + + let(:work_item_types) { table(:work_item_types) } + let(:work_item_widget_definitions) { table(:work_item_widget_definitions) } + let(:additional_base_types) { try(:additional_types) || {} } + let(:base_types) do + { + issue: 0, + incident: 1, + test_case: 2, + requirement: 3, + task: 4, + objective: 5, + key_result: 6, + epic: 7 + }.merge!(additional_base_types) + end + + after(:all) do + # Make sure base types are recreated after running the migration + # because migration specs are not run in a transaction + reset_work_item_types + end + + before do + # Database needs to be in a similar state as when the migration was created + reset_db_state_prior_to_migration + end + + describe '#up' do + it "adds widget to work item type", :aggregate_failures do + expect do + migrate! + end.to change { work_item_widget_definitions.count }.by(1) + + work_item_type = work_item_types.find_by(namespace_id: nil, base_type: target_type_enum_value) + created_widget = work_item_widget_definitions.last + + expect(created_widget).to have_attributes( + widget_type: described_class::WIDGET_ENUM_VALUE, + name: described_class::WIDGET_NAME, + work_item_type_id: work_item_type.id + ) + end + + context 'when type does not exist' do + it 'skips creating the new widget definition' do + work_item_types.where(namespace_id: nil, base_type: base_types[target_type]).delete_all + + expect do + migrate! + end.to not_change(work_item_widget_definitions, :count) + end + end + end + + describe '#down' do + it "removes widget from work item type" do + migrate! + + expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1) + end + end + + def reset_db_state_prior_to_migration + work_item_types.delete_all + + base_types.each do |type_sym, type_enum| + create_work_item_type!(type_sym.to_s.titleize, type_enum) + end + + target_type_record = work_item_types.find_by_name(target_type.to_s.titleize) + + widgets = widgets_for_type.map do |widget_name_value, widget_enum_value| + { + work_item_type_id: target_type_record.id, + name: widget_name_value, + widget_type: widget_enum_value + } + end + + # Creating all widgets for the type so the state in the DB is as close as possible to the actual state + work_item_widget_definitions.upsert_all( + widgets, + unique_by: :index_work_item_widget_definitions_on_default_witype_and_name + ) + end + + def create_work_item_type!(type_name, type_enum_value) + work_item_types.create!( + name: type_name, + namespace_id: nil, + base_type: type_enum_value + ) + end +end diff --git a/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb index efd27a051fe..eb37fe66c11 100644 --- a/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb @@ -78,5 +78,16 @@ RSpec.shared_examples 'includes LinkableItem concern' do expect(described_class.for_items(item, item2)).to contain_exactly(target_link) end end + + describe '.for_source_and_target' do + let_it_be(:item3) { create(:work_item, project: project) } + let_it_be(:link1) { create(link_factory, source: item, target: item1) } + let_it_be(:link2) { create(link_factory, source: item, target: item2) } + let_it_be(:link3) { create(link_factory, source: item, target: item3) } + + it 'includes links for provided source and target' do + expect(described_class.for_source_and_target(item, [item1, item2])).to contain_exactly(link1, link2) + end + end end end diff --git a/spec/support/shared_examples/models/group_shared_examples.rb b/spec/support/shared_examples/models/group_shared_examples.rb index 9f3359ba4ab..6397e7a87d7 100644 --- a/spec/support/shared_examples/models/group_shared_examples.rb +++ b/spec/support/shared_examples/models/group_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples 'checks self and root ancestor feature flag' do let_it_be(:root_group) { create(:group) } let_it_be(:group) { create(:group, parent: root_group) } - let_it_be(:project) { create(:project, group: group) } subject { group.public_send(feature_flag_method) } @@ -41,3 +40,47 @@ RSpec.shared_examples 'checks self and root ancestor feature flag' do it { is_expected.to be_truthy } end end + +RSpec.shared_examples 'checks self (project) and root ancestor feature flag' do + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:project) { create(:project, group: group) } + + subject { project.public_send(feature_flag_method) } + + context 'when FF is enabled for the root group' do + before do + stub_feature_flags(feature_flag => root_group) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is enabled for the group' do + before do + stub_feature_flags(feature_flag => group) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is enabled for the project' do + before do + stub_feature_flags(feature_flag => project) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is disabled globally' do + before do + stub_feature_flags(feature_flag => false) + end + + it { is_expected.to be_falsey } + end + + context 'when FF is enabled globally' do + it { is_expected.to be_truthy } + end +end diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb index 329cb812a08..5c783b5cfa7 100644 --- a/spec/support/shared_examples/models/members_notifications_shared_example.rb +++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'members notifications' do |entity_type| + let_it_be(:user) { create(:user) } + let(:notification_service) { double('NotificationService').as_null_object } before do @@ -8,7 +10,7 @@ RSpec.shared_examples 'members notifications' do |entity_type| end describe "#after_create" do - let(:member) { build(:"#{entity_type}_member", "#{entity_type}": create(entity_type.to_s)) } + let(:member) { build(:"#{entity_type}_member", "#{entity_type}": create(entity_type.to_s), user: user) } it "sends email to user" do expect(notification_service).to receive(:"new_#{entity_type}_member").with(member) @@ -35,7 +37,9 @@ RSpec.shared_examples 'members notifications' do |entity_type| describe '#after_commit' do context 'on creation of a member requesting access' do - let(:member) { build(:"#{entity_type}_member", :access_request, "#{entity_type}": create(entity_type.to_s)) } + let(:member) do + build(:"#{entity_type}_member", :access_request, "#{entity_type}": create(entity_type.to_s), user: user) + end it "calls NotificationService.new_access_request" do expect(notification_service).to receive(:new_access_request).with(member) diff --git a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb new file mode 100644 index 00000000000..0b3e8516d25 --- /dev/null +++ b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace visits model' do + it { is_expected.to validate_presence_of(:entity_id) } + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:visited_at) } + + describe '#visited_around?' do + context 'when the checked time matches a recent visit' do + [-15.minutes, 15.minutes].each do |time_diff| + it 'returns true' do + expect(described_class.visited_around?(entity_id: entity.id, user_id: user.id, + time: base_time + time_diff)).to be(true) + end + end + end + + context 'when the checked time does not match a recent visit' do + [-16.minutes, 16.minutes].each do |time_diff| + it 'returns false' do + expect(described_class.visited_around?(entity_id: entity.id, user_id: user.id, + time: base_time + time_diff)).to be(false) + end + end + end + end +end diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb index 9224e01b1fe..23ec4a632b7 100644 --- a/spec/support/shared_examples/redis/redis_shared_examples.rb +++ b/spec/support/shared_examples/redis/redis_shared_examples.rb @@ -365,6 +365,90 @@ RSpec.shared_examples "redis_shared_examples" do end end + describe "#parse_client_tls_options" do + let(:dummy_certificate) { OpenSSL::X509::Certificate.new } + let(:dummy_key) { OpenSSL::PKey::RSA.new } + let(:resque_yaml_config_without_tls) { { url: 'redis://localhost:6379' } } + let(:resque_yaml_config_with_tls) do + { + url: 'rediss://localhost:6380', + ssl_params: { + cert_file: '/tmp/client.crt', + key_file: '/tmp/client.key' + } + } + end + + let(:parsed_config_with_tls) do + { + url: 'rediss://localhost:6380', + ssl_params: { + cert: dummy_certificate, + key: dummy_key + } + } + end + + before do + allow(::File).to receive(:exist?).and_call_original + allow(::File).to receive(:read).and_call_original + end + + context 'when configuration does not have TLS related options' do + it 'returns the coniguration as-is' do + expect(subject.send(:parse_client_tls_options, + resque_yaml_config_without_tls)).to eq(resque_yaml_config_without_tls) + end + end + + context 'when specified certificate file does not exist' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(false) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(true) + end + + it 'raises error about missing certificate file' do + expect do + subject.send(:parse_client_tls_options, + resque_yaml_config_with_tls) + end.to raise_error(Gitlab::Redis::Wrapper::InvalidPathError, + "Certificate file /tmp/client.crt specified in in `resque.yml` does not exist.") + end + end + + context 'when specified key file does not exist' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(true) + allow(::File).to receive(:read).with("/tmp/client.crt").and_return("DUMMY_CERTIFICATE") + allow(OpenSSL::X509::Certificate).to receive(:new).with("DUMMY_CERTIFICATE").and_return(dummy_certificate) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(false) + end + + it 'raises error about missing key file' do + expect do + subject.send(:parse_client_tls_options, + resque_yaml_config_with_tls) + end.to raise_error(Gitlab::Redis::Wrapper::InvalidPathError, + "Key file /tmp/client.key specified in in `resque.yml` does not exist.") + end + end + + context 'when configuration valid TLS related options' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(true) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(true) + allow(::File).to receive(:read).with("/tmp/client.crt").and_return("DUMMY_CERTIFICATE") + allow(::File).to receive(:read).with("/tmp/client.key").and_return("DUMMY_KEY") + allow(OpenSSL::X509::Certificate).to receive(:new).with("DUMMY_CERTIFICATE").and_return(dummy_certificate) + allow(OpenSSL::PKey).to receive(:read).with("DUMMY_KEY").and_return(dummy_key) + end + + it "converts cert_file and key_file appropriately" do + expect(subject.send(:parse_client_tls_options, resque_yaml_config_with_tls)).to eq(parsed_config_with_tls) + end + end + end + describe '#fetch_config' do before do FileUtils.mkdir_p(File.join(rails_root, 'config')) diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb index 74dbec063e0..625f16824b4 100644 --- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -72,7 +72,7 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do first_token = assigns(:active_access_tokens).first.as_json expect(first_token['name']).to eq("Token1") - expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(first_token['expires_at']).to eq(expires_1_day_from_now.iso8601) end it "orders tokens on id in case token has same expires_at" do @@ -82,11 +82,11 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do first_token = assigns(:active_access_tokens).first.as_json expect(first_token['name']).to eq("Token3") - expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(first_token['expires_at']).to eq(expires_1_day_from_now.iso8601) second_token = assigns(:active_access_tokens).second.as_json expect(second_token['name']).to eq("Token1") - expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(second_token['expires_at']).to eq(expires_1_day_from_now.iso8601) end end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb index 6eceb7c350d..04f340fef37 100644 --- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -518,6 +518,39 @@ RSpec.shared_examples 'graphql issue list request spec' do end end + context 'when fetching external participants' do + before_all do + issue_a.update!(external_author: 'user@example.com') + end + + let(:fields) do + <<~QUERY + nodes { + id + externalAuthor + } + QUERY + end + + it 'returns the email address' do + post_query + + emails = issues_data.pluck('externalAuthor').compact + expect(emails).to contain_exactly('user@example.com') + end + + context 'when user does not have access to view emails' do + let(:current_user) { external_user } + + it 'obfuscates the email address' do + post_query + + emails = issues_data.pluck('externalAuthor').compact + expect(emails).to contain_exactly("us*****@e*****.c**") + end + end + end + context 'when fetching escalation status' do let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) } let_it_be(:incident_type) { WorkItems::Type.default_by_type(:incident) } diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb new file mode 100644 index 00000000000..a9c422c8f2d --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql work item list request spec' do + let(:work_item_ids) { graphql_dig_at(work_item_data, :id) } + + it_behaves_like 'a working graphql query' do + before do + post_query + end + end + + describe 'filters' do + before do + post_query + end + + context 'when filtering by author username' do + let(:author) { create(:author) } + let(:authored_work_item) { create(:work_item, author: author, **container_build_params) } + + let(:item_filter_params) { { author_username: authored_work_item.author.username } } + + it 'returns correct results' do + expect(work_item_ids).to contain_exactly(authored_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state' do + let_it_be(:opened_work_item) { create(:work_item, :opened, **container_build_params) } + let_it_be(:closed_work_item) { create(:work_item, :closed, **container_build_params) } + + context 'when filtering by state opened' do + let(:item_filter_params) { { state: :opened } } + + it 'filters by state' do + expect(work_item_ids).to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(closed_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state closed' do + let(:item_filter_params) { { state: :closed } } + + it 'filters by state' do + expect(work_item_ids).not_to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).to include(closed_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by type' do + let_it_be(:issue_work_item) { create(:work_item, :issue, **container_build_params) } + let_it_be(:task_work_item) { create(:work_item, :task, **container_build_params) } + + context 'when filtering by issue type' do + let(:item_filter_params) { { types: [:ISSUE] } } + + it 'filters by type' do + expect(work_item_ids).to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(task_work_item.to_global_id.to_s) + end + end + + context 'when filtering by task type' do + let(:item_filter_params) { { types: [:TASK] } } + + it 'filters by type' do + expect(work_item_ids).not_to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).to include(task_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by iid' do + let_it_be(:work_item_by_iid) { create(:work_item, **container_build_params) } + + context 'when using the iid filter' do + let(:item_filter_params) { { iid: work_item_by_iid.iid.to_s } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + + context 'when using the iids filter' do + let(:item_filter_params) { { iids: [work_item_by_iid.iid.to_s] } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + end + end + + def work_item_data + graphql_data.dig(*work_item_node_path) + end +end diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb index f2c38d70508..00e50b07909 100644 --- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb @@ -8,12 +8,25 @@ RSpec.shared_examples 'MLflow|Not Found - Resource Does Not Exist' do end end -RSpec.shared_examples 'MLflow|Requires api scope' do +RSpec.shared_examples 'MLflow|Requires api scope and write permission' do context 'when user has access but token has wrong scope' do let(:access_token) { tokens[:read] } it { is_expected.to have_gitlab_http_status(:forbidden) } end + + context 'when user has access but is not allowed to write' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :write_model_experiments, project) + .and_return(false) + end + + it "is Unauthorized" do + is_expected.to have_gitlab_http_status(:unauthorized) + end + end end RSpec.shared_examples 'MLflow|Requires read_api scope' do 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 6b6bf375827..5f043cdd996 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 @@ -280,7 +280,10 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end end - status = :not_found if scope == :group && params[:package_name_type] == :non_existing && !params[:request_forward] + if (scope == :group && params[:package_name_type] == :non_existing) && + (!params[:request_forward] || (!params[:auth] && params[:request_forward] && params[:visibility] != :public)) + status = :not_found + end # Check the error message for :not_found example_name = 'returning response status with error' if status == :not_found @@ -873,3 +876,67 @@ RSpec.shared_examples 'rejects invalid package names' do expect(Gitlab::Json.parse(response.body)).to eq({ 'error' => 'package_name should be a valid file path' }) end end + +RSpec.shared_examples 'handling get metadata requests for packages in multiple projects' do + let_it_be(:project2) { create(:project, namespace: namespace) } + let_it_be(:package2) do + create(:npm_package, + project: project2, + name: "@#{group.path}/scoped_package", + version: '1.2.0') + end + + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { get(url, headers: headers) } + + before_all do + project.update!(visibility: 'private') + + group.add_guest(user) + project.add_reporter(user) + project2.add_reporter(user) + end + + it 'includes all matching package versions in the response' do + subject + + expect(json_response['versions'].keys).to match_array([package.version, package2.version]) + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(npm_allow_packages_in_multiple_projects: false) + end + + it 'returns matching package versions from only one project' do + subject + + expect(json_response['versions'].keys).to match_array([package2.version]) + end + end + + context 'with limited access to the project with the last package version' do + before_all do + project2.add_guest(user) + end + + it 'includes matching package versions from authorized projects in the response' do + subject + + expect(json_response['versions'].keys).to contain_exactly(package.version) + end + end + + context 'with limited access to the project with the first package version' do + before do + project.add_guest(user) + end + + it 'includes matching package versions from authorized projects in the response' do + subject + + expect(json_response['versions'].keys).to contain_exactly(package2.version) + end + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 2e66bae26ba..1be99040ae5 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 @@ -373,14 +373,6 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st end it_behaves_like 'bumping the package last downloaded at field' - - context 'when nuget_normalized_version feature flag is disabled' do - before do - stub_feature_flags(nuget_normalized_version: false) - end - - it_behaves_like 'returning response status', :not_found - end end end end @@ -710,4 +702,38 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| it_behaves_like 'returning response status', :forbidden end + + context 'when package duplicates are not allowed' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + let_it_be(:existing_package) { create(:nuget_package, project: project) } + let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } } + let_it_be(:package_settings) do + create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false) + end + + before do + allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance| + allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata)) + end + end + + it_behaves_like 'returning response status', :conflict unless symbol_package + it_behaves_like 'returning response status', :created if symbol_package + + context 'when exception_regex is set' do + before do + package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*") + end + + it_behaves_like 'returning response status', :created + end + + context 'when nuget_duplicates_option feature flag is disabled' do + before do + stub_feature_flags(nuget_duplicates_option: false) + end + + it_behaves_like 'returning response status', :created + end + end end diff --git a/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb b/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb new file mode 100644 index 00000000000..85db36013dd --- /dev/null +++ b/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an endpoint with keyset pagination' do |invalid_order: 'name', invalid_sort: 'asc'| + include KeysetPaginationHelpers + + let(:keyset_params) { { pagination: 'keyset', per_page: 1 } } + let(:additional_params) { {} } + + subject do + get api_call, params: keyset_params.merge(additional_params) + response + end + + context 'on making requests with supported ordering structure' do + it 'includes keyset url params in the url response' do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to include_keyset_url_params + end + + it 'does not include pagination headers' do + is_expected.to have_gitlab_http_status(:ok) + is_expected.not_to include_pagination_headers + end + + it 'paginates the records correctly', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + records = json_response + expect(records.size).to eq(1) + expect(records.first['id']).to eq(first_record.id) + + get api_call, params: pagination_params_from_next_url(response) + + expect(response).to have_gitlab_http_status(:ok) + records = Gitlab::Json.parse(response.body) + expect(records.size).to eq(1) + expect(records.first['id']).to eq(second_record.id) + end + end + + context 'on making requests with unsupported ordering structure' do + let(:additional_params) { { order_by: invalid_order, sort: invalid_sort } } + + if invalid_order + it 'returns error', :aggregate_failures do + is_expected.to have_gitlab_http_status(:method_not_allowed) + expect(json_response['error']).to eq('Keyset pagination is not yet available for this type of request') + end + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index dafa324b3c6..48d3e438322 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -148,6 +148,12 @@ RSpec.shared_examples 'rate-limited token requests' do expect(response).not_to have_gitlab_http_status(:too_many_requests) end + matched = throttle_types[throttle_setting_prefix] + + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + matched = 'throttle_authenticated_get_protected_paths_api' + end + arguments = a_hash_including({ message: 'Rack_Attack', status: 429, @@ -155,7 +161,7 @@ RSpec.shared_examples 'rate-limited token requests' do remote_ip: '127.0.0.1', request_method: request_method, path: request_args.first, - matched: throttle_types[throttle_setting_prefix] + matched: matched }.merge(log_data)) expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once @@ -166,7 +172,14 @@ RSpec.shared_examples 'rate-limited token requests' do end it_behaves_like 'tracking when dry-run mode is set' do - let(:throttle_name) { throttle_types[throttle_setting_prefix] } + let(:throttle_name) do + name = throttle_types[throttle_setting_prefix] + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + name = 'throttle_authenticated_get_protected_paths_api' + end + + name + end def do_request make_request(request_args) @@ -315,7 +328,13 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do expect(response).not_to have_gitlab_http_status(:too_many_requests) end - arguments = a_hash_including({ + matched = throttle_types[throttle_setting_prefix] + + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + matched = 'throttle_authenticated_get_protected_paths_web' + end + + arguments = a_hash_including( message: 'Rack_Attack', status: 429, env: :throttle, @@ -324,15 +343,22 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do path: url_that_requires_authentication, user_id: user.id, 'meta.user' => user.username, - matched: throttle_types[throttle_setting_prefix] - }) + matched: matched + ) expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count) end it_behaves_like 'tracking when dry-run mode is set' do - let(:throttle_name) { throttle_types[throttle_setting_prefix] } + let(:throttle_name) do + name = throttle_types[throttle_setting_prefix] + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + name = 'throttle_authenticated_get_protected_paths_web' + end + + name + end def do_request request_authenticated_web_url diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index db2b448f567..94467ad53fa 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -40,7 +40,7 @@ end RSpec.shared_examples 'incident management label service' do let_it_be(:project) { create(:project, :private) } - let_it_be(:user) { User.alert_bot } + let_it_be(:user) { Users::Internal.alert_bot } let(:service) { described_class.new(project, user) } subject(:execute) { service.execute } diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb index 3f95d6060ea..9624f7a4450 100644 --- a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb @@ -145,6 +145,77 @@ RSpec.shared_examples 'updating issuable labels' do end end +RSpec.shared_examples 'updating merged MR with locked labels' do + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id], add_label_ids: [label_c.id] } } + + it 'replaces unlocked labels with the ones in label_ids and adds those in add_label_ids' do + issuable.update!(labels: [label_b, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id, label_b.id, label_c.id], remove_label_ids: [label_a.id] } } + + it 'replaces unlocked labels with the ones in label_ids and does not remove locked label in remove_label_ids' do + issuable.update!(labels: [label_a, label_c, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label_c.id], remove_label_ids: [label_a.id, label_unlocked.id] } } + + before do + issuable.update!(labels: [label_a, label_unlocked]) + update_issuable(params) + end + + it 'adds the passed labels' do + expect(issuable.label_ids).to include(label_c.id) + end + + it 'removes the passed unlocked labels' do + expect(issuable.label_ids).to include(label_a.id) + expect(issuable.label_ids).not_to include(label_unlocked.id) + end + end + + context 'when same id is passed as add_label_ids and remove_label_ids' do + let(:params) { { add_label_ids: [label_a.id], remove_label_ids: [label_a.id] } } + + context 'for a label assigned to an issue' do + it 'does not remove the label' do + issuable.update!(labels: [label_a]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id) + end + end + + context 'for a label not assigned to an issue' do + it 'does not add the label' do + expect(issuable.label_ids).to be_empty + end + end + end + + context 'when duplicate label titles are given' do + let(:params) { { labels: [label_c.title, label_c.title] } } + + it 'assigns the label once' do + update_issuable(params) + + expect(issuable.labels).to contain_exactly(label_c) + end + end +end + RSpec.shared_examples 'keeps issuable labels sorted after update' do before do update_issuable(label_ids: [label_b.id]) diff --git a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb index 1532e870dcc..b955b71a6bb 100644 --- a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true -RSpec.shared_examples 'a destroyable issuable link' do |required_role: :reporter| +RSpec.shared_examples 'a destroyable issuable link' do context 'when successfully removes an issuable link' do - before do - [issuable_link.target, issuable_link.source].each do |issuable| - issuable.resource_parent.try(:"add_#{required_role}", user) - end - end - it 'removes related issue' do expect { subject }.to change { issuable_link.class.count }.by(-1) end @@ -28,6 +22,9 @@ RSpec.shared_examples 'a destroyable issuable link' do |required_role: :reporter end context 'when failing to remove an issuable link' do + let_it_be(:non_member) { create(:user) } + let(:user) { non_member } + it 'does not remove relation' do expect { subject }.not_to change { issuable_link.class.count }.from(1) end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb deleted file mode 100644 index 9b2e038a331..00000000000 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ /dev/null @@ -1,193 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'misconfigured dashboard service response' do |status_code, message = nil| - it 'returns an appropriate message and status code', :aggregate_failures do - result = service_call - - expect(result.keys).to contain_exactly(:message, :http_status, :status) - expect(result[:status]).to eq(:error) - expect(result[:http_status]).to eq(status_code) - expect(result[:message]).to eq(message) if message - end -end - -RSpec.shared_examples 'valid dashboard service response for schema' do - it 'returns a json representation of the dashboard' do - result = service_call - - expect(result.keys).to contain_exactly(:dashboard, :status) - expect(result[:status]).to eq(:success) - - schema_path = Rails.root.join('spec/fixtures', dashboard_schema) - validator = JSONSchemer.schema(schema_path) - expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true - end -end - -RSpec.shared_examples 'valid dashboard service response' do - let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/dashboard.json' } - - it_behaves_like 'valid dashboard service response for schema' -end - -RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do - specify do - expect_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - expect(loader).to receive(:load_raw!).once.and_call_original - end - - described_class.new(*service_params).get_dashboard - described_class.new(*service_params).get_dashboard - end -end - -# This spec is applicable for predefined/out-of-the-box dashboard services. -RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do - specify do - allow_next_instance_of(described_class) do |service| - allow(service).to receive(:dashboard_version).and_return('1', '2') - end - - expect_file_read(Rails.root.join(described_class::DASHBOARD_PATH)).twice.and_call_original - - service = described_class.new(*service_params) - - service.get_dashboard - service.get_dashboard - end -end - -# This spec is applicable for predefined/out-of-the-box dashboard services. -# This shared_example requires the following variables to be defined: -# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml' -# dashboard_version: The version string used in the cache_key. -RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do - specify do - dashboard = File.read(Rails.root.join(dashboard_path)) - expect(dashboard_version).to eq(Digest::SHA256.hexdigest(dashboard)) - end -end - -RSpec.shared_examples 'valid embedded dashboard service response' do - let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json' } - - it_behaves_like 'valid dashboard service response for schema' -end - -RSpec.shared_examples 'raises error for users with insufficient permissions' do - context 'when the user does not have sufficient access' do - let(:user) { build(:user) } - - it_behaves_like 'misconfigured dashboard service response', :unauthorized - end - - context 'when the user is anonymous' do - let(:user) { nil } - - it_behaves_like 'misconfigured dashboard service response', :unauthorized - end -end - -RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence| - context "dashboard template: #{dashboard_template}" do - let(:dashboard) { dashboard_template } - let(:dashboard_attrs) do - { - commit_message: commit_message, - branch_name: branch, - start_branch: project.default_branch, - encoding: 'text', - file_path: ".gitlab/dashboards/#{file_name}", - file_content: file_content_hash.to_yaml - } - end - - it 'delegates commit creation to Files::CreateService', :aggregate_failures do - service_instance = instance_double(::Files::CreateService) - allow(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).and_return(double(process: file_content_hash)) - expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance) - expect(service_instance).to receive(:execute).and_return(status: :success) - - service_call - end - - context 'user has defined custom metrics' do - it 'uses external service to includes them into new file content', :aggregate_failures do - service_instance = double(::Gitlab::Metrics::Dashboard::Processor) - expect(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).with(project, file_content_hash, sequence, {}).and_return(service_instance) - expect(service_instance).to receive(:process).and_return(file_content_hash) - expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(double(execute: { status: :success })) - - service_call - end - end - end -end - -RSpec.shared_examples 'misconfigured dashboard service response with stepable' do |status_code, message = nil| - it 'returns an appropriate message and status code', :aggregate_failures do - result = service_call - - expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step) - expect(result[:status]).to eq(:error) - expect(result[:http_status]).to eq(status_code) - expect(result[:message]).to eq(message) if message - end -end - -RSpec.shared_examples 'updates gitlab_metrics_dashboard_processing_time_ms metric' do - specify :prometheus do - service_call - metric = subject.send(:processing_time_metric) - labels = subject.send(:processing_time_metric_labels) - - expect(metric.get(labels)).to be > 0 - end -end - -RSpec.shared_examples '#raw_dashboard raises error if dashboard loading fails' do - context 'when yaml is too large' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') - end - end - - it 'raises error' do - expect { subject.raw_dashboard }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::LayoutError, - 'The parsed YAML is too big' - ) - end - end - - context 'when yaml loader returns error' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::FormatError, 'Invalid configuration format') - end - end - - it 'raises error' do - expect { subject.raw_dashboard }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::LayoutError, - 'Invalid yaml' - ) - end - end - - context 'when yaml is not a hash' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::Yaml::NotHashError, 'Invalid configuration format') - end - end - - it 'returns nil' do - expect(subject.raw_dashboard).to eq({}) - end - end -end diff --git a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb index e77d73d1c72..621fb99afe5 100644 --- a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos migrated_record = record_class.find_by_id(record.id) migrated_fields.each do |field| - expect(migrated_record.public_send(field)).to eq(User.ghost) + expect(migrated_record.public_send(field)).to eq(Users::Internal.ghost) end end @@ -47,7 +47,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos migrated_record = record_class.find_by_id(record.id) - check_user = always_ghost ? User.ghost : user + check_user = always_ghost ? Users::Internal.ghost : user migrated_fields.each do |field| expect(migrated_record.public_send(field)).to eq(check_user) diff --git a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb deleted file mode 100644 index d9e906ebb75..00000000000 --- a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'pages size limit is' do |size_limit| - context "when size is below the limit" do - before do - allow(metadata).to receive(:total_size).and_return(size_limit - 1.megabyte) - allow(metadata).to receive(:entries).and_return([]) - end - - it 'updates pages correctly' do - subject.execute - - expect(deploy_status.description).not_to be_present - expect(project.pages_metadatum).to be_deployed - end - end - - context "when size is above the limit" do - before do - allow(metadata).to receive(:total_size).and_return(size_limit + 1.megabyte) - allow(metadata).to receive(:entries).and_return([]) - end - - it 'limits the maximum size of gitlab pages' do - subject.execute - - expect(deploy_status.description) - .to match(/artifacts for pages are too large/) - expect(deploy_status).to be_script_failure - end - end -end diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb new file mode 100644 index 00000000000..ce607a6b956 --- /dev/null +++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with scan result policy blocking protected branches' do + before do + create( + :scan_result_policy_read, + :blocking_protected_branches, + project: project) + + stub_licensed_features(security_orchestration_policies: true) + end +end diff --git a/spec/support/shared_examples/services/users/build_service_shared_examples.rb b/spec/support/shared_examples/services/users/build_service_shared_examples.rb index e448f2f874b..45ebc837e6d 100644 --- a/spec/support/shared_examples/services/users/build_service_shared_examples.rb +++ b/spec/support/shared_examples/services/users/build_service_shared_examples.rb @@ -33,57 +33,6 @@ RSpec.shared_examples 'common user build items' do end RSpec.shared_examples_for 'current user not admin build items' do - using RSpec::Parameterized::TableSyntax - - context 'with "user_default_external" application setting' do - where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do - true | nil | 'fl@example.com' | nil | true - true | true | 'fl@example.com' | nil | true - true | false | 'fl@example.com' | nil | true # admin difference - - true | nil | 'fl@example.com' | '' | true - true | true | 'fl@example.com' | '' | true - true | false | 'fl@example.com' | '' | true # admin difference - - true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - - true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference - - false | nil | 'fl@example.com' | nil | false - false | true | 'fl@example.com' | nil | false # admin difference - false | false | 'fl@example.com' | nil | false - - false | nil | 'fl@example.com' | '' | false - false | true | 'fl@example.com' | '' | false # admin difference - false | false | 'fl@example.com' | '' | false - - false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - - false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - end - - with_them do - before do - stub_application_setting(user_default_external: user_default_external) - stub_application_setting(user_default_internal_regex: user_default_internal_regex) - - params.merge!({ external: external, email: email }.compact) - end - - it 'sets the value of Gitlab::CurrentSettings.user_default_external' do - expect(user.external).to eq(result) - end - end - end - context 'when "email_confirmation_setting" application setting is set to `hard`' do before do stub_application_setting_enum('email_confirmation_setting', 'hard') diff --git a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb index eb03f0888b9..5d24aa10453 100644 --- a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb +++ b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb @@ -32,7 +32,7 @@ RSpec.shared_examples 'migrating records to the ghost user' do |record_class, fi migrated_record = record_class.find_by_id(record.id) migrated_fields.each do |field| - expect(migrated_record.public_send(field)).to eq(User.ghost) + expect(migrated_record.public_send(field)).to eq(Users::Internal.ghost) end end end diff --git a/spec/support/shared_examples/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/users/pages_visits_shared_examples.rb new file mode 100644 index 00000000000..1a613fa0aed --- /dev/null +++ b/spec/support/shared_examples/users/pages_visits_shared_examples.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace visits tracking worker' do + let_it_be(:base_time) { DateTime.now } + + context 'when params are provided' do + before do + worker.perform(entity_type, entity.id, user.id, base_time) + end + + include_examples 'an idempotent worker' do + let(:job_args) { [entity_type, entity.id, user.id, base_time] } + + it 'tracks the entity visit' do + latest_visit = model.last + + expect(model.count).to be(1) + expect(latest_visit[:entity_id]).to be(entity.id) + expect(latest_visit.user_id).to be(user.id) + end + end + + context 'when a visit occurs within 15 minutes of a previously tracked one' do + [-15.minutes, 15.minutes].each do |time_diff| + it 'does not track the visit' do + worker.perform(entity_type, entity.id, user.id, base_time + time_diff) + + expect(model.count).to be(1) + end + end + end + + context 'when a visit occurs more than 15 minutes away from a previously tracked one' do + [-16.minutes, 16.minutes].each do |time_diff| + it 'tracks the visit' do + worker.perform(entity_type, entity.id, user.id, base_time + time_diff) + + expect(model.count).to be > 1 + end + end + end + end + + context 'when user is missing' do + before do + worker.perform(entity_type, entity.id, nil, base_time) + end + + it 'does not do anything' do + expect(model.count).to be(0) + end + end + + context 'when entity is missing' do + before do + worker.perform(entity_type, nil, user.id, base_time) + end + + it 'does not do anything' do + expect(model.count).to be(0) + end + end +end diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb index 8ecb04bfdd6..1ea5eb6fd2e 100644 --- a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true RSpec.shared_examples 'it runs background migration jobs' do |tracking_database| + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + describe 'defining the job attributes' do it 'defines the data_consistency as always' do expect(described_class.get_data_consistency).to eq(:always) @@ -74,6 +78,38 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database| end end + context 'when disallow_database_ddl_feature_flags feature flag is disabled' do + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does not perform the job, reschedules it in the future, and logs a message' do + expect(worker).not_to receive(:perform_with_connection) + + expect(Sidekiq.logger).to receive(:info) do |payload| + expect(payload[:class]).to eq(described_class.name) + expect(payload[:database]).to eq(tracking_database) + expect(payload[:message]).to match(/skipping execution, migration rescheduled/) + end + + lease_attempts = 3 + delay = described_class::BACKGROUND_MIGRATIONS_DELAY + job_args = [10, 20] + + freeze_time do + worker.perform('Foo', job_args, lease_attempts) + + job = described_class.jobs.find { |job| job['args'] == ['Foo', job_args, lease_attempts] } + expect(job).to be, "Expected the job to be rescheduled with (#{job_args}, #{lease_attempts}), but it was not." + + expected_time = delay.to_i + Time.now.to_i + expect(job['at']).to eq(expected_time), + "Expected the job to be rescheduled in #{expected_time} seconds, " \ + "but it was rescheduled in #{job['at']} seconds." + end + end + end + context 'when execute_background_migrations feature flag is enabled' do before do stub_feature_flags(execute_background_migrations: true) diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb index 8fdd59d1d8c..cf488a4d753 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'batched background migrations execution worker' do include ExclusiveLeaseHelpers + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + it 'is a limited capacity worker' do expect(described_class.new).to be_a(LimitedCapacity::Worker) end @@ -100,6 +104,23 @@ RSpec.shared_examples 'batched background migrations execution worker' do end end + context 'when disable ddl flag is enabled' do + let(:migration) do + create(:batched_background_migration, :active, interval: job_interval, table_name: table_name) + end + + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does nothing' do + expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable) + expect(worker).not_to receive(:run_migration_job) + + worker.perform_work(database_name, migration.id) + end + end + context 'when the feature flag is enabled' do before do stub_feature_flags(execute_batched_migrations_on_schedule: true) diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index e7385f9abb6..003b8d07819 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, table_name| include ExclusiveLeaseHelpers + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + describe 'defining the job attributes' do it 'defines the data_consistency as always' do expect(described_class.get_data_consistency).to eq(:always) @@ -51,6 +55,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d expect(described_class.enabled?).to be_falsey end + + it 'returns false when disallow_database_ddl_feature_flags feature flag is enabled' do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + + expect(described_class.enabled?).to be_falsey + end end describe '#perform' do @@ -116,6 +126,18 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end + context 'when the disallow_database_ddl_feature_flags feature flag is enabled' do + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does nothing' do + expect(worker).not_to receive(:queue_migrations_for_execution) + + worker.perform + end + end + context 'when the execute_batched_migrations_on_schedule feature flag is enabled' do let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } let(:connection) { base_model.connection } diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb index dc475b92c0b..b25f39c5e74 100644 --- a/spec/support/sidekiq.rb +++ b/spec/support/sidekiq.rb @@ -5,9 +5,11 @@ RSpec.configure do |config| # We need to cleanup the queues before running jobs in specs because the # middleware might have written to redis redis_queues_cleanup! + redis_queues_metadata_cleanup! Sidekiq::Testing.inline!(&block) ensure redis_queues_cleanup! + redis_queues_metadata_cleanup! end # As we'll review the examples with this tag, we should either: |