diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
commit | 0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch) | |
tree | 4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /spec/support | |
parent | 744144d28e3e7fddc117924fef88de5d9674fe4c (diff) | |
download | gitlab-ce-0653e08efd039a5905f3fa4f6e9cef9f5d2f799c.tar.gz |
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'spec/support')
60 files changed, 1374 insertions, 247 deletions
diff --git a/spec/support/database/ci_tables.rb b/spec/support/database/ci_tables.rb deleted file mode 100644 index 99fc7ac2501..00000000000 --- a/spec/support/database/ci_tables.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -# This module stores the CI-related database tables which are -# going to be moved to a separate database. -module Database - module CiTables - def self.include?(name) - ci_tables.include?(name) - end - - def self.ci_tables - @@ci_tables ||= Set.new.tap do |tables| # rubocop:disable Style/ClassVars - tables.merge(Ci::ApplicationRecord.descendants.map(&:table_name).compact) - - # It was decided that taggings/tags are best placed with CI - # https://gitlab.com/gitlab-org/gitlab/-/issues/333413 - tables.add('taggings') - tables.add('tags') - end - end - end -end diff --git a/spec/support/database/cross-join-allowlist.yml b/spec/support/database/cross-join-allowlist.yml new file mode 100644 index 00000000000..2b4cfc6773a --- /dev/null +++ b/spec/support/database/cross-join-allowlist.yml @@ -0,0 +1,196 @@ +- "./ee/spec/controllers/operations_controller_spec.rb" +- "./ee/spec/controllers/projects/issues_controller_spec.rb" +- "./ee/spec/controllers/projects/security/vulnerabilities_controller_spec.rb" +- "./ee/spec/features/ci/ci_minutes_spec.rb" +- "./ee/spec/features/merge_request/user_merges_immediately_spec.rb" +- "./ee/spec/features/merge_request/user_sees_merge_widget_spec.rb" +- "./ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb" +- "./ee/spec/features/merge_trains/user_adds_merge_request_to_merge_train_spec.rb" +- "./ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb" +- "./ee/spec/features/projects/pipelines/pipeline_spec.rb" +- "./ee/spec/features/projects/settings/auto_rollback_spec.rb" +- "./ee/spec/features/projects/settings/pipeline_subscriptions_spec.rb" +- "./ee/spec/features/projects/settings/protected_environments_spec.rb" +- "./ee/spec/finders/ee/namespaces/projects_finder_spec.rb" +- "./ee/spec/finders/group_projects_finder_spec.rb" +- "./ee/spec/finders/security/findings_finder_spec.rb" +- "./ee/spec/graphql/ee/resolvers/namespace_projects_resolver_spec.rb" +- "./ee/spec/lib/analytics/devops_adoption/snapshot_calculator_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/migrate_security_scans_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings_spec.rb" +- "./ee/spec/lib/ee/gitlab/background_migration/populate_vulnerability_feedback_pipeline_id_spec.rb" +- "./ee/spec/lib/ee/gitlab/usage_data_spec.rb" +- "./ee/spec/migrations/schedule_populate_resolved_on_default_branch_column_spec.rb" +- "./ee/spec/models/ci/build_spec.rb" +- "./ee/spec/models/ci/minutes/project_monthly_usage_spec.rb" +- "./ee/spec/models/ci/pipeline_spec.rb" +- "./ee/spec/models/ee/vulnerability_spec.rb" +- "./ee/spec/models/merge_request_spec.rb" +- "./ee/spec/models/project_spec.rb" +- "./ee/spec/models/security/finding_spec.rb" +- "./ee/spec/models/security/scan_spec.rb" +- "./ee/spec/presenters/ci/pipeline_presenter_spec.rb" +- "./ee/spec/requests/api/ci/minutes_spec.rb" +- "./ee/spec/requests/api/graphql/ci/minutes/usage_spec.rb" +- "./ee/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb" +- "./ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb" +- "./ee/spec/requests/api/graphql/project/pipeline/security_report_summary_spec.rb" +- "./ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb" +- "./ee/spec/requests/api/groups_spec.rb" +- "./ee/spec/requests/api/namespaces_spec.rb" +- "./ee/spec/requests/api/vulnerability_findings_spec.rb" +- "./ee/spec/serializers/dashboard_environment_entity_spec.rb" +- "./ee/spec/serializers/dashboard_environments_serializer_spec.rb" +- "./ee/spec/services/auto_merge/add_to_merge_train_when_pipeline_succeeds_service_spec.rb" +- "./ee/spec/services/ci/create_pipeline_service/runnable_builds_spec.rb" +- "./ee/spec/services/ci/minutes/additional_packs/change_namespace_service_spec.rb" +- "./ee/spec/services/ci/minutes/additional_packs/create_service_spec.rb" +- "./ee/spec/services/ci/minutes/refresh_cached_data_service_spec.rb" +- "./ee/spec/services/ci/process_pipeline_service_spec.rb" +- "./ee/spec/services/ci/trigger_downstream_subscription_service_spec.rb" +- "./ee/spec/services/clear_namespace_shared_runners_minutes_service_spec.rb" +- "./ee/spec/services/deployments/auto_rollback_service_spec.rb" +- "./ee/spec/services/ee/ci/job_artifacts/destroy_all_expired_service_spec.rb" +- "./ee/spec/services/ee/ci/job_artifacts/destroy_batch_service_spec.rb" +- "./ee/spec/services/ee/issues/build_from_vulnerability_service_spec.rb" +- "./ee/spec/services/ee/merge_requests/create_pipeline_service_spec.rb" +- "./ee/spec/services/ee/merge_requests/refresh_service_spec.rb" +- "./ee/spec/services/security/report_summary_service_spec.rb" +- "./ee/spec/services/security/vulnerability_counting_service_spec.rb" +- "./ee/spec/workers/scan_security_report_secrets_worker_spec.rb" +- "./ee/spec/workers/security/store_scans_worker_spec.rb" +- "./spec/controllers/admin/runners_controller_spec.rb" +- "./spec/controllers/groups/runners_controller_spec.rb" +- "./spec/controllers/groups/settings/ci_cd_controller_spec.rb" +- "./spec/controllers/projects/logs_controller_spec.rb" +- "./spec/controllers/projects/merge_requests_controller_spec.rb" +- "./spec/controllers/projects/runners_controller_spec.rb" +- "./spec/controllers/projects/serverless/functions_controller_spec.rb" +- "./spec/controllers/projects/settings/ci_cd_controller_spec.rb" +- "./spec/features/admin/admin_runners_spec.rb" +- "./spec/features/groups/settings/ci_cd_spec.rb" +- "./spec/features/ide/user_opens_merge_request_spec.rb" +- "./spec/features/merge_request/user_merges_immediately_spec.rb" +- "./spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb" +- "./spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb" +- "./spec/features/merge_request/user_resolves_wip_mr_spec.rb" +- "./spec/features/merge_request/user_sees_deployment_widget_spec.rb" +- "./spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb" +- "./spec/features/merge_request/user_sees_merge_widget_spec.rb" +- "./spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb" +- "./spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb" +- "./spec/features/merge_request/user_sees_pipelines_spec.rb" +- "./spec/features/project_group_variables_spec.rb" +- "./spec/features/project_variables_spec.rb" +- "./spec/features/projects/badges/list_spec.rb" +- "./spec/features/projects/environments_pod_logs_spec.rb" +- "./spec/features/projects/infrastructure_registry_spec.rb" +- "./spec/features/projects/jobs_spec.rb" +- "./spec/features/projects/package_files_spec.rb" +- "./spec/features/projects/pipelines/pipeline_spec.rb" +- "./spec/features/projects/pipelines/pipelines_spec.rb" +- "./spec/features/projects/serverless/functions_spec.rb" +- "./spec/features/projects/settings/pipelines_settings_spec.rb" +- "./spec/features/runners_spec.rb" +- "./spec/features/security/project/internal_access_spec.rb" +- "./spec/features/security/project/private_access_spec.rb" +- "./spec/features/security/project/public_access_spec.rb" +- "./spec/features/triggers_spec.rb" +- "./spec/finders/ci/pipelines_finder_spec.rb" +- "./spec/finders/ci/pipelines_for_merge_request_finder_spec.rb" +- "./spec/finders/ci/runners_finder_spec.rb" +- "./spec/finders/clusters/knative_services_finder_spec.rb" +- "./spec/finders/projects/serverless/functions_finder_spec.rb" +- "./spec/frontend/fixtures/runner.rb" +- "./spec/graphql/mutations/ci/runner/delete_spec.rb" +- "./spec/graphql/resolvers/ci/group_runners_resolver_spec.rb" +- "./spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb" +- "./spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb" +- "./spec/graphql/types/ci/job_token_scope_type_spec.rb" +- "./spec/helpers/packages_helper_spec.rb" +- "./spec/lib/api/entities/package_spec.rb" +- "./spec/lib/gitlab/background_migration/migrate_legacy_artifacts_spec.rb" +- "./spec/lib/gitlab/prometheus/query_variables_spec.rb" +- "./spec/mailers/emails/pipelines_spec.rb" +- "./spec/migrations/cleanup_legacy_artifact_migration_spec.rb" +- "./spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb" +- "./spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb" +- "./spec/migrations/schedule_migrate_security_scans_spec.rb" +- "./spec/models/ci/build_spec.rb" +- "./spec/models/ci/job_artifact_spec.rb" +- "./spec/models/ci/job_token/scope_spec.rb" +- "./spec/models/ci/pipeline_spec.rb" +- "./spec/models/ci/runner_spec.rb" +- "./spec/models/clusters/applications/runner_spec.rb" +- "./spec/models/deployment_spec.rb" +- "./spec/models/environment_spec.rb" +- "./spec/models/merge_request_spec.rb" +- "./spec/models/project_spec.rb" +- "./spec/models/user_spec.rb" +- "./spec/presenters/ci/build_runner_presenter_spec.rb" +- "./spec/presenters/ci/pipeline_presenter_spec.rb" +- "./spec/presenters/packages/detail/package_presenter_spec.rb" +- "./spec/requests/api/ci/pipelines_spec.rb" +- "./spec/requests/api/ci/runner/jobs_request_post_spec.rb" +- "./spec/requests/api/ci/runner/runners_post_spec.rb" +- "./spec/requests/api/ci/runners_spec.rb" +- "./spec/requests/api/commit_statuses_spec.rb" +- "./spec/requests/api/graphql/group_query_spec.rb" +- "./spec/requests/api/graphql/merge_request/merge_request_spec.rb" +- "./spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb" +- "./spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb" +- "./spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb" +- "./spec/requests/api/graphql/mutations/merge_requests/create_spec.rb" +- "./spec/requests/api/graphql/packages/composer_spec.rb" +- "./spec/requests/api/graphql/packages/conan_spec.rb" +- "./spec/requests/api/graphql/packages/maven_spec.rb" +- "./spec/requests/api/graphql/packages/nuget_spec.rb" +- "./spec/requests/api/graphql/packages/package_spec.rb" +- "./spec/requests/api/graphql/packages/pypi_spec.rb" +- "./spec/requests/api/graphql/project/merge_request/pipelines_spec.rb" +- "./spec/requests/api/graphql/project/merge_request_spec.rb" +- "./spec/requests/api/graphql/project/merge_requests_spec.rb" +- "./spec/requests/api/graphql/project/pipeline_spec.rb" +- "./spec/requests/api/merge_requests_spec.rb" +- "./spec/requests/api/package_files_spec.rb" +- "./spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb" +- "./spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb" +- "./spec/services/ci/create_pipeline_service/needs_spec.rb" +- "./spec/services/ci/create_pipeline_service_spec.rb" +- "./spec/services/ci/destroy_pipeline_service_spec.rb" +- "./spec/services/ci/expire_pipeline_cache_service_spec.rb" +- "./spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb" +- "./spec/services/ci/job_artifacts/destroy_associations_service_spec.rb" +- "./spec/services/ci/job_artifacts/destroy_batch_service_spec.rb" +- "./spec/services/ci/pipeline_processing/shared_processing_service.rb" +- "./spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb" +- "./spec/services/ci/register_job_service_spec.rb" +- "./spec/services/clusters/applications/prometheus_config_service_spec.rb" +- "./spec/services/deployments/older_deployments_drop_service_spec.rb" +- "./spec/services/environments/auto_stop_service_spec.rb" +- "./spec/services/environments/stop_service_spec.rb" +- "./spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb" +- "./spec/services/merge_requests/create_service_spec.rb" +- "./spec/services/merge_requests/post_merge_service_spec.rb" +- "./spec/services/merge_requests/refresh_service_spec.rb" +- "./spec/support/prometheus/additional_metrics_shared_examples.rb" +- "./spec/support/shared_examples/ci/pipeline_email_shared_examples.rb" +- "./spec/support/shared_examples/features/packages_shared_examples.rb" +- "./spec/support/shared_examples/features/search_settings_shared_examples.rb" +- "./spec/support/shared_examples/features/variable_list_shared_examples.rb" +- "./spec/support/shared_examples/models/concerns/limitable_shared_examples.rb" +- "./spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb" +- "./spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb" +- "./spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb" +- "./spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb" +- "./spec/support/shared_examples/requests/api/status_shared_examples.rb" +- "./spec/support/shared_examples/requests/graphql_shared_examples.rb" +- "./spec/support/shared_examples/services/onboarding_progress_shared_examples.rb" +- "./spec/support/shared_examples/services/packages_shared_examples.rb" +- "./spec/support/shared_examples/workers/idempotency_shared_examples.rb" +- "./spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb" +- "./spec/workers/pipeline_process_worker_spec.rb" +- "./spec/workers/pipeline_schedule_worker_spec.rb" diff --git a/spec/support/database/gitlab_schema.rb b/spec/support/database/gitlab_schema.rb new file mode 100644 index 00000000000..fe05fb998e6 --- /dev/null +++ b/spec/support/database/gitlab_schema.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# This module gathes information about table to schema mapping +# to understand table affinity +module Database + module GitlabSchema + def self.table_schemas(tables) + tables.map { |table| table_schema(table) }.to_set + end + + def self.table_schema(name) + tables_to_schema[name] || :undefined + end + + def self.tables_to_schema + @tables_to_schema ||= all_classes_with_schema.to_h do |klass| + [klass.table_name, klass.gitlab_schema] + end + end + + def self.all_classes_with_schema + ActiveRecord::Base.descendants.reject(&:abstract_class?).select(&:gitlab_schema?) # rubocop:disable Database/MultipleDatabases + end + end +end diff --git a/spec/support/database/multiple_databases.rb b/spec/support/database/multiple_databases.rb new file mode 100644 index 00000000000..8ce642a682c --- /dev/null +++ b/spec/support/database/multiple_databases.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Database + module MultipleDatabases + def skip_if_multiple_databases_not_setup + skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci) + end + end +end diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb index 460ee99391b..b4c968e3c41 100644 --- a/spec/support/database/prevent_cross_database_modification.rb +++ b/spec/support/database/prevent_cross_database_modification.rb @@ -74,18 +74,20 @@ module Database return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?) - tables = PgQuery.parse(sql).dml_tables + parsed_query = PgQuery.parse(sql) + tables = sql.downcase.include?(' for update') ? parsed_query.tables : parsed_query.dml_tables return if tables.empty? cross_database_context[:modified_tables_by_db][database].merge(tables) all_tables = cross_database_context[:modified_tables_by_db].values.map(&:to_a).flatten + schemas = Database::GitlabSchema.table_schemas(all_tables) - unless PreventCrossJoins.only_ci_or_only_main?(all_tables) + if schemas.many? raise Database::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError, - "Cross-database data modification queries (CI and Main) were detected within " \ - "a transaction '#{all_tables.join(", ")}' discovered" + "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ + "a transaction modifying the '#{all_tables.to_a.join(", ")}'" end end end diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb index 789721ccd38..4b78aa9014c 100644 --- a/spec/support/database/prevent_cross_joins.rb +++ b/spec/support/database/prevent_cross_joins.rb @@ -11,7 +11,7 @@ # # class User # def ci_owned_runners -# ::Gitlab::Database.allow_cross_joins_across_databases!(url: link-to-issue-url) +# ::Gitlab::Database.allow_cross_joins_across_databases(url: link-to-issue-url) # # ... # end @@ -21,33 +21,43 @@ module Database module PreventCrossJoins CrossJoinAcrossUnsupportedTablesError = Class.new(StandardError) + ALLOW_THREAD_KEY = :allow_cross_joins_across_databases + def self.validate_cross_joins!(sql) - return if Thread.current[:allow_cross_joins_across_databases] + return if Thread.current[ALLOW_THREAD_KEY] + + # Allow spec/support/database_cleaner.rb queries to disable/enable triggers for many tables + # See https://gitlab.com/gitlab-org/gitlab/-/issues/339396 + return if sql.include?("DISABLE TRIGGER") || sql.include?("ENABLE TRIGGER") # PgQuery might fail in some cases due to limited nesting: # https://github.com/pganalyze/pg_query/issues/209 - tables = PgQuery.parse(sql).tables + # + # Also, we disable GC while parsing because of https://github.com/pganalyze/pg_query/issues/226 + begin + GC.disable + tables = PgQuery.parse(sql).tables + ensure + GC.enable + end - unless only_ci_or_only_main?(tables) + schemas = Database::GitlabSchema.table_schemas(tables) + + if schemas.include?(:gitlab_ci) && schemas.include?(:gitlab_main) + Thread.current[:has_cross_join_exception] = true raise CrossJoinAcrossUnsupportedTablesError, - "Unsupported cross-join across '#{tables.join(", ")}' discovered " \ - "when executing query '#{sql}'" + "Unsupported cross-join across '#{tables.join(", ")}' modifying '#{schemas.to_a.join(", ")}' discovered " \ + "when executing query '#{sql}'. Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-joins-between-ci_-and-non-ci_-tables for details on how to resolve this exception." end end - # Returns true if a set includes only CI tables, or includes only non-CI tables - def self.only_ci_or_only_main?(tables) - tables.all? { |table| CiTables.include?(table) } || - tables.none? { |table| CiTables.include?(table) } - end - module SpecHelpers def with_cross_joins_prevented subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event| ::Database::PreventCrossJoins.validate_cross_joins!(event.payload[:sql]) end - Thread.current[:allow_cross_joins_across_databases] = false + Thread.current[ALLOW_THREAD_KEY] = false yield ensure @@ -57,8 +67,12 @@ module Database module GitlabDatabaseMixin def allow_cross_joins_across_databases(url:) - Thread.current[:allow_cross_joins_across_databases] = true - super + old_value = Thread.current[ALLOW_THREAD_KEY] + Thread.current[ALLOW_THREAD_KEY] = true + + yield + ensure + Thread.current[ALLOW_THREAD_KEY] = old_value end end end @@ -67,11 +81,18 @@ end Gitlab::Database.singleton_class.prepend( Database::PreventCrossJoins::GitlabDatabaseMixin) +ALLOW_LIST = Set.new(YAML.load_file(File.join(__dir__, 'cross-join-allowlist.yml'))).freeze + RSpec.configure do |config| config.include(::Database::PreventCrossJoins::SpecHelpers) - # TODO: remove `:prevent_cross_joins` to enable the check by default - config.around(:each, :prevent_cross_joins) do |example| - with_cross_joins_prevented { example.run } + config.around do |example| + Thread.current[:has_cross_join_exception] = false + + if ALLOW_LIST.include?(example.file_path) + example.run + else + with_cross_joins_prevented { example.run } + end end end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 01bf390d9e9..b31881e3082 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -14,7 +14,7 @@ RSpec.configure do |config| end config.append_after(:context, :migration) do - delete_from_all_tables! + delete_from_all_tables!(except: ['work_item_types']) # Postgres maximum number of columns in a table is 1600 (https://github.com/postgres/postgres/blob/de41869b64d57160f58852eab20a27f248188135/src/include/access/htup_details.h#L23-L47). # And since: @@ -61,7 +61,7 @@ RSpec.configure do |config| example.run - delete_from_all_tables! + delete_from_all_tables!(except: ['work_item_types']) self.class.use_transactional_tests = true end diff --git a/spec/support/database_load_balancing.rb b/spec/support/database_load_balancing.rb index 03fa7886295..f22c69ea613 100644 --- a/spec/support/database_load_balancing.rb +++ b/spec/support/database_load_balancing.rb @@ -4,7 +4,10 @@ RSpec.configure do |config| config.before(:each, :db_load_balancing) do allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true) - proxy = ::Gitlab::Database::LoadBalancing::ConnectionProxy.new([Gitlab::Database.main.config['host']]) + config = Gitlab::Database::LoadBalancing::Configuration + .new(ActiveRecord::Base, [Gitlab::Database.main.config['host']]) + lb = ::Gitlab::Database::LoadBalancing::LoadBalancer.new(config) + proxy = ::Gitlab::Database::LoadBalancing::ConnectionProxy.new(lb) allow(ActiveRecord::Base).to receive(:load_balancing_proxy).and_return(proxy) diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 155dc3c17d9..940ff2751d3 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -12,7 +12,7 @@ module DbCleaner end def deletion_except_tables - [] + ['work_item_types'] end def setup_database_cleaner diff --git a/spec/support/helpers/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb index 98fa13db6c2..e29e12a15f6 100644 --- a/spec/support/helpers/bare_repo_operations.rb +++ b/spec/support/helpers/bare_repo_operations.rb @@ -17,26 +17,6 @@ class BareRepoOperations commit_id[0] end - # Based on https://stackoverflow.com/a/25556917/1856239 - def commit_file(file, dst_path, branch = 'master') - head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || Gitlab::Git::EMPTY_TREE_ID - - execute(['read-tree', '--empty']) - execute(['read-tree', head_id]) - - blob_id = execute(['hash-object', '--stdin', '-w']) do |stdin| - stdin.write(file.read) - end - - execute(['update-index', '--add', '--cacheinfo', '100644', blob_id[0], dst_path]) - - tree_id = execute(['write-tree']) - - commit_id = commit_tree(tree_id[0], "Add #{dst_path}", parent: head_id) - - execute(['update-ref', "refs/heads/#{branch}", commit_id]) - end - private def execute(args, allow_failure: false) diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index e48c8125d84..3ec52f8c832 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -23,12 +23,39 @@ module CycleAnalyticsHelpers end end + def select_event_label(sel) + page.within(sel) do + find('.dropdown-toggle').click + page.find(".dropdown-menu").all(".dropdown-item")[1].click + end + end + + def fill_in_custom_label_stage_fields + index = page.all('[data-testid="value-stream-stage-fields"]').length + last_stage = page.all('[data-testid="value-stream-stage-fields"]').last + + within last_stage do + find('[name*="custom-stage-name-"]').fill_in with: "Cool custom label stage - name #{index}" + select_dropdown_option_by_value "custom-stage-start-event-", :issue_label_added + select_dropdown_option_by_value "custom-stage-end-event-", :issue_label_removed + + select_event_label("[data-testid*='custom-stage-start-event-label-']") + select_event_label("[data-testid*='custom-stage-end-event-label-']") + end + end + def add_custom_stage_to_form page.find_button(s_('CreateValueStreamForm|Add another stage')).click fill_in_custom_stage_fields end + def add_custom_label_stage_to_form + page.find_button(s_('CreateValueStreamForm|Add another stage')).click + + fill_in_custom_label_stage_fields + end + def save_value_stream(custom_value_stream_name) fill_in 'create-value-stream-name', with: custom_value_stream_name @@ -44,12 +71,12 @@ module CycleAnalyticsHelpers save_value_stream(custom_value_stream_name) end - def wait_for_stages_to_load(selector = '.js-path-navigation') + def wait_for_stages_to_load(selector = '[data-testid="vsa-path-navigation"]') expect(page).to have_selector selector wait_for_requests end - def select_group(target_group, ready_selector = '.js-path-navigation') + def select_group(target_group, ready_selector = '[data-testid="vsa-path-navigation"]') visit group_analytics_cycle_analytics_path(target_group) wait_for_stages_to_load(ready_selector) diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb index 6df33e68629..d0f6fd466d0 100644 --- a/spec/support/helpers/email_helpers.rb +++ b/spec/support/helpers/email_helpers.rb @@ -2,7 +2,7 @@ module EmailHelpers def sent_to_user(user, recipients: email_recipients) - recipients.count { |to| to == user.notification_email } + recipients.count { |to| to == user.notification_email_or_default } end def reset_delivered_emails! @@ -45,7 +45,7 @@ module EmailHelpers end def find_email_for(user) - ActionMailer::Base.deliveries.find { |d| d.to.include?(user.notification_email) } + ActionMailer::Base.deliveries.find { |d| d.to.include?(user.notification_email_or_default) } end def have_referable_subject(referable, include_project: true, reply: false) diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_helpers.rb index 2e86e014a1b..2e86e014a1b 100644 --- a/spec/support/helpers/features/members_table_helpers.rb +++ b/spec/support/helpers/features/members_helpers.rb diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 4c90b907d2d..5174c145a93 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -42,9 +42,12 @@ module JavaScriptFixturesHelpers # Public: Reads a GraphQL query from the filesystem as a string # - # query_path - file path to the GraphQL query, relative to `app/assets/javascripts` - def get_graphql_query_as_string(query_path) - path = Rails.root / 'app/assets/javascripts' / query_path + # query_path - file path to the GraphQL query, relative to `app/assets/javascripts`. + # ee - boolean, when true `query_path` will be looked up in `/ee`. + def get_graphql_query_as_string(query_path, ee: false) + base = (ee ? 'ee/' : '') + 'app/assets/javascripts' + + path = Rails.root / base / query_path queries = Gitlab::Graphql::Queries.find(path) if queries.length == 1 queries.first.text(mode: Gitlab.ee? ? :ee : :ce ) diff --git a/spec/support/helpers/live_debugger.rb b/spec/support/helpers/live_debugger.rb index f4199d518a3..d196a6dc746 100644 --- a/spec/support/helpers/live_debugger.rb +++ b/spec/support/helpers/live_debugger.rb @@ -16,7 +16,7 @@ module LiveDebugger puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user puts "Press any key to resume the execution of the example!!" - `open #{current_url}` if is_headless_disabled? + `open #{current_url}` unless is_headless_disabled? loop until $stdin.getch diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index ef212938af5..7799e49d4c1 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -30,7 +30,7 @@ module MigrationsHelpers end end - klass.tap { Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions } + klass.tap { Gitlab::Database::Partitioning.sync_partitions([klass]) } end def migrations_paths diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index a6a7948d9d9..b9796ebbe62 100644 --- a/spec/support/helpers/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb @@ -5,9 +5,10 @@ module ReferenceParserHelpers Nokogiri::HTML.fragment('<a></a>').children[0] end - def expect_gathered_references(result, visible, not_visible_count) + def expect_gathered_references(result, visible, nodes, visible_nodes) expect(result[:visible]).to eq(visible) - expect(result[:not_visible].count).to eq(not_visible_count) + expect(result[:nodes]).to eq(nodes) + expect(result[:visible_nodes]).to eq(visible_nodes) end RSpec.shared_examples 'no project N+1 queries' do diff --git a/spec/support/helpers/session_helpers.rb b/spec/support/helpers/session_helpers.rb new file mode 100644 index 00000000000..4ef099a393e --- /dev/null +++ b/spec/support/helpers/session_helpers.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module SessionHelpers + def expect_single_session_with_authenticated_ttl + expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60) + end + + def expect_single_session_with_short_ttl + expect_single_session_with_expiration(Settings.gitlab['unauthenticated_session_expire_delay']) + end + + def expect_single_session_with_expiration(expiration) + session_keys = get_session_keys + + expect(session_keys.size).to eq(1) + expect(get_ttl(session_keys.first)).to be_within(5).of(expiration) + end + + def get_session_keys + Gitlab::Redis::SharedState.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a } + end + + def get_ttl(key) + Gitlab::Redis::SharedState.with { |redis| redis.ttl(key) } + end +end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index 3824ff2b68d..5ab778c11cb 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -18,6 +18,10 @@ module StubGitlabCalls stub_ci_pipeline_yaml_file(gitlab_ci_yaml) end + def gitlab_ci_yaml + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + def stub_ci_pipeline_yaml_file(ci_yaml_content) allow_any_instance_of(Repository) .to receive(:gitlab_ci_yml_for) diff --git a/spec/support/helpers/stub_gitlab_data.rb b/spec/support/helpers/stub_gitlab_data.rb deleted file mode 100644 index ed518393c03..00000000000 --- a/spec/support/helpers/stub_gitlab_data.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module StubGitlabData - def gitlab_ci_yaml - File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - end -end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index aa5fcf222f2..badd4e8212c 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -452,6 +452,10 @@ module TestEnv example_group end + def seed_db + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import + end + private # These are directories that should be preserved at cleanup time diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb index 8a88f0335a9..2fbc01a9195 100644 --- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb @@ -32,7 +32,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos expect(user).to be_blocked end - it 'migrates all associated fields to te "Ghost user"' do + it 'migrates all associated fields to the "Ghost user"' do service.execute migrated_record = record_class.find_by_id(record.id) @@ -46,40 +46,19 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos context "when #{record_class_name} migration fails and is rolled back" do before do expect_any_instance_of(ActiveRecord::Associations::CollectionProxy) - .to receive(:update_all).and_raise(ActiveRecord::Rollback) + .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout) end it 'rolls back the user block' do - service.execute + expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) expect(user.reload).not_to be_blocked end - it "doesn't unblock an previously-blocked user" do + it "doesn't unblock a previously-blocked user" do user.block - service.execute - - expect(user.reload).to be_blocked - end - end - - context "when #{record_class_name} migration fails with a non-rollback exception" do - before do - expect_any_instance_of(ActiveRecord::Associations::CollectionProxy) - .to receive(:update_all).and_raise(ArgumentError) - end - - it 'rolls back the user block' do - service.execute rescue nil - - expect(user.reload).not_to be_blocked - end - - it "doesn't unblock an previously-blocked user" do - user.block - - service.execute rescue nil + expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) expect(user.reload).to be_blocked end diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb index 14c6c85cc43..0dc66eeb2ee 100644 --- a/spec/support/shared_contexts/email_shared_context.rb +++ b/spec/support/shared_contexts/email_shared_context.rb @@ -18,6 +18,15 @@ RSpec.shared_context :email_shared_context do end end +def email_fixture(path) + fixture_file(path).gsub('project_id', project.project_id.to_s) +end + +def service_desk_fixture(path, slug: nil, key: 'mykey') + slug ||= project.full_path_slug.to_s + fixture_file(path).gsub('project_slug', slug).gsub('project_key', key) +end + RSpec.shared_examples :reply_processing_shared_examples do context 'when the user could not be found' do before do diff --git a/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb b/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb new file mode 100644 index 00000000000..a2cb9d41f45 --- /dev/null +++ b/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_context 'last_of_each_version setup context' do + let_it_be(:package1) { create(:npm_package, name: 'test', version: '1.2.3', project: project) } + let_it_be(:package2) { create(:npm_package, name: 'test2', version: '1.2.3', project: project) } + + let(:package_name) { 'test' } + let(:version) { '1.2.3' } + + before do + # create a duplicated package without triggering model validation errors + package2.update_column(:name, 'test') + end +end diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb new file mode 100644 index 00000000000..aa857cfdb70 --- /dev/null +++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_context 'runners resolver setup' do + let_it_be(:user) { create_default(:user, :admin) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:subgroup) { create(:group, :public, parent: group) } + let_it_be(:project) { create(:project, :public, group: group) } + + let_it_be(:inactive_project_runner) do + create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner)) + end + + let_it_be(:offline_project_runner) do + create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner)) + end + + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) } + let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) } + let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } +end diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb index 2c56411ca4c..b9cde12c537 100644 --- a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb +++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb @@ -16,7 +16,7 @@ RSpec.shared_context 'merge request show action' do assign(:merge_request, merge_request) assign(:note, note) assign(:noteable, merge_request) - assign(:pipelines, []) + assign(:number_of_pipelines, 0) assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, merge_request)) preload_view_requirements(merge_request, note) diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 8ae0885056e..2abc52fce85 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -118,7 +118,8 @@ RSpec.shared_context 'project navbar structure' do _('Access Tokens'), _('Repository'), _('CI/CD'), - _('Monitor') + _('Monitor'), + (s_('UsageQuota|Usage Quotas') if Feature.enabled?(:project_storage_ui, default_enabled: :yaml)) ] } ].compact diff --git a/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb b/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb deleted file mode 100644 index 4cec5ab3b74..00000000000 --- a/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -# the idea of creating zip archive with spoofed size is borrowed from -# https://github.com/rubyzip/rubyzip/pull/403/files#diff-118213fb4baa6404a40f89e1147661ebR88 -RSpec.shared_context 'pages zip with spoofed size' do - let(:real_zip_path) { Tempfile.new(['real', '.zip']).path } - let(:fake_zip_path) { Tempfile.new(['fake', '.zip']).path } - - before do - full_file_name = 'public/index.html' - true_size = 500_000 - fake_size = 1 - - ::Zip::File.open(real_zip_path, ::Zip::File::CREATE) do |zf| - zf.get_output_stream(full_file_name) do |os| - os.write 'a' * true_size - end - end - - compressed_size = nil - ::Zip::File.open(real_zip_path) do |zf| - a_entry = zf.find_entry(full_file_name) - compressed_size = a_entry.compressed_size - end - - true_size_bytes = [compressed_size, true_size, full_file_name.size].pack('LLS') - fake_size_bytes = [compressed_size, fake_size, full_file_name.size].pack('LLS') - - data = File.binread(real_zip_path) - data.gsub! true_size_bytes, fake_size_bytes - - File.open(fake_zip_path, 'wb') do |file| - file.write data - end - end - - after do - File.delete(real_zip_path) if File.exist?(real_zip_path) - File.delete(fake_zip_path) if File.exist?(fake_zip_path) - end -end 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 815108be447..89f290d8d68 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 @@ -8,14 +8,20 @@ RSpec.shared_context 'npm api setup' do let_it_be(:group) { 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") } + let_it_be(:package1, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.4') } + let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.3') } let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } + let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let(:package_name) { package.name } + + before do + # create a duplicated package without triggering model validation errors + package1.update_column(:version, '1.2.3') + end end RSpec.shared_context 'set package name from package name type' do diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb index 6b49a415889..2b810e790f0 100644 --- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb +++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb @@ -6,21 +6,25 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do let(:metrics_definitions) { standard_metrics + subscription_metrics + operational_metrics + optional_metrics } let(:standard_metrics) do [ - metric_attributes('uuid', "standard") + metric_attributes('uuid', 'standard'), + metric_attributes('recorded_at', 'standard'), + metric_attributes('settings.collected_data_categories', 'standard', 'object') ] end let(:operational_metrics) do [ - metric_attributes('counts.merge_requests', "operational"), + metric_attributes('counts.merge_requests', 'operational'), metric_attributes('counts.todos', "operational") ] end let(:optional_metrics) do [ - metric_attributes('counts.boards', "optional"), - metric_attributes('gitaly.filesystems', '').except('data_category') + metric_attributes('counts.boards', 'optional', 'number'), + metric_attributes('gitaly.filesystems', '').except('data_category'), + metric_attributes('usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram', 'optional', 'object'), + metric_attributes('topology', 'optional', 'object') ] end @@ -34,10 +38,11 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do ) end - def metric_attributes(key_path, category) + def metric_attributes(key_path, category, value_type = 'string') { 'key_path' => key_path, - 'data_category' => category + 'data_category' => category, + 'value_type' => value_type } end end diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index cadc753513d..1e303197990 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -3,14 +3,10 @@ RSpec.shared_examples 'multiple issue boards' do context 'authorized user' do before do - stub_feature_flags(board_new_list: false) - parent.add_maintainer(user) login_as(user) - stub_feature_flags(board_new_list: false) - visit boards_path wait_for_requests end @@ -79,13 +75,13 @@ RSpec.shared_examples 'multiple issue boards' do expect(page).to have_content(board2.name) end - click_button 'Add list' + click_button 'Create list' - wait_for_requests + click_button 'Select a label' - page.within '.dropdown-menu-issues-board-new' do - click_link planning.title - end + page.choose(planning.title) + + click_button 'Add to board' wait_for_requests diff --git a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb new file mode 100644 index 00000000000..748a3acf17b --- /dev/null +++ b/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.shared_examples IntegrationsActions do + let(:integration) do + create(:datadog_integration, + integration_attributes.merge( + api_url: 'http://example.com', + api_key: 'secret' + ) + ) + end + + describe 'GET #edit' do + before do + get :edit, params: routing_params + end + + it 'assigns the integration' do + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:integration)).to eq(integration) + end + end + + describe 'PUT #update' do + let(:params) do + { + datadog_env: 'env', + datadog_service: 'service' + } + end + + before do + put :update, params: routing_params.merge(integration: params) + end + + it 'updates the integration with the provided params and redirects to the form' do + expect(response).to redirect_to(routing_params.merge(action: :edit)) + expect(integration.reload).to have_attributes(params) + end + + context 'when sending a password field' do + let(:params) { super().merge(api_key: 'new') } + + it 'updates the integration with the password and other params' do + expect(response).to be_redirect + expect(integration.reload).to have_attributes(params) + end + end + + context 'when sending a blank password field' do + let(:params) { super().merge(api_key: '') } + + it 'ignores the password field and saves the other params' do + expect(response).to be_redirect + expect(integration.reload).to have_attributes(params.merge(api_key: 'secret')) + end + end + end +end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index a9c6da7bc2b..0ffa32dec9e 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -82,16 +82,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id) end - it "does not show already added project" do - project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') - stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum) - - get :status, format: :json - - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos")).to eq([]) - end - it "touches the etag cache store" do stub_client(repos: [], orgs: [], each_page: []) diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index b9ae0e23e26..44baadaaade 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -19,14 +19,4 @@ RSpec.shared_examples 'import controller status' do expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) end - - it "does not show already added project" do - project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos")).to eq([]) - end end diff --git a/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb b/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb new file mode 100644 index 00000000000..e77acb93798 --- /dev/null +++ b/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable list with anonymous search disabled' do |action| + let(:controller_action) { :index } + let(:params_with_search) { params.merge(search: 'some search term') } + + context 'when disable_anonymous_search is enabled' do + before do + stub_feature_flags(disable_anonymous_search: true) + end + + it 'shows a flash message' do + get controller_action, params: params_with_search + + expect(flash.now[:notice]).to eq('You must sign in to search for specific terms.') + end + + context 'when search param is not given' do + it 'does not show a flash message' do + get controller_action, params: params + + expect(flash.now[:notice]).to be_nil + end + end + + context 'when user is signed-in' do + it 'does not show a flash message' do + sign_in(create(:user)) + get controller_action, params: params_with_search + + expect(flash.now[:notice]).to be_nil + end + end + + context 'when format is not HTML' do + it 'does not show a flash message' do + get controller_action, params: params_with_search.merge(format: :atom) + + expect(flash.now[:notice]).to be_nil + end + end + end + + context 'when disable_anonymous_search is disabled' do + before do + stub_feature_flags(disable_anonymous_search: false) + end + + it 'does not show a flash message' do + get controller_action, params: params_with_search + + expect(flash.now[:notice]).to be_nil + end + end +end diff --git a/spec/support/shared_examples/features/atom/issuable_shared_examples.rb b/spec/support/shared_examples/features/atom/issuable_shared_examples.rb new file mode 100644 index 00000000000..17993830f37 --- /dev/null +++ b/spec/support/shared_examples/features/atom/issuable_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an authenticated issuable atom feed" do + it "renders atom feed with common issuable information" do + expect(response_headers['Content-Type']) + .to have_content('application/atom+xml') + expect(body).to have_selector('author email', text: issuable.author_public_email) + expect(body).to have_selector('assignees assignee email', text: issuable.assignees.first.public_email) + expect(body).to have_selector('assignee email', text: issuable.assignees.first.public_email) + expect(body).to have_selector('entry summary', text: issuable.title) + end +end diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb new file mode 100644 index 00000000000..2332285540a --- /dev/null +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'edits content using the content editor' do + it 'formats text as bold using bubble menu' do + content_editor_testid = '[data-testid="content-editor"] [contenteditable]' + + expect(page).to have_css(content_editor_testid) + + find(content_editor_testid).send_keys 'Typing text in the content editor' + find(content_editor_testid).send_keys [:shift, :left] + + expect(page).to have_css('[data-testid="formatting-bubble-menu"]') + end +end diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb index fd77297a490..e70f9b52c09 100644 --- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb +++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb @@ -1,15 +1,22 @@ # frozen_string_literal: true RSpec.shared_examples 'a deploy token in settings' do - it 'view deploy tokens' do + it 'view deploy tokens', :js do + user.update!(time_display_relative: true) + + visit page_path + within('.deploy-tokens') do expect(page).to have_content(deploy_token.name) expect(page).to have_content('read_repository') expect(page).to have_content('read_registry') + expect(page).to have_content('in 4 days') end end it 'add a new deploy token' do + visit page_path + fill_in 'deploy_token_name', with: 'new_deploy_key' fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s fill_in 'deploy_token_username', with: 'deployer' @@ -24,4 +31,18 @@ RSpec.shared_examples 'a deploy token in settings' do expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']") end end + + context 'when User#time_display_relative is false', :js do + before do + user.update!(time_display_relative: false) + end + + it 'shows absolute times for expires_at' do + visit page_path + + within('.deploy-tokens') do + expect(page).to have_content(deploy_token.expires_at.strftime('%b %d')) + end + end + end end diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index fb2e422559d..318ba67b9e9 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" } let(:submit_selector) { "#{form_selector} .js-comment-submit-button" } let(:close_selector) { "#{form_selector} .btn-comment-and-close" } - let(:comments_selector) { '.timeline > .note.timeline-entry' } + let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' } let(:comment) { 'My comment' } it 'clicking "Comment" will post a comment' do @@ -187,7 +187,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle-split" } let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" } let(:close_selector) { "#{form_selector} .btn-comment-and-close" } - let(:comments_selector) { '.timeline > .note.timeline-entry' } + let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' } let(:comment) { 'My comment' } it 'clicking "Comment" will post a comment' do @@ -197,6 +197,8 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re find(submit_button_selector).click + wait_for_all_requests + expect(page).to have_content(comment) new_comment = all(comments_selector).last diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index c0cfc27ceaf..149486320ae 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'issuable invite members' do page.within '.dropdown-menu-user' do expect(page).to have_link('Invite Members') - expect(page).to have_selector('[data-track-event="click_invite_members"]') + expect(page).to have_selector('[data-track-action="click_invite_members"]') expect(page).to have_selector('[data-track-label="edit_assignee"]') end diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index 38bb87eaed2..0161899cb76 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -9,9 +9,11 @@ RSpec.shared_examples 'manage applications' do visit new_application_path expect(page).to have_content 'Add new application' + expect(find('#doorkeeper_application_expire_access_tokens')).to be_checked fill_in :doorkeeper_application_name, with: application_name fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + uncheck :doorkeeper_application_expire_access_tokens check :doorkeeper_application_scopes_read_user click_on 'Save application' @@ -22,6 +24,8 @@ RSpec.shared_examples 'manage applications' do click_on 'Edit' + expect(find('#doorkeeper_application_expire_access_tokens')).not_to be_checked + application_name_changed = "#{application_name} changed" fill_in :doorkeeper_application_name, with: application_name_changed diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index c7c2aeea358..0991de21d8d 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -25,3 +25,23 @@ RSpec.shared_examples "it has an RSS button without a feed token" do .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage end end + +RSpec.shared_examples "updates atom feed link" do |type| + it "for #{type}" do + sign_in(user) + visit path + + link = find_link('Subscribe to RSS feed') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find("link[type='application/atom+xml']", visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expected = { + 'feed_token' => [user.feed_token], + 'assignee_id' => [user.id.to_s] + } + + expect(params).to include(expected) + expect(auto_discovery_params).to include(expected) + end +end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 9587da0233e..7ced8508a31 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -136,6 +136,14 @@ RSpec.shared_examples 'User updates wiki page' do expect(find('textarea#wiki_content').value).to eq('Updated Wiki Content') end end + + context 'when using the content editor' do + before do + click_button 'Use the new editor' + end + + it_behaves_like 'edits content using the content editor' + end end context 'when the page is in a subdir', :js do diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index 61feeff57bb..96df5a5f972 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -157,7 +157,7 @@ RSpec.shared_examples 'User views a wiki page' do expect(page).to have_link('updated home', href: wiki_page_path(wiki, wiki_page, version_id: commit2, action: :diff)) end - it 'between the current and the previous version of a page' do + it 'between the current and the previous version of a page', :js do commit = wiki.commit visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) @@ -169,7 +169,7 @@ RSpec.shared_examples 'User views a wiki page' do expect_diff_links(commit) end - it 'between two old versions of a page' do + it 'between two old versions of a page', :js do wiki_page.update(message: 'latest home change', content: 'updated [another link](other-page)') # rubocop:disable Rails/SaveBang: commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) @@ -184,7 +184,7 @@ RSpec.shared_examples 'User views a wiki page' do expect_diff_links(commit) end - it 'for the oldest version of a page' do + it 'for the oldest version of a page', :js do commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb new file mode 100644 index 00000000000..6342064beb8 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +shared_examples 'deployment metrics examples' do + def create_deployment(args) + project = args[:project] + environment = project.environments.production.first || create(:environment, :production, project: project) + create(:deployment, :success, args.merge(environment: environment)) + + # this is needed for the dora_deployment_frequency_in_vsa feature flag so we have aggregated data + ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee? + end + + describe "#deploys" do + subject { stage_summary.third } + + context 'when from date is given' do + before do + travel_to(5.days.ago) { create_deployment(project: project) } + create_deployment(project: project) + end + + it "finds the number of deploys made created after the 'from date'" do + expect(subject[:value]).to eq('1') + end + + it 'returns the localized title' do + Gitlab::I18n.with_locale(:ru) do + expect(subject[:title]).to eq(n_('Deploy', 'Deploys', 1)) + end + end + end + + it "doesn't find commits from other projects" do + travel_to(5.days.from_now) do + create_deployment(project: create(:project, :repository)) + end + + expect(subject[:value]).to eq('-') + end + + context 'when `to` parameter is given' do + before do + travel_to(5.days.ago) { create_deployment(project: project) } + travel_to(5.days.from_now) { create_deployment(project: project) } + end + + it "doesn't find any record" do + options[:to] = Time.now + + expect(subject[:value]).to eq('-') + end + + it "finds records created between `from` and `to` range" do + options[:from] = 10.days.ago + options[:to] = 10.days.from_now + + expect(subject[:value]).to eq('2') + end + end + end + + describe '#deployment_frequency' do + subject { stage_summary.fourth[:value] } + + it 'includes the unit: `per day`' do + expect(stage_summary.fourth[:unit]).to eq _('per day') + end + + before do + travel_to(5.days.ago) { create_deployment(project: project) } + end + + it 'returns 0.0 when there were deploys but the frequency was too low' do + options[:from] = 30.days.ago + + # 1 deployment over 30 days + # frequency of 0.03, rounded off to 0.0 + expect(subject).to eq('0') + end + + it 'returns `-` when there were no deploys' do + options[:from] = 4.days.ago + + # 0 deployment in the last 4 days + expect(subject).to eq('-') + end + + context 'when `to` is nil' do + it 'includes range until now' do + options[:from] = 6.days.ago + options[:to] = nil + + # 1 deployment over 7 days + expect(subject).to eq('0.1') + end + end + + context 'when `to` is given' do + before do + travel_to(5.days.from_now) { create_deployment(project: project, finished_at: Time.zone.now) } + end + + it 'finds records created between `from` and `to` range' do + options[:from] = 10.days.ago + options[:to] = 10.days.from_now + + # 2 deployments over 20 days + expect(subject).to eq('0.1') + end + + context 'when `from` and `to` are within a day' do + it 'returns the number of deployments made on that day' do + freeze_time do + create_deployment(project: project, finished_at: Time.current) + options[:from] = Time.current.yesterday.beginning_of_day + options[:to] = Time.current.end_of_day + + expect(subject).to eq('0.5') + end + end + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb index 89b793d5e16..708bc71ae96 100644 --- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -39,6 +39,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) allow(fake_duplicate_job).to receive(:check!).and_return('the jid') allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) + allow(fake_duplicate_job).to receive(:update_latest_wal_location!) allow(fake_duplicate_job).to receive(:options).and_return({}) job_hash = {} @@ -63,6 +64,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) .and_return('the jid')) allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) + allow(fake_duplicate_job).to receive(:update_latest_wal_location!) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -83,6 +85,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to( receive(:check!).with(time_diff.to_i).and_return('the jid')) allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) + allow(fake_duplicate_job).to receive(:update_latest_wal_location!) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -105,6 +108,13 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:options).and_return({}) allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) + allow(fake_duplicate_job).to receive(:update_latest_wal_location!) + end + + it 'updates latest wal location' do + expect(fake_duplicate_job).to receive(:update_latest_wal_location!) + + strategy.schedule({ 'jid' => 'new jid' }) {} end it 'drops the job' do @@ -136,4 +146,46 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| end end end + + describe '#perform' do + let(:proc) { -> {} } + let(:job) { { 'jid' => 'new jid', 'wal_locations' => { 'main' => '0/1234', 'ci' => '0/1234' } } } + let(:wal_locations) do + { + main: '0/D525E3A8', + ci: 'AB/12345' + } + end + + before do + allow(fake_duplicate_job).to receive(:delete!) + allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations ) + end + + it 'updates job hash with dedup_wal_locations' do + strategy.perform(job) do + proc.call + end + + expect(job['dedup_wal_locations']).to eq(wal_locations) + end + + shared_examples 'does not update job hash' do + it 'does not update job hash with dedup_wal_locations' do + strategy.perform(job) do + proc.call + end + + expect(job).not_to include('dedup_wal_locations') + end + end + + context 'when latest_wal_location is empty' do + before do + allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( {} ) + end + + include_examples 'does not update job hash' + end + end end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index b10ebb4d2a3..e1f7a9030e2 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'a multiple recipients email' do it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email + is_expected.to deliver_to recipient.notification_email_or_default end end @@ -21,7 +21,7 @@ end RSpec.shared_examples 'an email sent to a user' do it 'is sent to user\'s global notification email address' do - expect(subject).to deliver_to(recipient.notification_email) + expect(subject).to deliver_to(recipient.notification_email_or_default) end context 'with group notification email' do @@ -227,7 +227,7 @@ RSpec.shared_examples 'a note email' do aggregate_failures do expect(sender.display_name).to eq("#{note_author.name} (@#{note_author.username})") expect(sender.address).to eq(gitlab_sender) - expect(subject).to deliver_to(recipient.notification_email) + expect(subject).to deliver_to(recipient.notification_email_or_default) end end diff --git a/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb b/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb new file mode 100644 index 00000000000..2f165ef604f --- /dev/null +++ b/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'access level validation' do |features| + features.each do |feature| + it "does not allow public access level for #{feature}" do + field = "#{feature}_access_level".to_sym + container_features.update_attribute(field, ProjectFeature::PUBLIC) + + expect(container_features.valid?).to be_falsy, "#{field} failed" + end + end +end diff --git a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb new file mode 100644 index 00000000000..ed94a71892d --- /dev/null +++ b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'sanitizable' do |factory, fields| + let(:attributes) { fields.to_h { |field| [field, input] } } + + it 'includes Sanitizable' do + expect(described_class).to include(Sanitizable) + end + + fields.each do |field| + subject do + record = build(factory, attributes) + record.valid? + + record.public_send(field) + end + + describe "##{field}" do + context 'when input includes javascript tags' do + let(:input) { 'hello<script>alert(1)</script>' } + + it 'gets sanitized' do + expect(subject).to eq('hello') + end + end + end + + describe "##{field} validation" do + context 'when input contains pre-escaped html entities' do + let_it_be(:input) { '<script>alert(1)</script>' } + + subject { build(factory, attributes) } + + it 'is not valid', :aggregate_failures do + expect(subject).not_to be_valid + expect(subject.errors.details[field].flat_map(&:values)).to include('cannot contain escaped HTML entities') + end + end + end + end +end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index c111d250d34..56c202cb228 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -300,8 +300,21 @@ RSpec.shared_examples_for "member creation" do end end end +end + +RSpec.shared_examples_for "bulk member creation" do + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + describe '#execute' do + it 'raises an error when exiting_members is not passed in the args hash' do + expect do + described_class.new(source, user, :maintainer, current_user: user).execute + end.to raise_error(ArgumentError, 'existing_members must be included in the args hash') + end + end - describe '.add_users' do + describe '.add_users', :aggregate_failures do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -310,8 +323,8 @@ RSpec.shared_examples_for "member creation" do expect(members).to be_a Array expect(members.size).to eq(2) - expect(members.first).to be_a member_type - expect(members.first).to be_persisted + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) end it 'returns an empty array' do @@ -329,5 +342,42 @@ RSpec.shared_examples_for "member creation" do expect(members.size).to eq(4) expect(members.first).to be_invite end + + context 'with de-duplication' do + it 'with the same user by id and user' do + members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer) + + expect(members).to be_a Array + expect(members.size).to eq(2) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + + it 'with the same user sent more than once' do + members = described_class.add_users(source, [user1, user1], :maintainer) + + expect(members).to be_a Array + expect(members.size).to eq(1) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + end + + context 'when a member already exists' do + before do + source.add_user(user1, :developer) + end + + it 'supports existing users as expected' do + user3 = create(:user) + + members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer) + + expect(members).to be_a Array + expect(members.size).to eq(3) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + end end end diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index 07c5f730e95..e23658d1774 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -207,7 +207,7 @@ RSpec.shared_examples 'an editable mentionable' do end RSpec.shared_examples 'mentions in description' do |mentionable_type| - shared_examples 'when storing user mentions' do + context 'when storing user mentions' do before do mentionable.store_mentions! end @@ -238,26 +238,10 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| end end end - - context 'when store_mentions_without_subtransaction is enabled' do - before do - stub_feature_flags(store_mentions_without_subtransaction: true) - end - - it_behaves_like 'when storing user mentions' - end - - context 'when store_mentions_without_subtransaction is disabled' do - before do - stub_feature_flags(store_mentions_without_subtransaction: false) - end - - it_behaves_like 'when storing user mentions' - end end RSpec.shared_examples 'mentions in notes' do |mentionable_type| - shared_examples 'when mentionable notes contain mentions' do + context 'when mentionable notes contain mentions' do let(:user) { create(:user) } let(:user2) { create(:user) } let(:group) { create(:group) } @@ -277,22 +261,6 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| expect(mentionable.referenced_groups(user)).to eq [group] end end - - context 'when store_mentions_without_subtransaction is enabled' do - before do - stub_feature_flags(store_mentions_without_subtransaction: true) - end - - it_behaves_like 'when mentionable notes contain mentions' - end - - context 'when store_mentions_without_subtransaction is disabled' do - before do - stub_feature_flags(store_mentions_without_subtransaction: false) - end - - it_behaves_like 'when mentionable notes contain mentions' - end end RSpec.shared_examples 'load mentions from DB' do |mentionable_type| diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 4d328c03641..74b1bacc560 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -31,6 +31,131 @@ RSpec.shared_examples 'namespace traversal scopes' do it { expect(subject.where_values_hash).not_to have_key(:type) } end + describe '.order_by_depth' do + subject { described_class.where(id: [group_1, nested_group_1, deep_nested_group_1]).order_by_depth(direction) } + + context 'ascending' do + let(:direction) { :asc } + + it { is_expected.to eq [deep_nested_group_1, nested_group_1, group_1] } + end + + context 'descending' do + let(:direction) { :desc } + + it { is_expected.to eq [group_1, nested_group_1, deep_nested_group_1] } + end + end + + describe '.normal_select' do + let(:query_result) { described_class.where(id: group_1).normal_select } + + subject { query_result.column_names } + + it { is_expected.to eq described_class.column_names } + end + + shared_examples '.self_and_ancestors' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors } + + it { is_expected.to contain_exactly(group_1, nested_group_1, group_2, nested_group_2) } + + context 'when include_self is false' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(include_self: false) } + + it { is_expected.to contain_exactly(group_1, group_2) } + end + + context 'when hierarchy_order is ascending' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(hierarchy_order: :asc) } + + # Recursive order per level is not defined. + it { is_expected.to contain_exactly(nested_group_1, nested_group_2, group_1, group_2) } + it { expect(subject[0, 2]).to contain_exactly(nested_group_1, nested_group_2) } + it { expect(subject[2, 2]).to contain_exactly(group_1, group_2) } + end + + context 'when hierarchy_order is descending' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(hierarchy_order: :desc) } + + # Recursive order per level is not defined. + it { is_expected.to contain_exactly(nested_group_1, nested_group_2, group_1, group_2) } + it { expect(subject[0, 2]).to contain_exactly(group_1, group_2) } + it { expect(subject[2, 2]).to contain_exactly(nested_group_1, nested_group_2) } + end + end + + describe '.self_and_ancestors' do + context "use_traversal_ids_ancestor_scopes feature flag is true" do + before do + stub_feature_flags(use_traversal_ids: true) + stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true) + end + + it_behaves_like '.self_and_ancestors' + + it 'not make recursive queries' do + expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/) + end + end + + context "use_traversal_ids_ancestor_scopes feature flag is false" do + before do + stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false) + end + + it_behaves_like '.self_and_ancestors' + + it 'make recursive queries' do + expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + + shared_examples '.self_and_ancestor_ids' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestor_ids.pluck(:id) } + + it { is_expected.to contain_exactly(group_1.id, nested_group_1.id, group_2.id, nested_group_2.id) } + + context 'when include_self is false' do + subject do + described_class + .where(id: [nested_group_1, nested_group_2]) + .self_and_ancestor_ids(include_self: false) + .pluck(:id) + end + + it { is_expected.to contain_exactly(group_1.id, group_2.id) } + end + end + + describe '.self_and_ancestor_ids' do + context "use_traversal_ids_ancestor_scopes feature flag is true" do + before do + stub_feature_flags(use_traversal_ids: true) + stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true) + end + + it_behaves_like '.self_and_ancestor_ids' + + it 'make recursive queries' do + expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/) + end + end + + context "use_traversal_ids_ancestor_scopes feature flag is false" do + before do + stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false) + end + + it_behaves_like '.self_and_ancestor_ids' + + it 'make recursive queries' do + expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + describe '.self_and_descendants' do subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants } diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 1ad38a17f9c..acbcf4f7f3d 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -36,8 +36,8 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo') expect(yaml_response['entries']).to be_a(Hash) - expect(yaml_response['entries'].keys).to contain_exactly(package.name) - expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project.id}/packages/helm" }) + expect(yaml_response['entries'].keys).to contain_exactly(package.name, package2.name) + expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project_id}/packages/helm" }) package_entry = yaml_response['entries'][package.name] @@ -45,6 +45,14 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls') expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db') expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"]) + + package_entry = yaml_response['entries'][package2.name] + + expect(package_entry.length).to eq(1) + expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls', 'description') + expect(package_entry.first['digest']).to eq('file2') + expect(package_entry.first['description']).to eq('hello from stable channel') + expect(package_entry.first['urls']).to eq(['charts/filename2.tgz']) end end end @@ -174,6 +182,13 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + + expect_next_found_instance_of(::Packages::PackageFile) do |package_file| + expect(package_file).to receive(:file).and_wrap_original do |m, *args| + expect(package_file.id).to eq(package_file2.id) + m.call(*args) + end + end end it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package' @@ -189,7 +204,7 @@ end RSpec.shared_examples 'rejects helm access with unknown project id' do context 'with an unknown project' do - let(:project) { OpenStruct.new(id: 1234567890) } + let(:project_id) { 1234567890 } context 'as anonymous' do it_behaves_like 'rejects helm packages access', :anonymous, :unauthorized 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 0390e60747f..2af7b616659 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 @@ -21,11 +21,24 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| expect(response).to match_response_schema('public_api/v4/packages/npm_package') expect(json_response['name']).to eq(package.name) expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') - ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any end expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') end + + it 'avoids N+1 database queries' do + control = ActiveRecord::QueryRecorder.new { get(url, headers: headers) } + + create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + create(:packages_dependency_link, package: package, dependency_type: dependency_type) + end + end + + # query count can slightly change between the examples so we're using a custom threshold + expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) + end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index ecde4ee8565..eb650b7a09f 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -153,3 +153,15 @@ RSpec.shared_examples 'a package tracking event' do |category, action| expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) end end + +RSpec.shared_examples 'not a package tracking event' do + before do + stub_feature_flags(collect_package_events: true) + end + + it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do + expect { subject }.not_to change { Packages::Event.count } + + expect_no_snowplow_event + 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 95817624658..2a19ff6f590 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api" +# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api", "throttle_authenticated_git_lfs", "throttle_authenticated_files_api" # * request_method # * request_args # * other_user_request_args @@ -14,7 +14,9 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do "throttle_protected_paths" => "throttle_authenticated_protected_paths_api", "throttle_authenticated_api" => "throttle_authenticated_api", "throttle_authenticated_web" => "throttle_authenticated_web", - "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api" + "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api", + "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs", + "throttle_authenticated_files_api" => "throttle_authenticated_files_api" } end @@ -165,7 +167,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths" +# * throttle_setting_prefix: "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_git_lfs" # * user # * url_that_requires_authentication # * request_method @@ -176,7 +178,8 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do let(:throttle_types) do { "throttle_protected_paths" => "throttle_authenticated_protected_paths_web", - "throttle_authenticated_web" => "throttle_authenticated_web" + "throttle_authenticated_web" => "throttle_authenticated_web", + "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs" } end @@ -385,3 +388,194 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do end end end + +# Requires let variables: +# * throttle_name: "throttle_unauthenticated_api", "throttle_unauthenticated_web" +# * throttle_setting_prefix: "throttle_unauthenticated_api", "throttle_unauthenticated" +# * url_that_does_not_require_authentication +# * url_that_is_not_matched +# * requests_per_period +# * period_in_seconds +# * period +RSpec.shared_examples 'rate-limited unauthenticated requests' do + before do + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + # the last straw + expect_rejection { get url_that_does_not_require_authentication } + end + + context 'with custom response text' do + before do + stub_application_setting(rate_limiting_response_text: 'Custom response') + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + # the last straw + expect_rejection { get url_that_does_not_require_authentication } + expect(response.body).to eq("Custom response\n") + end + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + expect_rejection { get url_that_does_not_require_authentication } + + travel_to(period.from_now) do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + expect_rejection { get url_that_does_not_require_authentication } + end + end + + it 'counts requests from different IPs separately' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + expect_next_instance_of(Rack::Attack::Request) do |instance| + expect(instance).to receive(:ip).at_least(:once).and_return('1.2.3.4') + end + + # would be over limit for the same IP + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the request is not matched by the throttle' do + it 'does not throttle the requests' do + (1 + requests_per_period).times do + get url_that_is_not_matched + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is to the api internal endpoints' do + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get '/api/v4/internal/check', params: { secret_token: Gitlab::Shell.secret_token } + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is authenticated by a runner token' do + let(:request_jobs_url) { '/api/v4/jobs/request' } + let(:runner) { create(:ci_runner) } + + it 'does not count as unauthenticated' do + (1 + requests_per_period).times do + post request_jobs_url, params: { token: runner.token } + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'when the request is to a health endpoint' do + let(:health_endpoint) { '/-/metrics' } + + it 'does not throttle the requests' do + (1 + requests_per_period).times do + get health_endpoint + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the request is to a container registry notification endpoint' do + let(:secret_token) { 'secret_token' } + let(:events) { [{ action: 'push' }] } + let(:registry_endpoint) { '/api/v4/container_registry_event/events' } + let(:registry_headers) { { 'Content-Type' => ::API::ContainerRegistryEvent::DOCKER_DISTRIBUTION_EVENTS_V1_JSON } } + + before do + allow(Gitlab.config.registry).to receive(:notification_secret) { secret_token } + + event = spy(:event) + allow(::ContainerRegistry::Event).to receive(:new).and_return(event) + allow(event).to receive(:supported?).and_return(true) + end + + it 'does not throttle the requests' do + (1 + requests_per_period).times do + post registry_endpoint, + params: { events: events }.to_json, + headers: registry_headers.merge('Authorization' => secret_token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + it 'logs RackAttack info into structured logs' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + + arguments = a_hash_including({ + message: 'Rack_Attack', + env: :throttle, + remote_ip: '127.0.0.1', + request_method: 'GET', + path: url_that_does_not_require_authentication, + matched: throttle_name + }) + + expect(Gitlab::AuthLogger).to receive(:error).with(arguments) + + get url_that_does_not_require_authentication + end + + it_behaves_like 'tracking when dry-run mode is set' do + def do_request + get url_that_does_not_require_authentication + end + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_does_not_require_authentication + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end diff --git a/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb new file mode 100644 index 00000000000..f6692646ca8 --- /dev/null +++ b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating the dependency proxy image ttl policy attributes' do |from: {}, to:| + it_behaves_like 'not creating the dependency proxy image ttl policy' + + it 'updates the dependency proxy image ttl policy' do + expect { subject } + .to change { group.dependency_proxy_image_ttl_policy.reload.enabled }.from(from[:enabled]).to(to[:enabled]) + .and change { group.dependency_proxy_image_ttl_policy.reload.ttl }.from(from[:ttl]).to(to[:ttl]) + end +end + +RSpec.shared_examples 'not creating the dependency proxy image ttl policy' do + it "doesn't create the dependency proxy image ttl policy" do + expect { subject }.not_to change { DependencyProxy::ImageTtlGroupPolicy.count } + end +end + +RSpec.shared_examples 'creating the dependency proxy image ttl policy' do + it 'creates a new package setting' do + expect { subject }.to change { DependencyProxy::ImageTtlGroupPolicy.count }.by(1) + end + + it 'saves the settings' do + subject + + expect(group.dependency_proxy_image_ttl_policy).to have_attributes( + enabled: ttl_policy[:enabled], + ttl: ttl_policy[:ttl] + ) + end + + it_behaves_like 'returning a success' +end diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index 9fced12b543..0277cce975a 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -13,6 +13,7 @@ RSpec.shared_examples 'incident issue' do it 'has incident as issue type' do expect(issue.issue_type).to eq('incident') + expect(issue.work_item_type.base_type).to eq('incident') end end @@ -41,6 +42,7 @@ RSpec.shared_examples 'not an incident issue' do it 'has not incident as issue type' do expect(issue.issue_type).not_to eq('incident') + expect(issue.work_item_type.base_type).not_to eq('incident') end it 'has not an incident label' do diff --git a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb new file mode 100644 index 00000000000..09820593cdb --- /dev/null +++ b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'dismissing user callout' do |model| + it 'creates a new user callout' do + expect { execute }.to change { model.count }.by(1) + end + + it 'returns a user callout' do + expect(execute).to be_an_instance_of(model) + end + + it 'sets the dismissed_at attribute to current time' do + freeze_time do + expect(execute).to have_attributes(dismissed_at: Time.current) + end + end + + it 'updates an existing callout dismissed_at time' do + freeze_time do + old_time = 1.day.ago + new_time = Time.current + attributes = params.merge(dismissed_at: old_time, user: user) + existing_callout = create("#{model.name.split('::').last.underscore}".to_sym, attributes) + + expect { execute }.to change { existing_callout.reload.dismissed_at }.from(old_time).to(new_time) + end + end + + it 'does not update an invalid record with dismissed_at time', :aggregate_failures do + callout = described_class.new( + container: nil, current_user: user, params: { feature_name: nil } + ).execute + + expect(callout.dismissed_at).to be_nil + expect(callout).to be_invalid + end +end diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb new file mode 100644 index 00000000000..7d652be8d05 --- /dev/null +++ b/spec/support/shared_examples/work_item_base_types_importer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'work item base types importer' do + it 'creates all base work item types' do + # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite) + WorkItem::Type.delete_all + + expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count) + end +end |