summaryrefslogtreecommitdiff
path: root/spec/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /spec/lib
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
downloadgitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'spec/lib')
-rw-r--r--spec/lib/api/ci/helpers/runner_helpers_spec.rb (renamed from spec/lib/api/helpers/runner_helpers_spec.rb)4
-rw-r--r--spec/lib/api/ci/helpers/runner_spec.rb (renamed from spec/lib/api/helpers/runner_spec.rb)4
-rw-r--r--spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb62
-rw-r--r--spec/lib/api/helpers_spec.rb36
-rw-r--r--spec/lib/backup/database_backup_error_spec.rb30
-rw-r--r--spec/lib/backup/file_backup_error_spec.rb35
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb10
-rw-r--r--spec/lib/backup/manager_spec.rb81
-rw-r--r--spec/lib/backup/repository_backup_error_spec.rb42
-rw-r--r--spec/lib/banzai/filter/references/alert_reference_filter_spec.rb29
-rw-r--r--spec/lib/banzai/filter/references/commit_reference_filter_spec.rb30
-rw-r--r--spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/references/project_reference_filter_spec.rb30
-rw-r--r--spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb38
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb46
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb19
-rw-r--r--spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb32
-rw-r--r--spec/lib/extracts_path_spec.rb16
-rw-r--r--spec/lib/feature/gitaly_spec.rb2
-rw-r--r--spec/lib/feature_spec.rb4
-rw-r--r--spec/lib/generators/gitlab/usage_metric_generator_spec.rb38
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb50
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb12
-rw-r--r--spec/lib/gitlab/auth/result_spec.rb79
-rw-r--r--spec/lib/gitlab/auth_spec.rb196
-rw-r--r--spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb51
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb11
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb66
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb3
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb201
-rw-r--r--spec/lib/gitlab/checks/matching_merge_request_spec.rb3
-rw-r--r--spec/lib/gitlab/checks/single_change_access_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb90
-rw-r--r--spec/lib/gitlab/ci/config/entry/include/rules_spec.rb98
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb71
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules_spec.rb72
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/normalizer_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb350
-rw-r--r--spec/lib/gitlab/ci/parsers/security/sast_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_key_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb224
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb113
-rw-r--r--spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb163
-rw-r--r--spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/npm_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb116
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb6
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb35
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb52
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb1
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb85
-rw-r--r--spec/lib/gitlab/database/async_indexes/index_creator_spec.rb50
-rw-r--r--spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb176
-rw-r--r--spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb17
-rw-r--r--spec/lib/gitlab/database/async_indexes_spec.rb23
-rw-r--r--spec/lib/gitlab/database/connection_spec.rb467
-rw-r--r--spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb30
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_list_spec.rb84
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_spec.rb15
-rw-r--r--spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb247
-rw-r--r--spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb9
-rw-r--r--spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb67
-rw-r--r--spec/lib/gitlab/database/load_balancing/sticking_spec.rb16
-rw-r--r--spec/lib/gitlab/database/load_balancing_spec.rb150
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb108
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb97
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb47
-rw-r--r--spec/lib/gitlab/database/migrations/observers/query_details_spec.rb11
-rw-r--r--spec/lib/gitlab/database/migrations/observers/query_log_spec.rb12
-rw-r--r--spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb8
-rw-r--r--spec/lib/gitlab/database/multi_threaded_migration_spec.rb43
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb181
-rw-r--r--spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb20
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb143
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb12
-rw-r--r--spec/lib/gitlab/database/postgres_foreign_key_spec.rb41
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb31
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb29
-rw-r--r--spec/lib/gitlab/database/schema_migrations/context_spec.rb61
-rw-r--r--spec/lib/gitlab/database/transaction/context_spec.rb144
-rw-r--r--spec/lib/gitlab/database/transaction/observer_spec.rb57
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb10
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb11
-rw-r--r--spec/lib/gitlab/database_spec.rb416
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb54
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb45
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb47
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb40
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb45
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb20
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb16
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb18
-rw-r--r--spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb2
-rw-r--r--spec/lib/gitlab/etag_caching/router/restful_spec.rb8
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb16
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb16
-rw-r--r--spec/lib/gitlab/fake_application_settings_spec.rb16
-rw-r--r--spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb88
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_stats_spec.rb30
-rw-r--r--spec/lib/gitlab/git/conflict/file_spec.rb16
-rw-r--r--spec/lib/gitlab/git/remote_mirror_spec.rb25
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb202
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb83
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb86
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb19
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb113
-rw-r--r--spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb25
-rw-r--r--spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb25
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb11
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb62
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb153
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb76
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb26
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb28
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb120
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb59
-rw-r--r--spec/lib/gitlab/github_import/logger_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/object_counter_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb146
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb29
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb23
-rw-r--r--spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/copy_field_description_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/mount_mutation_spec.rb4
-rw-r--r--spec/lib/gitlab/graphql/negatable_arguments_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/pagination/connections_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/present/field_extension_spec.rb16
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb24
-rw-r--r--spec/lib/gitlab/highlight_spec.rb7
-rw-r--r--spec/lib/gitlab/http_spec.rb62
-rw-r--r--spec/lib/gitlab/import/database_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/import/import_failure_service_spec.rb136
-rw-r--r--spec/lib/gitlab/import/logger_spec.rb39
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml6
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb35
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb31
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb8
-rw-r--r--spec/lib/gitlab/jira_import/issue_serializer_spec.rb13
-rw-r--r--spec/lib/gitlab/json_cache_spec.rb6
-rw-r--r--spec/lib/gitlab/kas_spec.rb32
-rw-r--r--spec/lib/gitlab/kubernetes/default_namespace_spec.rb30
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb23
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb23
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb14
-rw-r--r--spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb84
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb7
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb3
-rw-r--r--spec/lib/gitlab/object_hierarchy_spec.rb4
-rw-r--r--spec/lib/gitlab/pagination/keyset/order_spec.rb87
-rw-r--r--spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb2
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb6
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/setup_helper/workhorse_spec.rb24
-rw-r--r--spec/lib/gitlab/sidekiq_cluster/cli_spec.rb23
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_router_spec.rb7
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_config_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb57
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb1
-rw-r--r--spec/lib/gitlab/sql/cte_spec.rb8
-rw-r--r--spec/lib/gitlab/sql/glob_spec.rb4
-rw-r--r--spec/lib/gitlab/sql/recursive_cte_spec.rb12
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/docs/helper_spec.rb79
-rw-r--r--spec/lib/gitlab/usage/docs/renderer_spec.rb24
-rw-r--r--spec/lib/gitlab/usage/docs/value_formatter_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb51
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb68
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb72
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb23
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb44
-rw-r--r--spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb25
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb16
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/global_spec.rb1
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb1
-rw-r--r--spec/lib/gitlab/x509/tag_spec.rb20
-rw-r--r--spec/lib/peek/views/active_record_spec.rb57
-rw-r--r--spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb40
-rw-r--r--spec/lib/sidebars/groups/menus/group_information_menu_spec.rb64
-rw-r--r--spec/lib/sidebars/groups/menus/issues_menu_spec.rb54
-rw-r--r--spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb32
-rw-r--r--spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb36
-rw-r--r--spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb163
-rw-r--r--spec/lib/sidebars/groups/menus/settings_menu_spec.rb97
-rw-r--r--spec/lib/sidebars/menu_spec.rb8
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb22
248 files changed, 7812 insertions, 2611 deletions
diff --git a/spec/lib/api/helpers/runner_helpers_spec.rb b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
index 65b35845aab..c6638bea59e 100644
--- a/spec/lib/api/helpers/runner_helpers_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe API::Helpers::Runner do
+RSpec.describe API::Ci::Helpers::Runner do
let(:ip_address) { '1.2.3.4' }
let(:runner_class) do
Class.new do
include API::Helpers
- include API::Helpers::Runner
+ include API::Ci::Helpers::Runner
attr_accessor :params
diff --git a/spec/lib/api/helpers/runner_spec.rb b/spec/lib/api/ci/helpers/runner_spec.rb
index e55c20b7ab6..99f2db544a5 100644
--- a/spec/lib/api/helpers/runner_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe API::Helpers::Runner do
- let(:helper) { Class.new { include API::Helpers::Runner }.new }
+RSpec.describe API::Ci::Helpers::Runner do
+ let(:helper) { Class.new { include API::Ci::Helpers::Runner }.new }
before do
allow(helper).to receive(:env).and_return({})
diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
index 99b52236771..ae0c0f53acd 100644
--- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
let_it_be(:helper) { Class.new.include(described_class).new }
- describe 'redirect_registry_request' do
+ describe '#redirect_registry_request' do
using RSpec::Parameterized::TableSyntax
let(:options) { {} }
@@ -13,7 +13,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } }
before do
- allow(helper).to receive(:options).and_return(for: API::NpmInstancePackages)
+ allow(helper).to receive(:options).and_return(for: described_class)
end
shared_examples 'executing fallback' do
@@ -34,38 +34,66 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject
- expect_snowplow_event(category: 'API::NpmInstancePackages', action: 'npm_request_forward')
+ expect_snowplow_event(category: described_class.to_s, action: "#{package_type}_request_forward")
end
end
- context 'with npm packages' do
- let(:package_type) { :npm }
+ %i[npm pypi].each do |forwardable_package_type|
+ context "with #{forwardable_package_type} packages" do
+ include_context 'dependency proxy helpers context'
- where(:application_setting, :forward_to_registry, :example_name) do
- true | true | 'executing redirect'
- true | false | 'executing fallback'
- false | true | 'executing fallback'
- false | false | 'executing fallback'
- end
+ let(:package_type) { forwardable_package_type }
- with_them do
- before do
- stub_application_setting(npm_package_requests_forwarding: application_setting)
+ where(:application_setting, :forward_to_registry, :example_name) do
+ true | true | 'executing redirect'
+ true | false | 'executing fallback'
+ false | true | 'executing fallback'
+ false | false | 'executing fallback'
end
- it_behaves_like params[:example_name]
+ with_them do
+ before do
+ allow_fetch_application_setting(attribute: "#{forwardable_package_type}_package_requests_forwarding", return_value: application_setting)
+ end
+
+ it_behaves_like params[:example_name]
+ end
end
end
- context 'with non-forwardable packages' do
+ context 'with non-forwardable package type' do
let(:forward_to_registry) { true }
before do
stub_application_setting(npm_package_requests_forwarding: true)
+ stub_application_setting(pypi_package_requests_forwarding: true)
end
- Packages::Package.package_types.keys.without('npm').each do |pkg_type|
+ Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
context "#{pkg_type}" do
+ let(:package_type) { pkg_type.to_sym }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "Can't find application setting for package_type #{package_type}")
+ end
+ end
+ end
+ end
+
+ describe '#registry_url' do
+ subject { helper.registry_url(package_type, package_name: 'test') }
+
+ where(:package_type, :expected_result) do
+ :npm | 'https://registry.npmjs.org/test'
+ :pypi | 'https://pypi.org/simple/test/'
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_result) }
+ end
+
+ Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
+ context "with non-forwardable package type #{pkg_type}" do
let(:package_type) { pkg_type }
it 'raises an error' do
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 6e48ee4c315..587fe60860a 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -581,4 +581,40 @@ RSpec.describe API::Helpers do
end
end
end
+
+ describe '#order_by_similarity?' do
+ where(:params, :allow_unauthorized, :current_user_set, :expected) do
+ {} | false | false | false
+ {} | true | false | false
+ {} | false | true | false
+ {} | true | true | false
+ { order_by: 'similarity' } | false | false | false
+ { order_by: 'similarity' } | true | false | false
+ { order_by: 'similarity' } | true | true | false
+ { order_by: 'similarity' } | false | true | false
+ { search: 'test' } | false | false | false
+ { search: 'test' } | true | false | false
+ { search: 'test' } | true | true | false
+ { search: 'test' } | false | true | false
+ { order_by: 'similarity', search: 'test' } | false | false | false
+ { order_by: 'similarity', search: 'test' } | true | false | true
+ { order_by: 'similarity', search: 'test' } | true | true | true
+ { order_by: 'similarity', search: 'test' } | false | true | true
+ end
+
+ with_them do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ u = current_user_set ? user : nil
+ subject.instance_variable_set(:@current_user, u)
+
+ allow(subject).to receive(:params).and_return(params)
+ end
+
+ it 'returns the expected result' do
+ expect(subject.order_by_similarity?(allow_unauthorized: allow_unauthorized)).to eq(expected)
+ end
+ end
+ end
end
diff --git a/spec/lib/backup/database_backup_error_spec.rb b/spec/lib/backup/database_backup_error_spec.rb
new file mode 100644
index 00000000000..ef627900050
--- /dev/null
+++ b/spec/lib/backup/database_backup_error_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::DatabaseBackupError do
+ let(:config) do
+ {
+ host: 'localhost',
+ port: 5432,
+ database: 'gitlabhq_test'
+ }
+ end
+
+ let(:db_file_name) { File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') }
+
+ subject { described_class.new(config, db_file_name) }
+
+ it { is_expected.to respond_to :config }
+ it { is_expected.to respond_to :db_file_name }
+
+ it 'expects exception message to include database file' do
+ expect(subject.message).to include("#{db_file_name}")
+ end
+
+ it 'expects exception message to include database paths being back-up' do
+ expect(subject.message).to include("#{config[:host]}")
+ expect(subject.message).to include("#{config[:port]}")
+ expect(subject.message).to include("#{config[:database]}")
+ end
+end
diff --git a/spec/lib/backup/file_backup_error_spec.rb b/spec/lib/backup/file_backup_error_spec.rb
new file mode 100644
index 00000000000..bb174bbe4a0
--- /dev/null
+++ b/spec/lib/backup/file_backup_error_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::FileBackupError do
+ let_it_be(:lfs) { create(:lfs_object) }
+ let_it_be(:upload) { create(:upload) }
+
+ let(:backup_tarball) { '/tmp/backup/uploads' }
+
+ shared_examples 'includes backup path' do
+ it { is_expected.to respond_to :app_files_dir }
+ it { is_expected.to respond_to :backup_tarball }
+
+ it 'expects exception message to include file backup path location' do
+ expect(subject.message).to include("#{subject.backup_tarball}")
+ end
+
+ it 'expects exception message to include file being back-up' do
+ expect(subject.message).to include("#{subject.app_files_dir}")
+ end
+ end
+
+ context 'with lfs file' do
+ subject { described_class.new(lfs, backup_tarball) }
+
+ it_behaves_like 'includes backup path'
+ end
+
+ context 'with uploads file' do
+ subject { described_class.new(upload, backup_tarball) }
+
+ it_behaves_like 'includes backup path'
+ end
+end
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index cdb35c0ce01..a48a1752eff 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Backup::GitalyBackup do
project_snippet = create(:project_snippet, :repository, project: project)
personal_snippet = create(:personal_snippet, :repository, author: project.owner)
- expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, { in: anything, out: progress }).and_call_original
+ expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything).and_call_original
subject.start(:create)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
@@ -53,7 +53,7 @@ RSpec.describe Backup::GitalyBackup do
let(:parallel) { 3 }
it 'passes parallel option through' do
- expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel', '3', { in: anything, out: progress }).and_call_original
+ expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything, '-parallel', '3').and_call_original
subject.start(:create)
subject.wait
@@ -64,7 +64,7 @@ RSpec.describe Backup::GitalyBackup do
let(:parallel_storage) { 3 }
it 'passes parallel option through' do
- expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel-storage', '3', { in: anything, out: progress }).and_call_original
+ expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything, '-parallel-storage', '3').and_call_original
subject.start(:create)
subject.wait
@@ -109,7 +109,7 @@ RSpec.describe Backup::GitalyBackup do
copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle')
copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle')
- expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original
+ expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything).and_call_original
subject.start(:restore)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
@@ -132,7 +132,7 @@ RSpec.describe Backup::GitalyBackup do
let(:parallel) { 3 }
it 'does not pass parallel option through' do
- expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original
+ expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything).and_call_original
subject.start(:restore)
subject.wait
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index feaca6164eb..2cc1bf41d18 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -12,20 +12,13 @@ RSpec.describe Backup::Manager do
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
-
- @old_progress = $progress # rubocop:disable Style/GlobalVars
- $progress = progress # rubocop:disable Style/GlobalVars
- end
-
- after do
- $progress = @old_progress # rubocop:disable Style/GlobalVars
end
describe '#pack' do
- let(:backup_contents) { ['backup_contents'] }
+ let(:expected_backup_contents) { %w(repositories db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml) }
+ let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' }
let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } }
- let(:tar_cmdline) { ['tar', '-cf', '-', *backup_contents, tar_system_options] }
-
+ let(:tar_cmdline) { ['tar', '-cf', '-', *expected_backup_contents, tar_system_options] }
let(:backup_information) do
{
backup_created_at: Time.zone.parse('2019-01-01'),
@@ -36,20 +29,20 @@ RSpec.describe Backup::Manager do
before do
allow(ActiveRecord::Base.connection).to receive(:reconnect!)
allow(Kernel).to receive(:system).and_return(true)
+ allow(YAML).to receive(:load_file).and_return(backup_information)
+
+ ::Backup::Manager::FOLDERS_TO_BACKUP.each do |folder|
+ allow(Dir).to receive(:exist?).with(File.join(Gitlab.config.backup.path, folder)).and_return(true)
+ end
- allow(subject).to receive(:backup_contents).and_return(backup_contents)
allow(subject).to receive(:backup_information).and_return(backup_information)
allow(subject).to receive(:upload)
end
- context 'when BACKUP is not set' do
- let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' }
-
- it 'uses the default tar file name' do
- subject.pack
+ it 'executes tar' do
+ subject.pack
- expect(Kernel).to have_received(:system).with(*tar_cmdline)
- end
+ expect(Kernel).to have_received(:system).with(*tar_cmdline)
end
context 'when BACKUP is set' do
@@ -62,6 +55,58 @@ RSpec.describe Backup::Manager do
expect(Kernel).to have_received(:system).with(*tar_cmdline)
end
end
+
+ context 'when skipped is set in backup_information.yml' do
+ let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml} }
+ let(:backup_information) do
+ {
+ backup_created_at: Time.zone.parse('2019-01-01'),
+ gitlab_version: '12.3',
+ skipped: ['repositories']
+ }
+ end
+
+ it 'executes tar' do
+ subject.pack
+
+ expect(Kernel).to have_received(:system).with(*tar_cmdline)
+ end
+ end
+
+ context 'when a directory does not exist' do
+ let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml} }
+
+ before do
+ expect(Dir).to receive(:exist?).with(File.join(Gitlab.config.backup.path, 'repositories')).and_return(false)
+ end
+
+ it 'executes tar' do
+ subject.pack
+
+ expect(Kernel).to have_received(:system).with(*tar_cmdline)
+ end
+ end
+ end
+
+ describe '#remove_tmp' do
+ let(:path) { File.join(Gitlab.config.backup.path, 'tmp') }
+
+ before do
+ allow(FileUtils).to receive(:rm_rf).and_return(true)
+ end
+
+ it 'removes backups/tmp dir' do
+ subject.remove_tmp
+
+ expect(FileUtils).to have_received(:rm_rf).with(path)
+ end
+
+ it 'prints running task with a done confirmation' do
+ subject.remove_tmp
+
+ expect(progress).to have_received(:print).with('Deleting backups/tmp ... ')
+ expect(progress).to have_received(:puts).with('done')
+ end
end
describe '#remove_old' do
diff --git a/spec/lib/backup/repository_backup_error_spec.rb b/spec/lib/backup/repository_backup_error_spec.rb
new file mode 100644
index 00000000000..44c75c1cf77
--- /dev/null
+++ b/spec/lib/backup/repository_backup_error_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::RepositoryBackupError do
+ let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:wiki) { ProjectWiki.new(project, nil ) }
+
+ let(:backup_repos_path) { '/tmp/backup/repositories' }
+
+ shared_examples 'includes backup path' do
+ it { is_expected.to respond_to :container }
+ it { is_expected.to respond_to :backup_repos_path }
+
+ it 'expects exception message to include repo backup path location' do
+ expect(subject.message).to include("#{subject.backup_repos_path}")
+ end
+
+ it 'expects exception message to include container being back-up' do
+ expect(subject.message).to include("#{subject.container.disk_path}")
+ end
+ end
+
+ context 'with snippet repository' do
+ subject { described_class.new(snippet, backup_repos_path) }
+
+ it_behaves_like 'includes backup path'
+ end
+
+ context 'with project repository' do
+ subject { described_class.new(project, backup_repos_path) }
+
+ it_behaves_like 'includes backup path'
+ end
+
+ context 'with wiki repository' do
+ subject { described_class.new(wiki, backup_repos_path) }
+
+ it_behaves_like 'includes backup path'
+ end
+end
diff --git a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
index 7c6b0cac24b..cba41166be4 100644
--- a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
@@ -220,4 +220,33 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter do
expect(reference_filter(act, project: nil, group: group).to_html).to eq exp
end
end
+
+ context 'checking N+1' do
+ let(:namespace) { create(:namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:alert2) { create(:alert_management_alert, project: project2) }
+ let(:alert_reference) { alert.to_reference }
+ let(:alert2_reference) { alert2.to_reference(full: true) }
+
+ it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
+ markdown = "#{alert_reference}"
+ max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ reference_filter(markdown)
+ end.count
+
+ expect(max_count).to eq 1
+
+ markdown = "#{alert_reference} ^alert#2 ^alert#3 ^alert#4 #{alert2_reference}"
+
+ # Since we're not batching alert queries across projects,
+ # we have to account for that.
+ # 1 for both projects, 1 for alerts in each project == 3
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
+ max_count += 2
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
index bee8e42d12e..6bcea41a603 100644
--- a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
@@ -269,4 +269,34 @@ RSpec.describe Banzai::Filter::References::CommitReferenceFilter do
expect(reference_filter(act, context).css('a').first.text).to eql("#{project.full_path}@#{commit.short_id}")
end
end
+
+ context 'checking N+1' do
+ let(:namespace2) { create(:namespace) }
+ let(:namespace3) { create(:namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace2) }
+ let(:project3) { create(:project, :public, :repository, namespace: namespace3) }
+ let(:commit2) { project2.commit }
+ let(:commit3) { project3.commit }
+ let(:commit_reference) { commit.to_reference }
+ let(:commit2_reference) { commit2.to_reference(full: true) }
+ let(:commit3_reference) { commit3.to_reference(full: true) }
+
+ it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
+ markdown = "#{commit_reference}"
+ max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ reference_filter(markdown)
+ end.count
+
+ markdown = "#{commit_reference} 8b95f2f1 8b95f2f2 8b95f2f3 #{commit2_reference} #{commit3_reference}"
+
+ # Commits are not DB entries, they are on the project itself.
+ # So adding commits from two more projects to the markdown should
+ # only increase by 1 query
+ max_count += 1
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
index f8a00716680..cdf6110dd6c 100644
--- a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
@@ -92,6 +92,11 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
expect(doc.to_html).to match(%r(\(<a.+>#{milestone.reference_link_text}</a>\.\)))
end
+ it 'links with adjacent html tags' do
+ doc = reference_filter("Milestone <p>#{reference}</p>.")
+ expect(doc.to_html).to match(%r(<p><a.+>#{milestone.reference_link_text}</a></p>))
+ end
+
it 'ignores invalid milestone names' do
exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}"
diff --git a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
index 63a5a9184c1..d88e262883f 100644
--- a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
@@ -97,4 +97,34 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
expect(filter.send(:projects)).to eq([project.full_path])
end
end
+
+ context 'checking N+1' do
+ let_it_be(:normal_project) { create(:project, :public) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_project) { create(:project, group: group) }
+ let_it_be(:nested_group) { create(:group, :nested) }
+ let_it_be(:nested_project) { create(:project, group: nested_group) }
+ let_it_be(:normal_project_reference) { get_reference(normal_project) }
+ let_it_be(:group_project_reference) { get_reference(group_project) }
+ let_it_be(:nested_project_reference) { get_reference(nested_project) }
+
+ it 'does not have N+1 per multiple project references', :use_sql_query_cache do
+ markdown = "#{normal_project_reference}"
+
+ # warm up first
+ reference_filter(markdown)
+
+ max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ reference_filter(markdown)
+ end.count
+
+ expect(max_count).to eq 1
+
+ markdown = "#{normal_project_reference} #{invalidate_reference(normal_project_reference)} #{group_project_reference} #{nested_project_reference}"
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
index 56f36af5066..082e5c92e53 100644
--- a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
@@ -6,18 +6,42 @@ RSpec.describe Banzai::Filter::TableOfContentsTagFilter do
include FilterSpecHelper
context 'table of contents' do
- let(:html) { '<p>[[<em>TOC</em>]]</p>' }
+ shared_examples 'table of contents tag' do
+ it 'replaces toc tag with ToC result' do
+ doc = filter(html, {}, { toc: "FOO" })
- it 'replaces [[<em>TOC</em>]] with ToC result' do
- doc = filter(html, {}, { toc: "FOO" })
+ expect(doc.to_html).to eq("FOO")
+ end
- expect(doc.to_html).to eq("FOO")
+ it 'handles an empty ToC result' do
+ doc = filter(html)
+
+ expect(doc.to_html).to eq ''
+ end
+ end
+
+ context '[[_TOC_]] as tag' do
+ it_behaves_like 'table of contents tag' do
+ let(:html) { '<p>[[<em>TOC</em>]]</p>' }
+ end
end
- it 'handles an empty ToC result' do
- doc = filter(html)
+ context '[[_toc_]] as tag' do
+ it_behaves_like 'table of contents tag' do
+ let(:html) { '<p>[[<em>toc</em>]]</p>' }
+ end
+ end
+
+ context '[TOC] as tag' do
+ it_behaves_like 'table of contents tag' do
+ let(:html) { '<p>[TOC]</p>' }
+ end
+ end
- expect(doc.to_html).to eq ''
+ context '[toc] as tag' do
+ it_behaves_like 'table of contents tag' do
+ let(:html) { '<p>[toc]</p>' }
+ end
end
end
end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 989e06a992d..72661003361 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -102,33 +102,45 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
describe 'table of contents' do
let(:project) { create(:project, :public) }
- let(:markdown) do
- <<-MARKDOWN.strip_heredoc
- [[_TOC_]]
+
+ shared_examples 'table of contents tag' do |tag, tag_html|
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ #{tag}
# Header
- MARKDOWN
- end
+ MARKDOWN
+ end
- let(:invalid_markdown) do
- <<-MARKDOWN.strip_heredoc
- test [[_TOC_]]
+ let(:invalid_markdown) do
+ <<-MARKDOWN.strip_heredoc
+ test #{tag}
# Header
- MARKDOWN
- end
+ MARKDOWN
+ end
- it 'inserts a table of contents' do
- output = described_class.to_html(markdown, project: project)
+ it 'inserts a table of contents' do
+ output = described_class.to_html(markdown, project: project)
- expect(output).to include("<ul class=\"section-nav\">")
- expect(output).to include("<li><a href=\"#header\">Header</a></li>")
+ expect(output).to include("<ul class=\"section-nav\">")
+ expect(output).to include("<li><a href=\"#header\">Header</a></li>")
+ end
+
+ it 'does not insert a table of contents' do
+ output = described_class.to_html(invalid_markdown, project: project)
+
+ expect(output).to include("test #{tag_html}")
+ end
end
- it 'does not insert a table of contents' do
- output = described_class.to_html(invalid_markdown, project: project)
+ context 'with [[_TOC_]] as tag' do
+ it_behaves_like 'table of contents tag', '[[_TOC_]]', '[[<em>TOC</em>]]'
+ end
- expect(output).to include("test [[<em>TOC</em>]]")
+ context 'with [toc] as tag' do
+ it_behaves_like 'table of contents tag', '[toc]', '[toc]'
+ it_behaves_like 'table of contents tag', '[TOC]', '[TOC]'
end
end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 007d310247b..59f5e4a6900 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
end
end
- it 'is case-sensitive' do
+ it 'is not case-sensitive' do
markdown = <<-MD.strip_heredoc
[[_toc_]]
@@ -36,9 +36,22 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- output = described_class.to_html(markdown, project: project, wiki: wiki)
+ result = described_class.call(markdown, project: project, wiki: wiki)
+
+ expect(result[:output].to_html).to include(result[:toc])
+ end
+
+ it 'works with alternative [toc] tag' do
+ markdown = <<-MD.strip_heredoc
+ [toc]
- expect(output).to include('[[<em>toc</em>]]')
+ # Header 1
+
+ Foo
+ MD
+
+ result = described_class.call(markdown, project: project, wiki: wiki)
+ expect(result[:output].to_html).to include(result[:toc])
end
it 'handles an empty pipeline result' do
diff --git a/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb b/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb
new file mode 100644
index 00000000000..4f00b1ec654
--- /dev/null
+++ b/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::Collector::SentryAuthParser do
+ describe '.parse' do
+ let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=glet_1fedb514e17f4b958435093deb02048c" } }
+ let(:request) { double('request', headers: headers) }
+
+ subject { described_class.parse(request) }
+
+ context 'empty headers' do
+ let(:headers) { {} }
+
+ it 'fails with exception' do
+ expect { subject }.to raise_error(StandardError)
+ end
+ end
+
+ context 'missing sentry_key' do
+ let(:headers) { { 'X-Sentry-Auth' => "Sentry foo=bar" } }
+
+ it 'returns empty value for public_key' do
+ expect(subject[:public_key]).to be_nil
+ end
+ end
+
+ it 'returns correct value for public_key' do
+ expect(subject[:public_key]).to eq('glet_1fedb514e17f4b958435093deb02048c')
+ end
+ end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 05f3bb2f71a..9b2bb024fa6 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -213,20 +213,4 @@ RSpec.describe ExtractsPath do
expect(extract_ref_without_atom('foo.atom')).to eq(nil)
end
end
-
- describe '#lfs_blob_ids' do
- let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') }
- let(:ref) { tag.target }
- let(:params) { { ref: ref, path: 'README.md' } }
-
- before do
- @project = create(:project, :repository)
- end
-
- it 'handles annotated tags' do
- assign_ref_vars
-
- expect(lfs_blob_ids).to eq([])
- end
- end
end
diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb
index 696427bb8b6..311589c3253 100644
--- a/spec/lib/feature/gitaly_spec.rb
+++ b/spec/lib/feature/gitaly_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Feature::Gitaly do
context 'when table does not exist' do
before do
- allow(::Gitlab::Database).to receive(:cached_table_exists?).and_return(false)
+ allow(::Gitlab::Database.main).to receive(:cached_table_exists?).and_return(false)
end
it 'returns an empty Hash' do
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index dc8fd0de313..9d4820f9a4c 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -310,7 +310,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when database exists' do
before do
- allow(Gitlab::Database).to receive(:exists?).and_return(true)
+ allow(Gitlab::Database.main).to receive(:exists?).and_return(true)
end
it 'checks the persisted status and returns false' do
@@ -322,7 +322,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when database does not exist' do
before do
- allow(Gitlab::Database).to receive(:exists?).and_return(false)
+ allow(Gitlab::Database.main).to receive(:exists?).and_return(false)
end
it 'returns false without checking the status in the database' do
diff --git a/spec/lib/generators/gitlab/usage_metric_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_generator_spec.rb
index f38815acca6..207ecb88aad 100644
--- a/spec/lib/generators/gitlab/usage_metric_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_generator_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::UsageMetricGenerator, :silence_stdout do
let(:spec_ce_temp_dir) { Dir.mktmpdir }
let(:spec_ee_temp_dir) { Dir.mktmpdir }
let(:args) { ['CountFoo'] }
- let(:options) { { 'type' => 'redis_hll' } }
+ let(:options) { { 'type' => 'generic' } }
before do
stub_const("#{described_class}::CE_DIR", ce_temp_dir)
@@ -30,27 +30,39 @@ RSpec.describe Gitlab::UsageMetricGenerator, :silence_stdout do
describe 'Creating metric instrumentation files' do
let(:sample_metric_dir) { 'lib/generators/gitlab/usage_metric_generator' }
- let(:sample_metric) { fixture_file(File.join(sample_metric_dir, 'sample_metric.rb')) }
+ let(:generic_sample_metric) { fixture_file(File.join(sample_metric_dir, 'sample_generic_metric.rb')) }
+ let(:database_sample_metric) { fixture_file(File.join(sample_metric_dir, 'sample_database_metric.rb')) }
let(:sample_spec) { fixture_file(File.join(sample_metric_dir, 'sample_metric_test.rb')) }
it 'creates CE metric instrumentation files using the template' do
described_class.new(args, options).invoke_all
- expect_generated_file(ce_temp_dir, 'count_foo_metric.rb', sample_metric)
+ expect_generated_file(ce_temp_dir, 'count_foo_metric.rb', generic_sample_metric)
expect_generated_file(spec_ce_temp_dir, 'count_foo_metric_spec.rb', sample_spec)
end
context 'with EE flag true' do
- let(:options) { { 'type' => 'redis_hll', 'ee' => true } }
+ let(:options) { { 'type' => 'generic', 'ee' => true } }
it 'creates EE metric instrumentation files using the template' do
described_class.new(args, options).invoke_all
- expect_generated_file(ee_temp_dir, 'count_foo_metric.rb', sample_metric)
+ expect_generated_file(ee_temp_dir, 'count_foo_metric.rb', generic_sample_metric)
expect_generated_file(spec_ee_temp_dir, 'count_foo_metric_spec.rb', sample_spec)
end
end
+ context 'for database type' do
+ let(:options) { { 'type' => 'database', 'operation' => 'count' } }
+
+ it 'creates the metric instrumentation file using the template' do
+ described_class.new(args, options).invoke_all
+
+ expect_generated_file(ce_temp_dir, 'count_foo_metric.rb', database_sample_metric)
+ expect_generated_file(spec_ce_temp_dir, 'count_foo_metric_spec.rb', sample_spec)
+ end
+ end
+
context 'with type option missing' do
let(:options) { {} }
@@ -66,5 +78,21 @@ RSpec.describe Gitlab::UsageMetricGenerator, :silence_stdout do
expect { described_class.new(args, options).invoke_all }.to raise_error(ArgumentError, /Unknown type 'some_other_type'/)
end
end
+
+ context 'without operation for database metric' do
+ let(:options) { { 'type' => 'database' } }
+
+ it 'raises an ArgumentError' do
+ expect { described_class.new(args, options).invoke_all }.to raise_error(ArgumentError, /Unknown operation ''/)
+ end
+ end
+
+ context 'with wrong operation for database metric' do
+ let(:options) { { 'type' => 'database', 'operation' => 'sleep' } }
+
+ it 'raises an ArgumentError' do
+ expect { described_class.new(args, options).invoke_all }.to raise_error(ArgumentError, /Unknown operation 'sleep'/)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
index ebc5ae2a632..4fe55ba0c0c 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -79,56 +79,6 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
include_context 'when records are loaded by maintainer'
end
-
- describe 'special case' do
- let(:mr1) { create(:merge_request, source_project: project, allow_broken: true, created_at: 20.days.ago) }
- let(:mr2) { create(:merge_request, source_project: project, allow_broken: true, created_at: 19.days.ago) }
- let(:ci_build1) { create(:ci_build) }
- let(:ci_build2) { create(:ci_build) }
- let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages }
- let(:stage) { build(:cycle_analytics_project_stage, default_stages.params_for_test_stage.merge(project: project)) }
-
- before do
- mr1.metrics.update!({
- merged_at: 5.days.ago,
- first_deployed_to_production_at: 1.day.ago,
- latest_build_started_at: 5.days.ago,
- latest_build_finished_at: 1.day.ago,
- pipeline: ci_build1.pipeline
- })
- mr2.metrics.update!({
- merged_at: 10.days.ago,
- first_deployed_to_production_at: 5.days.ago,
- latest_build_started_at: 9.days.ago,
- latest_build_finished_at: 7.days.ago,
- pipeline: ci_build2.pipeline
- })
-
- project.add_user(user, Gitlab::Access::MAINTAINER)
- end
-
- context 'returns build records' do
- shared_examples 'orders build records by `latest_build_finished_at`' do
- it 'orders by `latest_build_finished_at`' do
- build_ids = subject.map { |item| item[:id] }
-
- expect(build_ids).to eq([ci_build1.id, ci_build2.id])
- end
- end
-
- context 'when requesting records for default test stage' do
- include_examples 'orders build records by `latest_build_finished_at`'
- end
-
- context 'when requesting records for default staging stage' do
- before do
- stage.assign_attributes(default_stages.params_for_staging_stage)
- end
-
- include_examples 'orders build records by `latest_build_finished_at`'
- end
- end
- end
end
describe 'pagination' do
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 2d4239eb761..b0522e269e0 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -496,18 +496,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_user_from_web_access_token(:archive)).to eq(user)
end
- context 'when allow_archive_as_web_access_format feature flag is disabled' do
- before do
- stub_feature_flags(allow_archive_as_web_access_format: false)
- end
-
- it 'returns nil for ARCHIVE requests' do
- set_header('SCRIPT_NAME', '/-/archive/main.zip')
-
- expect(find_user_from_web_access_token(:archive)).to be_nil
- end
- end
-
context 'for API requests' do
it 'returns the user' do
set_header('SCRIPT_NAME', '/api/endpoint')
diff --git a/spec/lib/gitlab/auth/result_spec.rb b/spec/lib/gitlab/auth/result_spec.rb
new file mode 100644
index 00000000000..f8de4b80db2
--- /dev/null
+++ b/spec/lib/gitlab/auth/result_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Result do
+ let_it_be(:actor) { create(:user) }
+
+ subject { described_class.new(actor, nil, nil, []) }
+
+ context 'when actor is User' do
+ let_it_be(:actor) { create(:user) }
+
+ it 'returns auth_user' do
+ expect(subject.auth_user).to eq(actor)
+ end
+
+ it 'does not return deploy token' do
+ expect(subject.deploy_token).to be_nil
+ end
+ end
+
+ context 'when actor is Deploy token' do
+ let_it_be(:actor) { create(:deploy_token) }
+
+ it 'returns deploy token' do
+ expect(subject.deploy_token).to eq(actor)
+ end
+
+ it 'does not return auth_user' do
+ expect(subject.auth_user).to be_nil
+ end
+ end
+
+ describe '#authentication_abilities_include?' do
+ context 'when authentication abilities are empty' do
+ it 'returns false' do
+ expect(subject.authentication_abilities_include?(:read_code)).to be_falsey
+ end
+ end
+
+ context 'when authentication abilities are not empty' do
+ subject { described_class.new(actor, nil, nil, [:push_code]) }
+
+ it 'returns false when ability is not allowed' do
+ expect(subject.authentication_abilities_include?(:read_code)).to be_falsey
+ end
+
+ it 'returns true when ability is allowed' do
+ expect(subject.authentication_abilities_include?(:push_code)).to be_truthy
+ end
+ end
+ end
+
+ describe '#can_perform_action_on_project?' do
+ let(:project) { double }
+
+ it 'returns if actor can do perform given action on given project' do
+ expect(Ability).to receive(:allowed?).with(actor, :push_code, project).and_return(true)
+ expect(subject.can_perform_action_on_project?(:push_code, project)).to be_truthy
+ end
+
+ it 'returns if actor cannot do perform given action on given project' do
+ expect(Ability).to receive(:allowed?).with(actor, :push_code, project).and_return(false)
+ expect(subject.can_perform_action_on_project?(:push_code, project)).to be_falsey
+ end
+ end
+
+ describe '#can?' do
+ it 'returns if actor can do perform given action on given project' do
+ expect(actor).to receive(:can?).with(:push_code).and_return(true)
+ expect(subject.can?(:push_code)).to be_truthy
+ end
+
+ it 'returns if actor cannot do perform given action on given project' do
+ expect(actor).to receive(:can?).with(:push_code).and_return(false)
+ expect(subject.can?(:push_code)).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 1d708b17076..cc592bb8f24 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let_it_be(:project) { create(:project) }
+ let(:auth_failure) { { actor: nil, project: nil, type: nil, authentication_abilities: nil } }
let(:gl_auth) { described_class }
describe 'constants' do
@@ -159,26 +160,26 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let(:project) { build.project }
it 'recognises user-less build' do
- expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, described_class.build_authentication_abilities))
+ expect(subject).to have_attributes(actor: nil, project: build.project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
it 'recognises user token' do
build.update(user: create(:user))
- expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, described_class.build_authentication_abilities))
+ expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
end
it 'fails with blocked user token' do
build.update(user: create(:user, :blocked))
- expect(subject).to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ expect(subject).to have_attributes(auth_failure)
end
context 'username is not gitlab-ci-token' do
let(:username) { 'another_username' }
it 'fails to authenticate' do
- expect(subject).to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ expect(subject).to have_attributes(auth_failure)
end
end
end
@@ -189,7 +190,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let(:project) { build.project }
it 'denies authentication' do
- expect(subject).to eq(Gitlab::Auth::Result.new)
+ expect(subject).to have_attributes(auth_failure)
end
end
end
@@ -199,20 +200,20 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
project.create_drone_ci_integration(active: true)
project.drone_ci_integration.update(token: 'token')
- expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, described_class.build_authentication_abilities))
+ expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to have_attributes(actor: nil, project: project, type: :ci, authentication_abilities: described_class.build_authentication_abilities)
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
include_examples 'user login operation with unique ip limit' do
let(:user) { create(:user, password: 'password') }
def operation
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -221,14 +222,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
user = create(:user)
token = Gitlab::LfsToken.new(user).token
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, described_class.read_write_project_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :lfs_token, authentication_abilities: described_class.read_write_project_authentication_abilities)
end
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
it 'does not try password auth before oauth' do
@@ -245,14 +246,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
create(:deploy_keys_project, :write_access, deploy_key: key, project: project)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_write_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_write_authentication_abilities)
end
it 'does not grant deploy key write permissions' do
key = create(:deploy_key)
token = Gitlab::LfsToken.new(key).token
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, described_class.read_only_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to have_attributes(actor: key, project: nil, type: :lfs_deploy_token, authentication_abilities: described_class.read_only_authentication_abilities)
end
end
@@ -264,18 +265,18 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
shared_examples 'an oauth failure' do
it 'fails' do
expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ .to have_attributes(auth_failure)
end
end
it 'succeeds for OAuth tokens with the `api` scope' do
- expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, described_class.full_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :oauth, authentication_abilities: described_class.full_authentication_abilities)
end
it 'fails for OAuth tokens with other scopes' do
token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to have_attributes(auth_failure)
end
it 'does not try password auth before oauth' do
@@ -342,7 +343,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ .to have_attributes(auth_failure)
end
it 'limits abilities based on scope' do
@@ -365,36 +366,27 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails if user is blocked' do
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ .to have_attributes(auth_failure)
end
end
- context 'when using a project access token' do
- let_it_be(:project_bot_user) { create(:user, :project_bot) }
- let_it_be(:project_access_token) { create(:personal_access_token, user: project_bot_user) }
-
- context 'with valid project access token' do
- before do
- project.add_maintainer(project_bot_user)
- end
-
+ context 'when using a resource access token' do
+ shared_examples 'with a valid access token' do
it 'successfully authenticates the project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ .to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
it 'successfully authenticates the project bot with a nil project' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, ip: 'ip'))
+ .to have_attributes(actor: project_bot_user, project: nil, type: :personal_access_token, authentication_abilities: described_class.full_authentication_abilities)
end
end
- context 'with invalid project access token' do
- context 'when project bot is not a project member' do
- it 'fails for a non-project member' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
- end
+ shared_examples 'with an invalid access token' do
+ it 'fails for a non-member' do
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ .to have_attributes(auth_failure )
end
context 'when project bot user is blocked' do
@@ -403,9 +395,59 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'fails for a blocked project bot' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip'))
+ .to have_attributes(auth_failure )
+ end
+ end
+ end
+
+ context 'when using a personal namespace project access token' do
+ let_it_be(:project_bot_user) { create(:user, :project_bot) }
+ let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) }
+
+ context 'when the token belongs to the project' do
+ before do
+ project.add_maintainer(project_bot_user)
+ end
+
+ it_behaves_like 'with a valid access token'
+ end
+
+ it_behaves_like 'with an invalid access token'
+ end
+
+ context 'when in a group namespace' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ context 'when using a project access token' do
+ let_it_be(:project_bot_user) { create(:user, :project_bot) }
+ let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) }
+
+ context 'when token user belongs to the project' do
+ before do
+ project.add_maintainer(project_bot_user)
+ end
+
+ it_behaves_like 'with a valid access token'
+ end
+
+ it_behaves_like 'with an invalid access token'
+ end
+
+ context 'when using a group access token' do
+ let_it_be(:project_bot_user) { create(:user, name: 'Group token bot', email: "group_#{group.id}_bot@example.com", username: "group_#{group.id}_bot", user_type: :project_bot) }
+ let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) }
+
+ context 'when the token belongs to the group' do
+ before do
+ group.add_maintainer(project_bot_user)
+ end
+
+ it_behaves_like 'with a valid access token'
end
+
+ it_behaves_like 'with an invalid access token'
end
end
end
@@ -421,7 +463,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ .to have_attributes(auth_failure)
end
it 'goes through lfs authentication' do
@@ -432,7 +474,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
+ .to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
it 'goes through oauth authentication when the username is oauth2' do
@@ -443,14 +485,14 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))
+ .to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
it 'returns double nil for invalid credentials' do
login = 'foo'
- expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to have_attributes(auth_failure)
end
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
@@ -460,27 +502,25 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
context 'while using deploy tokens' do
- let(:auth_failure) { Gitlab::Auth::Result.new(nil, nil) }
-
shared_examples 'registry token scope' do
it 'fails when login is not valid' do
expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token is nil' do
expect(gl_auth.find_for_git_client(login, nil, project: nil, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
expect(gl_auth.find_for_git_client(login, 'abcdef', project: nil, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token has been revoked' do
@@ -488,7 +528,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
expect(deploy_token.revoked?).to be_truthy
expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: nil, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
end
@@ -500,7 +540,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails when login and token are valid' do
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
end
@@ -509,7 +549,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails when login and token are valid' do
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
end
end
@@ -520,17 +560,17 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let(:deploy_token) { create(:deploy_token, username: username, read_registry: false, projects: [project]) }
it 'succeeds for the token' do
- auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code])
+ auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
expect(gl_auth.find_for_git_client(username, deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it 'succeeds for the user' do
- auth_success = Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities)
+ auth_success = { actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities }
expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
end
@@ -538,16 +578,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
context 'and belong to the same project' do
let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) }
let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [project]) }
- let(:auth_success) { Gitlab::Auth::Result.new(read_repository, project, :deploy_token, [:download_code]) }
+ let(:auth_success) { { actor: read_repository, project: project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it 'fails for the wrong token' do
expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: project, ip: 'ip'))
- .not_to eq(auth_success)
+ .not_to have_attributes(auth_success)
end
end
@@ -556,16 +596,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let!(:read_registry) { create(:deploy_token, username: 'deployer', read_repository: false, projects: [project]) }
let!(:read_repository) { create(:deploy_token, username: read_registry.username, read_registry: false, projects: [other_project]) }
- let(:auth_success) { Gitlab::Auth::Result.new(read_repository, other_project, :deploy_token, [:download_code]) }
+ let(:auth_success) { { actor: read_repository, project: other_project, type: :deploy_token, authentication_abilities: [:download_code] } }
it 'succeeds for the right token' do
expect(gl_auth.find_for_git_client('deployer', read_repository.token, project: other_project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it 'fails for the wrong token' do
expect(gl_auth.find_for_git_client('deployer', read_registry.token, project: other_project, ip: 'ip'))
- .not_to eq(auth_success)
+ .not_to have_attributes(auth_success)
end
end
end
@@ -575,18 +615,18 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let(:login) { deploy_token.username }
it 'succeeds when login and token are valid' do
- auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code])
+ auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it 'succeeds when custom login and token are valid' do
deploy_token = create(:deploy_token, username: 'deployer', read_registry: false, projects: [project])
- auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code])
+ auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:download_code] }
expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it 'does not attempt to rate limit unique IPs for a deploy token' do
@@ -597,23 +637,23 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'fails when login is not valid' do
expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails when token is not valid' do
expect(gl_auth.find_for_git_client(login, '123123', project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token is nil' do
expect(gl_auth.find_for_git_client(login, nil, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token is not related to project' do
another_deploy_token = create(:deploy_token)
expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
it 'fails if token has been revoked' do
@@ -621,7 +661,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
expect(deploy_token.revoked?).to be_truthy
expect(gl_auth.find_for_git_client('deploy-token', deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
end
@@ -633,16 +673,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') }
it 'succeeds when login and a group deploy token are valid' do
- auth_success = Gitlab::Auth::Result.new(deploy_token, project_with_group, :deploy_token, [:download_code, :read_container_image])
+ auth_success = { actor: deploy_token, project: project_with_group, type: :deploy_token, authentication_abilities: [:download_code, :read_container_image] }
- expect(subject).to eq(auth_success)
+ expect(subject).to have_attributes(auth_success)
end
it 'fails if token is not related to group' do
another_deploy_token = create(:deploy_token, :group, read_repository: true)
expect(gl_auth.find_for_git_client(another_deploy_token.username, another_deploy_token.token, project: project_with_group, ip: 'ip'))
- .to eq(auth_failure)
+ .to have_attributes(auth_failure)
end
end
@@ -656,10 +696,10 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'succeeds when login and a project token are valid' do
- auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image])
+ auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:read_container_image] }
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it_behaves_like 'registry token scope'
@@ -678,10 +718,10 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'succeeds when login and a project token are valid' do
- auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:create_container_image])
+ auth_success = { actor: deploy_token, project: project, type: :deploy_token, authentication_abilities: [:create_container_image] }
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
- .to eq(auth_success)
+ .to have_attributes(auth_success)
end
it_behaves_like 'registry token scope'
@@ -891,6 +931,6 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
def expect_results_with_abilities(personal_access_token, abilities, success = true)
expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(personal_access_token&.user, nil, personal_access_token.nil? ? nil : :personal_access_token, abilities))
+ .to have_attributes(actor: personal_access_token&.user, project: nil, type: personal_access_token.nil? ? nil : :personal_access_token, authentication_abilities: abilities)
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb
index f56cf899410..a7895623d6f 100644
--- a/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb
@@ -23,6 +23,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequests d
end
context "for MRs with #draft? == true titles but draft attribute false" do
+ let(:mr_ids) { merge_requests.all.collect(&:id) }
+
before do
draft_prefixes.each do |prefix|
(1..4).each do |n|
@@ -37,11 +39,16 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequests d
it "updates all open draft merge request's draft field to true" do
mr_count = merge_requests.all.count
- mr_ids = merge_requests.all.collect(&:id)
expect { subject.perform(mr_ids.first, mr_ids.last) }
.to change { MergeRequest.where(draft: false).count }
.from(mr_count).to(mr_count - draft_prefixes.length)
end
+
+ it "marks successful slices as completed" do
+ expect(subject).to receive(:mark_job_as_succeeded).with(mr_ids.first, mr_ids.last)
+
+ subject.perform(mr_ids.first, mr_ids.last)
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb
new file mode 100644
index 00000000000..8f765a7a536
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsTypeNew do
+ let(:migration) { described_class.new }
+ let(:integrations) { table(:integrations) }
+ let(:namespaced_integrations) { Gitlab::Integrations::StiType.namespaced_integrations }
+
+ before do
+ integrations.connection.execute 'ALTER TABLE integrations DISABLE TRIGGER "trigger_type_new_on_insert"'
+
+ namespaced_integrations.each_with_index do |type, i|
+ integrations.create!(id: i + 1, type: "#{type}Service")
+ end
+
+ integrations.create!(id: namespaced_integrations.size + 1, type: 'LegacyService')
+ ensure
+ integrations.connection.execute 'ALTER TABLE integrations ENABLE TRIGGER "trigger_type_new_on_insert"'
+ end
+
+ it 'backfills `type_new` for the selected records' do
+ # We don't want to mock `Kernel.sleep`, so instead we mock it on the migration
+ # class before it gets forwarded.
+ expect(migration).to receive(:sleep).with(0.05).exactly(5).times
+
+ queries = ActiveRecord::QueryRecorder.new do
+ migration.perform(2, 10, :integrations, :id, 2, 50)
+ end
+
+ expect(queries.count).to be(16)
+ expect(queries.log.grep(/^SELECT/).size).to be(11)
+ expect(queries.log.grep(/^UPDATE/).size).to be(5)
+ expect(queries.log.grep(/^UPDATE/).join.scan(/WHERE .*/)).to eq([
+ 'WHERE integrations.id BETWEEN 2 AND 3',
+ 'WHERE integrations.id BETWEEN 4 AND 5',
+ 'WHERE integrations.id BETWEEN 6 AND 7',
+ 'WHERE integrations.id BETWEEN 8 AND 9',
+ 'WHERE integrations.id BETWEEN 10 AND 10'
+ ])
+
+ expect(integrations.where(id: 2..10).pluck(:type, :type_new)).to contain_exactly(
+ ['AssemblaService', 'Integrations::Assembla'],
+ ['BambooService', 'Integrations::Bamboo'],
+ ['BugzillaService', 'Integrations::Bugzilla'],
+ ['BuildkiteService', 'Integrations::Buildkite'],
+ ['CampfireService', 'Integrations::Campfire'],
+ ['ConfluenceService', 'Integrations::Confluence'],
+ ['CustomIssueTrackerService', 'Integrations::CustomIssueTracker'],
+ ['DatadogService', 'Integrations::Datadog'],
+ ['DiscordService', 'Integrations::Discord']
+ )
+
+ expect(integrations.where.not(id: 2..10)).to all(have_attributes(type_new: nil))
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index dbf74bd9333..d22aa86dbe0 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -304,7 +304,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
end
def blob_at(snippet, path)
- raw_repository(snippet).blob_at('master', path)
+ raw_repository(snippet).blob_at('main', path)
end
def repository_exists?(snippet)
diff --git a/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb b/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb
new file mode 100644
index 00000000000..db822f36c21
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::CopyCiBuildsColumnsToSecurityScans, schema: 20210728174349 do
+ let(:migration) { described_class.new }
+
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:ci_pipelines) { table(:ci_pipelines) }
+ let_it_be(:ci_builds) { table(:ci_builds) }
+ let_it_be(:security_scans) { table(:security_scans) }
+
+ let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
+ let!(:project1) { projects.create!(namespace_id: namespace.id) }
+ let!(:project2) { projects.create!(namespace_id: namespace.id) }
+ let!(:pipeline1) { ci_pipelines.create!(status: "success")}
+ let!(:pipeline2) { ci_pipelines.create!(status: "success")}
+
+ let!(:build1) { ci_builds.create!(commit_id: pipeline1.id, type: 'Ci::Build', project_id: project1.id) }
+ let!(:build2) { ci_builds.create!(commit_id: pipeline2.id, type: 'Ci::Build', project_id: project2.id) }
+ let!(:build3) { ci_builds.create!(commit_id: pipeline1.id, type: 'Ci::Build', project_id: project1.id) }
+
+ let!(:scan1) { security_scans.create!(build_id: build1.id, scan_type: 1) }
+ let!(:scan2) { security_scans.create!(build_id: build2.id, scan_type: 1) }
+ let!(:scan3) { security_scans.create!(build_id: build3.id, scan_type: 1) }
+
+ subject { migration.perform(scan1.id, scan2.id) }
+
+ before do
+ stub_const("#{described_class}::UPDATE_BATCH_SIZE", 2)
+ end
+
+ it 'copies `project_id`, `commit_id` from `ci_builds` to `security_scans`', :aggregate_failures do
+ expect(migration).to receive(:mark_job_as_succeeded).with(scan1.id, scan2.id)
+
+ subject
+
+ scan1.reload
+ expect(scan1.project_id).to eq(project1.id)
+ expect(scan1.pipeline_id).to eq(pipeline1.id)
+
+ scan2.reload
+ expect(scan2.project_id).to eq(project2.id)
+ expect(scan2.pipeline_id).to eq(pipeline2.id)
+
+ scan3.reload
+ expect(scan3.project_id).to be_nil
+ expect(scan3.pipeline_id).to be_nil
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 70906961641..30908145782 100644
--- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -13,12 +13,13 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
external_id: 'uuid-v5',
- fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('7e394d1b1eb461a7406d7b1e08f057a1cf11287a'),
name: 'Identifier for UUIDv5')
end
@@ -27,7 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
project_id: project.id,
external_type: 'uuid-v4',
external_id: 'uuid-v4',
- fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('772da93d34a1ba010bcb5efa9fb6f8e01bafcc89'),
name: 'Identifier for UUIDv4')
end
@@ -59,7 +60,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast"
- location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
end
@@ -91,7 +92,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: scanner.id,
primary_identifier_id: vulnerability_identifier.id,
report_type: 0, # "sast"
- location_fingerprint: "838574be0210968bf6b9f569df9c2576242cbf0a",
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("838574be0210968bf6b9f569df9c2576242cbf0a"),
uuid: known_uuid_v5
)
end
@@ -115,7 +116,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast"
- location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 5b20572578c..f32e6891716 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -13,7 +13,11 @@ RSpec.describe Gitlab::BackgroundMigration do
describe '.steal' do
context 'when there are enqueued jobs present' do
let(:queue) do
- [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ [
+ double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
+ double(args: ['Bar', [20, 30]], klass: 'BackgroundMigrationWorker'),
+ double(args: ['Foo', [20, 30]], klass: 'MergeWorker')
+ ]
end
before do
@@ -45,7 +49,7 @@ RSpec.describe Gitlab::BackgroundMigration do
expect(queue[0]).not_to receive(:delete)
- described_class.steal('Bar')
+ described_class.steal('Baz')
end
context 'when a custom predicate is given' do
@@ -72,8 +76,8 @@ RSpec.describe Gitlab::BackgroundMigration do
let(:migration) { spy(:migration) }
let(:queue) do
- [double(args: ['Foo', [10, 20]], queue: described_class.queue),
- double(args: ['Foo', [20, 30]], queue: described_class.queue)]
+ [double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
+ double(args: ['Foo', [20, 30]], klass: 'BackgroundMigrationWorker')]
end
before do
@@ -128,11 +132,11 @@ RSpec.describe Gitlab::BackgroundMigration do
context 'when retry_dead_jobs is true', :redis do
let(:retry_queue) do
- [double(args: ['Object', [3]], queue: described_class.queue, delete: true)]
+ [double(args: ['Object', [3]], klass: 'BackgroundMigrationWorker', delete: true)]
end
let(:dead_queue) do
- [double(args: ['Object', [4]], queue: described_class.queue, delete: true)]
+ [double(args: ['Object', [4]], klass: 'BackgroundMigrationWorker', delete: true)]
end
before do
@@ -187,20 +191,22 @@ RSpec.describe Gitlab::BackgroundMigration do
describe '.remaining', :redis do
context 'when there are jobs remaining' do
- let(:queue) { Array.new(12) }
-
before do
- allow(Sidekiq::Queue).to receive(:new)
- .with(described_class.queue)
- .and_return(Array.new(12))
-
Sidekiq::Testing.disable! do
- BackgroundMigrationWorker.perform_in(10.minutes, 'Foo')
+ MergeWorker.perform_async('Foo')
+ MergeWorker.perform_in(10.minutes, 'Foo')
+
+ 5.times do
+ BackgroundMigrationWorker.perform_async('Foo')
+ end
+ 3.times do
+ BackgroundMigrationWorker.perform_in(10.minutes, 'Foo')
+ end
end
end
it 'returns the enqueued jobs plus the scheduled jobs' do
- expect(described_class.remaining).to eq(13)
+ expect(described_class.remaining).to eq(8)
end
end
@@ -211,16 +217,13 @@ RSpec.describe Gitlab::BackgroundMigration do
end
end
- describe '.exists?' do
+ describe '.exists?', :redis do
context 'when there are enqueued jobs present' do
- let(:queue) do
- [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
- end
-
before do
- allow(Sidekiq::Queue).to receive(:new)
- .with(described_class.queue)
- .and_return(queue)
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_async('Bar')
+ BackgroundMigrationWorker.perform_async('Foo')
+ end
end
it 'returns true if specific job exists' do
@@ -232,19 +235,14 @@ RSpec.describe Gitlab::BackgroundMigration do
end
end
- context 'when there are scheduled jobs present', :redis do
+ context 'when there are scheduled jobs present' do
before do
Sidekiq::Testing.disable! do
+ MergeWorker.perform_in(10.minutes, 'Bar')
BackgroundMigrationWorker.perform_in(10.minutes, 'Foo')
-
- expect(Sidekiq::ScheduledSet.new).to be_one
end
end
- after do
- Sidekiq::ScheduledSet.new.clear
- end
-
it 'returns true if specific job exists' do
expect(described_class.exists?('Foo')).to eq(true)
end
@@ -257,7 +255,10 @@ RSpec.describe Gitlab::BackgroundMigration do
describe '.dead_jobs?' do
let(:queue) do
- [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ [
+ double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
+ double(args: ['Bar'], klass: 'MergeWorker')
+ ]
end
context 'when there are dead jobs present' do
@@ -277,7 +278,10 @@ RSpec.describe Gitlab::BackgroundMigration do
describe '.retrying_jobs?' do
let(:queue) do
- [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ [
+ double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
+ double(args: ['Bar'], klass: 'MergeWorker')
+ ]
end
context 'when there are dead jobs present' do
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index c9ad78ec760..4e4d921d67f 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -32,8 +32,7 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
expect(subject).to receive(:delete_temp_branches)
expect(project.repository).to receive(:fetch_as_mirror)
.with('http://bitbucket:test@my-bitbucket',
- refmap: [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head'],
- remote_name: 'bitbucket_server')
+ refmap: [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head'])
subject.execute
end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index a46732f8255..4a74dfcec34 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -3,40 +3,199 @@
require 'spec_helper'
RSpec.describe Gitlab::Checks::ChangesAccess do
+ include_context 'changes access checks context'
+
+ subject { changes_access }
+
describe '#validate!' do
- include_context 'changes access checks context'
+ shared_examples '#validate!' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
- before do
- allow(project).to receive(:lfs_enabled?).and_return(true)
- end
+ context 'without failed checks' do
+ it "doesn't raise an error" do
+ expect { subject.validate! }.not_to raise_error
+ end
- subject { changes_access }
+ it 'calls lfs checks' do
+ expect_next_instance_of(Gitlab::Checks::LfsCheck) do |instance|
+ expect(instance).to receive(:validate!)
+ end
- context 'without failed checks' do
- it "doesn't raise an error" do
- expect { subject.validate! }.not_to raise_error
+ subject.validate!
+ end
end
- it 'calls lfs checks' do
- expect_next_instance_of(Gitlab::Checks::LfsCheck) do |instance|
- expect(instance).to receive(:validate!)
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
+
+ expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
end
+ end
+ end
+
+ context 'with batched commits enabled' do
+ before do
+ stub_feature_flags(changes_batch_commits: true)
+ end
+
+ it_behaves_like '#validate!'
+ end
+
+ context 'with batched commits disabled' do
+ before do
+ stub_feature_flags(changes_batch_commits: false)
+ end
+
+ it_behaves_like '#validate!'
+ end
+ end
+
+ describe '#commits' do
+ it 'calls #new_commits' do
+ expect(project.repository).to receive(:new_commits).and_call_original
+
+ expect(subject.commits).to eq([])
+ end
+
+ context 'when changes contain empty revisions' do
+ let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] }
+ let(:expected_commit) { instance_double(Commit) }
+
+ it 'returns only commits with non empty revisions' do
+ expect(project.repository).to receive(:new_commits).with([newrev], { allow_quarantine: true }) { [expected_commit] }
+ expect(subject.commits).to eq([expected_commit])
+ end
+ end
+ end
+
+ describe '#commits_for' do
+ let(:new_commits) { [] }
+ let(:expected_commits) { [] }
+
+ shared_examples 'a listing of new commits' do
+ it 'returns expected commits' do
+ expect(subject).to receive(:commits).and_return(new_commits)
+
+ expect(subject.commits_for(newrev)).to eq(expected_commits)
+ end
+ end
+
+ context 'with no commits' do
+ it_behaves_like 'a listing of new commits'
+ end
+
+ context 'with unrelated commits' do
+ let(:new_commits) { [create_commit('1234', %w[1111 2222])] }
+
+ it_behaves_like 'a listing of new commits'
+ end
+
+ context 'with single related commit' do
+ let(:new_commits) { [create_commit(newrev, %w[1111 2222])] }
+ let(:expected_commits) { new_commits }
- subject.validate!
+ it_behaves_like 'a listing of new commits'
+ end
+
+ context 'with single related and unrelated commit' do
+ let(:new_commits) do
+ [
+ create_commit(newrev, %w[1111 2222]),
+ create_commit('abcd', %w[1111 2222])
+ ]
+ end
+
+ let(:expected_commits) do
+ [create_commit(newrev, %w[1111 2222])]
end
+
+ it_behaves_like 'a listing of new commits'
end
- context 'when time limit was reached' do
- it 'raises a TimeoutError' do
- logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
- access = described_class.new(changes,
- project: project,
- user_access: user_access,
- protocol: protocol,
- logger: logger)
+ context 'with multiple related commits' do
+ let(:new_commits) do
+ [
+ create_commit(newrev, %w[1111]),
+ create_commit('1111', %w[2222]),
+ create_commit('abcd', [])
+ ]
+ end
- expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ let(:expected_commits) do
+ [
+ create_commit(newrev, %w[1111]),
+ create_commit('1111', %w[2222])
+ ]
end
+
+ it_behaves_like 'a listing of new commits'
end
+
+ context 'with merge commits' do
+ let(:new_commits) do
+ [
+ create_commit(newrev, %w[1111 2222 3333]),
+ create_commit('1111', []),
+ create_commit('3333', %w[4444]),
+ create_commit('4444', [])
+ ]
+ end
+
+ let(:expected_commits) do
+ [
+ create_commit(newrev, %w[1111 2222 3333]),
+ create_commit('1111', []),
+ create_commit('3333', %w[4444]),
+ create_commit('4444', [])
+ ]
+ end
+
+ it_behaves_like 'a listing of new commits'
+ end
+
+ context 'with criss-cross merges' do
+ let(:new_commits) do
+ [
+ create_commit(newrev, %w[a1 b1]),
+ create_commit('a1', %w[a2 b2]),
+ create_commit('a2', %w[a3 b3]),
+ create_commit('a3', %w[c]),
+ create_commit('b1', %w[b2 a2]),
+ create_commit('b2', %w[b3 a3]),
+ create_commit('b3', %w[c]),
+ create_commit('c', [])
+ ]
+ end
+
+ let(:expected_commits) do
+ [
+ create_commit(newrev, %w[a1 b1]),
+ create_commit('a1', %w[a2 b2]),
+ create_commit('b1', %w[b2 a2]),
+ create_commit('a2', %w[a3 b3]),
+ create_commit('b2', %w[b3 a3]),
+ create_commit('a3', %w[c]),
+ create_commit('b3', %w[c]),
+ create_commit('c', [])
+ ]
+ end
+
+ it_behaves_like 'a listing of new commits'
+ end
+ end
+
+ def create_commit(id, parent_ids)
+ Gitlab::Git::Commit.new(project.repository, {
+ id: id,
+ parent_ids: parent_ids
+ })
end
end
diff --git a/spec/lib/gitlab/checks/matching_merge_request_spec.rb b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
index feda488a936..2e562a5a350 100644
--- a/spec/lib/gitlab/checks/matching_merge_request_spec.rb
+++ b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
@@ -49,12 +49,11 @@ RSpec.describe Gitlab::Checks::MatchingMergeRequest do
end
end
- context 'with load balancing enabled', :request_store, :redis do
+ context 'with load balancing enabled', :db_load_balancing do
let(:session) { ::Gitlab::Database::LoadBalancing::Session.current }
let(:all_caught_up) { true }
before do
- expect(::Gitlab::Database::LoadBalancing).to receive(:enable?).at_least(:once).and_return(true)
allow(::Gitlab::Database::LoadBalancing::Sticking).to receive(:all_caught_up?).and_return(all_caught_up)
expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:select_valid_host).with(:project, project.id).and_call_original
diff --git a/spec/lib/gitlab/checks/single_change_access_spec.rb b/spec/lib/gitlab/checks/single_change_access_spec.rb
index 8b235005b3e..e81e4951539 100644
--- a/spec/lib/gitlab/checks/single_change_access_spec.rb
+++ b/spec/lib/gitlab/checks/single_change_access_spec.rb
@@ -58,5 +58,52 @@ RSpec.describe Gitlab::Checks::SingleChangeAccess do
expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
end
end
+
+ describe '#commits' do
+ let(:expected_commits) { [Gitlab::Git::Commit.new(project.repository, { id: "1234" })] }
+
+ let(:access) do
+ described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger,
+ commits: provided_commits)
+ end
+
+ shared_examples '#commits' do
+ it 'returns expected commits' do
+ expect(access.commits).to eq(expected_commits)
+ end
+
+ it 'returns expected commits on repeated calls' do
+ expect(access.commits).to eq(expected_commits)
+ expect(access.commits).to eq(expected_commits)
+ end
+ end
+
+ context 'with provided commits' do
+ let(:provided_commits) { expected_commits }
+
+ before do
+ expect(project.repository).not_to receive(:new_commits)
+ end
+
+ it_behaves_like '#commits'
+ end
+
+ context 'without provided commits' do
+ let(:provided_commits) { nil }
+
+ before do
+ expect(project.repository)
+ .to receive(:new_commits)
+ .once
+ .and_return(expected_commits)
+ end
+
+ it_behaves_like '#commits'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index bf1f2bae7da..27c2b005a93 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -150,6 +150,10 @@ RSpec.describe Gitlab::Ci::Ansi2html do
expect(convert_html("\r\n")).to eq('<span><br/></span>')
end
+ it 'replaces invalid UTF-8 data' do
+ expect(convert_html("UTF-8 dashes here: ───\n🐤🐤🐤🐤\xF0\x9F\x90\n")).to eq("<span>UTF-8 dashes here: ───<br/>🐤🐤🐤🐤�<br/></span>")
+ end
+
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1_stream) { StringIO.new(pre_text) }
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index b107553bbce..e83e1326206 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -53,24 +53,8 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
context 'with retries max config option' do
let(:build) { create(:ci_build, options: { retry: { max: 1 } }) }
- context 'when build_metadata_config is set' do
- before do
- stub_feature_flags(ci_build_metadata_config: true)
- end
-
- it 'returns the number of configured max retries' do
- expect(result).to eq 1
- end
- end
-
- context 'when build_metadata_config is not set' do
- before do
- stub_feature_flags(ci_build_metadata_config: false)
- end
-
- it 'returns the number of configured max retries' do
- expect(result).to eq 1
- end
+ it 'returns the number of configured max retries' do
+ expect(result).to eq 1
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index d294eca7f15..6c9c8fa5df5 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -106,7 +106,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] },
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage)
@@ -131,7 +130,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
ignore: false,
stage: 'test',
only: { refs: %w[branches tags] },
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage)
@@ -287,7 +285,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
only: { refs: %w[branches tags] },
parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) },
{ 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] },
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage
diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
new file mode 100644
index 00000000000..b99048e2c18
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
+ let(:factory) do
+ Gitlab::Config::Entry::Factory.new(described_class)
+ .value(config)
+ end
+
+ subject(:entry) { factory.create! }
+
+ describe '.new' do
+ shared_examples 'an invalid config' do |error_message|
+ it { is_expected.not_to be_valid }
+
+ it 'has errors' do
+ expect(entry.errors).to include(error_message)
+ end
+ end
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT' } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'using a list of multiple expressions' do
+ let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } }
+
+ it_behaves_like 'an invalid config', /invalid expression syntax/
+ end
+
+ context 'when specifying an invalid if: clause expression' do
+ let(:config) { { if: ['$MY_VAR =='] } }
+
+ it_behaves_like 'an invalid config', /invalid expression syntax/
+ end
+
+ context 'when specifying an if: clause expression with an invalid token' do
+ let(:config) { { if: ['$MY_VAR == 123'] } }
+
+ it_behaves_like 'an invalid config', /invalid expression syntax/
+ end
+
+ context 'when using invalid regex in an if: clause' do
+ let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it_behaves_like 'an invalid config', /invalid expression syntax/
+ end
+
+ context 'when using an if: clause with lookahead regex character "?"' do
+ let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ it_behaves_like 'an invalid config', /invalid expression syntax/
+ end
+ end
+
+ context 'when specifying unknown policy' do
+ let(:config) { { invalid: :something } }
+
+ it_behaves_like 'an invalid config', /unknown keys: invalid/
+ end
+
+ context 'when clause is empty' do
+ let(:config) { {} }
+
+ it_behaves_like 'an invalid config', /can't be blank/
+ end
+
+ context 'when policy strategy does not match' do
+ let(:config) { 'string strategy' }
+
+ it_behaves_like 'an invalid config', /should be a hash/
+ end
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT' } }
+
+ it 'returns the config' do
+ expect(subject).to eq(if: '$THIS || $THAT')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb
new file mode 100644
index 00000000000..c255d6e9dd6
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do
+ let(:factory) do
+ Gitlab::Config::Entry::Factory.new(described_class)
+ .value(config)
+ end
+
+ subject(:entry) { factory.create! }
+
+ describe '.new' do
+ shared_examples 'a valid config' do
+ it { is_expected.to be_valid }
+
+ context 'when composed' do
+ before do
+ entry.compose!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ shared_examples 'an invalid config' do |error_message|
+ it { is_expected.not_to be_valid }
+
+ it 'has errors' do
+ expect(entry.errors).to include(error_message)
+ end
+ end
+
+ context 'with an "if"' do
+ let(:config) do
+ [{ if: '$THIS == "that"' }]
+ end
+
+ it_behaves_like 'a valid config'
+ end
+
+ context 'with a "changes"' do
+ let(:config) do
+ [{ changes: ['filename.txt'] }]
+ end
+
+ context 'when composed' do
+ before do
+ entry.compose!
+ end
+
+ it_behaves_like 'an invalid config', /contains unknown keys: changes/
+ end
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"' },
+ { if: '$SKIP' }
+ ]
+ end
+
+ it_behaves_like 'a valid config'
+ end
+
+ context 'without an array' do
+ let(:config) do
+ { if: '$SKIP' }
+ end
+
+ it_behaves_like 'an invalid config', /should be a array/
+ end
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ context 'with an "if"' do
+ let(:config) do
+ [{ if: '$THIS == "that"' }]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"' },
+ { if: '$SKIP' }
+ ]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb
index 59f0b0e7a48..275cdcddeb0 100644
--- a/spec/lib/gitlab/ci/config/entry/include_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
subject(:include_entry) { described_class.new(config) }
@@ -86,6 +86,22 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
end
end
end
+
+ context 'when using with "rules"' do
+ let(:config) { { local: 'test.yml', rules: [{ if: '$VARIABLE' }] } }
+
+ it { is_expected.to be_valid }
+
+ context 'when rules is not an array of hashes' do
+ let(:config) { { local: 'test.yml', rules: ['$VARIABLE'] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'has specific error' do
+ expect(include_entry.errors).to include('include rules should be an array of hashes')
+ end
+ end
+ end
end
context 'when value is something else' do
@@ -94,4 +110,26 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
it { is_expected.not_to be_valid }
end
end
+
+ describe '#value' do
+ subject(:value) { include_entry.value }
+
+ context 'when config is a string' do
+ let(:config) { 'test.yml' }
+
+ it { is_expected.to eq('test.yml') }
+ end
+
+ context 'when config is a hash' do
+ let(:config) { { local: 'test.yml' } }
+
+ it { is_expected.to eq(local: 'test.yml') }
+ end
+
+ context 'when config has "rules"' do
+ let(:config) { { local: 'test.yml', rules: [{ if: '$VARIABLE' }] } }
+
+ it { is_expected.to eq(local: 'test.yml', rules: [{ if: '$VARIABLE' }]) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb
index b1a8fbcdbe0..bdb4d25c142 100644
--- a/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb
@@ -24,19 +24,4 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Inherit::Variables do
end
end
end
-
- describe '#inherit?' do
- where(:config, :inherit) do
- true | true
- false | false
- %w[A] | true
- %w[B] | false
- end
-
- with_them do
- it do
- expect(subject.inherit?('A')).to eq(inherit)
- end
- end
- end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1d23ab0c2c7..5b47d3a3922 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -434,20 +434,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include 'job dependencies the another-job should be part of needs'
end
end
-
- context 'when stage: is missing' do
- let(:config) do
- {
- script: 'echo',
- needs: ['build-job']
- }
- end
-
- it 'returns error about invalid data' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'job config missing required keys: stage'
- end
- end
end
context 'when timeout value is not correct' do
@@ -626,7 +612,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
ignore: false,
after_script: %w[cleanup],
only: { refs: %w[branches tags] },
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage)
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index cb73044b62b..9a2a67389fc 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -99,7 +99,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
only: { refs: %w[branches tags] },
stage: 'test',
trigger: { project: 'my/project' },
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage
@@ -110,7 +109,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
only: { refs: %w[branches tags] },
script: ['something'],
stage: 'test',
- variables: {},
job_variables: {},
root_variables_inheritance: true,
scheduling_type: :stage
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index f98a6a869d6..b872f6644a2 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -362,76 +362,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
context 'with inheritance' do
- context 'of variables' do
- let(:config) do
- { variables: { A: 'job', B: 'job' } }
- end
-
- before do
- entry.compose!(deps)
- end
-
- context 'with only job variables' do
- it 'does return defined variables' do
- expect(entry.value).to include(
- variables: { 'A' => 'job', 'B' => 'job' },
- job_variables: { 'A' => 'job', 'B' => 'job' },
- root_variables_inheritance: true
- )
- end
- end
-
- context 'when root yaml variables are used' do
- let(:variables) do
- Gitlab::Ci::Config::Entry::Variables.new(
- { A: 'root', C: 'root', D: 'root' }
- ).value
- end
-
- it 'does return job and root variables' do
- expect(entry.value).to include(
- variables: { 'A' => 'job', 'B' => 'job', 'C' => 'root', 'D' => 'root' },
- job_variables: { 'A' => 'job', 'B' => 'job' },
- root_variables_inheritance: true
- )
- end
-
- context 'when inherit of defaults is disabled' do
- let(:config) do
- {
- variables: { A: 'job', B: 'job' },
- inherit: { variables: false }
- }
- end
-
- it 'does return job and root variables' do
- expect(entry.value).to include(
- variables: { 'A' => 'job', 'B' => 'job' },
- job_variables: { 'A' => 'job', 'B' => 'job' },
- root_variables_inheritance: false
- )
- end
- end
-
- context 'when inherit of only specific variable is enabled' do
- let(:config) do
- {
- variables: { A: 'job', B: 'job' },
- inherit: { variables: ['D'] }
- }
- end
-
- it 'does return job and root variables' do
- expect(entry.value).to include(
- variables: { 'A' => 'job', 'B' => 'job', 'D' => 'root' },
- job_variables: { 'A' => 'job', 'B' => 'job' },
- root_variables_inheritance: ['D']
- )
- end
- end
- end
- end
-
context 'of default:tags' do
using RSpec::Parameterized::TableSyntax
@@ -493,7 +423,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
name: :rspec,
stage: 'test',
only: { refs: %w[branches tags] },
- variables: {},
job_variables: {},
root_variables_inheritance: true
)
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 31e3545e8d8..d862fbf5b78 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -132,7 +132,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- variables: { 'VAR' => 'root', 'VAR2' => 'val 2' },
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -148,7 +147,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- variables: { 'VAR' => 'root', 'VAR2' => 'val 2' },
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -166,7 +164,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }],
only: { refs: %w(branches tags) },
- variables: { 'VAR' => 'job', 'VAR2' => 'val 2' },
job_variables: { 'VAR' => 'job' },
root_variables_inheritance: true,
after_script: [],
@@ -214,7 +211,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- variables: { 'VAR' => 'root' },
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -228,7 +224,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
- variables: { 'VAR' => 'job' },
job_variables: { 'VAR' => 'job' },
root_variables_inheritance: true,
ignore: false,
diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
index 7d26365e7b3..91252378541 100644
--- a/spec/lib/gitlab/ci/config/entry/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do
describe '.new' do
subject { entry }
+ before do
+ subject.compose!
+ end
+
context 'with a list of rule rule' do
let(:config) do
[{ if: '$THIS == "that"', when: 'never' }]
@@ -24,14 +28,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do
it { is_expected.to be_a(described_class) }
it { is_expected.to be_valid }
-
- context 'when composed' do
- before do
- subject.compose!
- end
-
- it { is_expected.to be_valid }
- end
end
context 'with a list of two rules' do
@@ -42,21 +38,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do
]
end
- it { is_expected.to be_a(described_class) }
it { is_expected.to be_valid }
+ end
- context 'when composed' do
- before do
- subject.compose!
- end
+ context 'with a single rule object' do
+ let(:config) do
+ { if: '$SKIP', when: 'never' }
+ end
- it { is_expected.to be_valid }
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with nested rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ [{ if: '$SKIP', when: 'never' }]
+ ]
end
+
+ it { is_expected.to be_valid }
end
- context 'with a single rule object' do
+ context 'with rules nested more than one level' do
let(:config) do
- { if: '$SKIP', when: 'never' }
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ [{ if: '$SKIP', when: 'never' }, [{ if: '$THIS == "other"', when: 'aways' }]]
+ ]
end
it { is_expected.not_to be_valid }
@@ -90,7 +99,36 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do
{ if: '$SKIP', when: 'never' }
end
- it { is_expected.to eq(config) }
+ it { is_expected.to eq([config]) }
+ end
+
+ context 'with nested rules' do
+ let(:first_rule) { { if: '$THIS == "that"', when: 'always' } }
+ let(:second_rule) { { if: '$SKIP', when: 'never' } }
+
+ let(:config) do
+ [
+ first_rule,
+ [second_rule]
+ ]
+ end
+
+ it { is_expected.to contain_exactly(first_rule, second_rule) }
+ end
+
+ context 'with rules nested more than one level' do
+ let(:first_rule) { { if: '$THIS == "that"', when: 'always' } }
+ let(:second_rule) { { if: '$SKIP', when: 'never' } }
+ let(:third_rule) { { if: '$THIS == "other"', when: 'aways' } }
+
+ let(:config) do
+ [
+ first_rule,
+ [second_rule, [third_rule]]
+ ]
+ end
+
+ it { is_expected.to contain_exactly(first_rule, second_rule, third_rule) }
end
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 88097f3f56a..a471997e43a 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' }
- let(:context_params) { { project: project, sha: '123456', user: user, variables: project.predefined_variables.to_runner_variables } }
+ let(:context_params) { { project: project, sha: '123456', user: user, variables: project.predefined_variables } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:file_content) do
@@ -347,15 +347,51 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
expect(subject.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
end
+ end
+
+ context "when 'include' has rules" do
+ let(:values) do
+ { include: [{ remote: remote_url },
+ { local: local_file, rules: [{ if: "$CI_PROJECT_ID == '#{project_id}'" }] }],
+ image: 'ruby:2.7' }
+ end
- context 'when the FF ci_wildcard_file_paths is disabled' do
- before do
- stub_feature_flags(ci_wildcard_file_paths: false)
+ context 'when the rules matches' do
+ let(:project_id) { project.id }
+
+ it 'includes the file' do
+ expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local))
end
- it 'cannot find any file returns an error message' do
- expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local))
- expect(subject[0].errors).to eq(['Local file `myfolder/*.yml` does not exist!'])
+ context 'when the FF ci_include_rules is disabled' do
+ before do
+ stub_feature_flags(ci_include_rules: false)
+ end
+
+ it 'includes the file' do
+ expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ end
+ end
+ end
+
+ context 'when the rules does not match' do
+ let(:project_id) { non_existing_record_id }
+
+ it 'does not include the file' do
+ expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote))
+ end
+
+ context 'when the FF ci_include_rules is disabled' do
+ before do
+ stub_feature_flags(ci_include_rules: false)
+ end
+
+ it 'includes the file' do
+ expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Local))
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
new file mode 100644
index 00000000000..89ea13d710d
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Rules do
+ let(:rule_hashes) {}
+
+ subject(:rules) { described_class.new(rule_hashes) }
+
+ describe '#evaluate' do
+ let(:context) { double(variables: {}) }
+
+ subject(:result) { rules.evaluate(context).pass? }
+
+ context 'when there is no rule' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when there is a rule' do
+ let(:rule_hashes) { [{ if: '$MY_VAR == "hello"' }] }
+
+ context 'when the rule matches' do
+ let(:context) { double(variables: { MY_VAR: 'hello' }) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when the rule does not match' do
+ let(:context) { double(variables: { MY_VAR: 'invalid' }) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
index e5f0341c5fe..a29471706cc 100644
--- a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
+++ b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb
@@ -50,10 +50,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do
name: 'test: [aws, app1]',
instance: 1,
parallel: { total: 4 },
- variables: {
- 'PROVIDER' => 'aws',
- 'STACK' => 'app1'
- },
job_variables: {
'PROVIDER' => 'aws',
'STACK' => 'app1'
@@ -63,10 +59,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do
name: 'test: [aws, app2]',
instance: 2,
parallel: { total: 4 },
- variables: {
- 'PROVIDER' => 'aws',
- 'STACK' => 'app2'
- },
job_variables: {
'PROVIDER' => 'aws',
'STACK' => 'app2'
@@ -76,10 +68,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do
name: 'test: [ovh, app]',
instance: 3,
parallel: { total: 4 },
- variables: {
- 'PROVIDER' => 'ovh',
- 'STACK' => 'app'
- },
job_variables: {
'PROVIDER' => 'ovh',
'STACK' => 'app'
@@ -89,10 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do
name: 'test: [gcp, app]',
instance: 4,
parallel: { total: 4 },
- variables: {
- 'PROVIDER' => 'gcp',
- 'STACK' => 'app'
- },
job_variables: {
'PROVIDER' => 'gcp',
'STACK' => 'app'
diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb
index 4c19657413c..354392eb42e 100644
--- a/spec/lib/gitlab/ci/config/normalizer_spec.rb
+++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb
@@ -4,7 +4,7 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Config::Normalizer do
let(:job_name) { :rspec }
- let(:job_config) { { script: 'rspec', parallel: parallel_config, name: 'rspec', variables: variables_config } }
+ let(:job_config) { { script: 'rspec', parallel: parallel_config, name: 'rspec', job_variables: variables_config } }
let(:config) { { job_name => job_config } }
describe '.normalize_jobs' do
@@ -202,21 +202,21 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
it 'sets job variables', :aggregate_failures do
expect(subject.values[0]).to match(
- a_hash_including(variables: { VAR_1: 'A', VAR_2: 'B', USER_VARIABLE: 'user value' })
+ a_hash_including(job_variables: { VAR_1: 'A', VAR_2: 'B', USER_VARIABLE: 'user value' })
)
expect(subject.values[1]).to match(
- a_hash_including(variables: { VAR_1: 'A', VAR_2: 'C', USER_VARIABLE: 'user value' })
+ a_hash_including(job_variables: { VAR_1: 'A', VAR_2: 'C', USER_VARIABLE: 'user value' })
)
end
it 'parallelizes jobs with original config' do
configs = subject.values.map do |config|
- config.except(:name, :instance, :variables)
+ config.except(:name, :instance, :job_variables)
end
original_config = config[job_name]
- .except(:name, :variables)
+ .except(:name, :job_variables)
.deep_merge(parallel: { total: 2 })
expect(configs).to all(match(a_hash_including(original_config)))
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 45ce4cac6c4..3ec4519748f 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -286,7 +286,9 @@ RSpec.describe Gitlab::Ci::Config do
end
context "when using 'include' directive" do
- let(:project) { create(:project, :repository) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :repository, group: group) }
+ let(:main_project) { create(:project, :repository, :public, group: group) }
let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
@@ -317,7 +319,9 @@ RSpec.describe Gitlab::Ci::Config do
include:
- #{local_location}
- #{remote_location}
-
+ - project: '$MAIN_PROJECT'
+ ref: '$REF'
+ file: '$FILENAME'
image: ruby:2.7
HEREDOC
end
@@ -331,6 +335,26 @@ RSpec.describe Gitlab::Ci::Config do
allow(project.repository)
.to receive(:blob_data_at).and_return(local_file_content)
+
+ main_project.repository.create_file(
+ main_project.creator,
+ '.gitlab-ci.yml',
+ local_file_content,
+ message: 'Add README.md',
+ branch_name: 'master'
+ )
+
+ main_project.repository.create_file(
+ main_project.creator,
+ '.another-ci-file.yml',
+ local_file_content,
+ message: 'Add README.md',
+ branch_name: 'master'
+ )
+
+ create(:ci_variable, project: project, key: "REF", value: "HEAD")
+ create(:ci_group_variable, group: group, key: "FILENAME", value: ".gitlab-ci.yml")
+ create(:ci_instance_variable, key: 'MAIN_PROJECT', value: main_project.full_path)
end
context "when gitlab_ci_yml has valid 'include' defined" do
@@ -344,6 +368,38 @@ RSpec.describe Gitlab::Ci::Config do
expect(config.to_hash).to eq(composed_hash)
end
+
+ context 'handling variables' do
+ it 'contains all project variables' do
+ ref = config.context.variables.find { |v| v[:key] == 'REF' }
+
+ expect(ref[:value]).to eq("HEAD")
+ end
+
+ it 'contains all group variables' do
+ filename = config.context.variables.find { |v| v[:key] == 'FILENAME' }
+
+ expect(filename[:value]).to eq(".gitlab-ci.yml")
+ end
+
+ it 'contains all instance variables' do
+ project = config.context.variables.find { |v| v[:key] == 'MAIN_PROJECT' }
+
+ expect(project[:value]).to eq(main_project.full_path)
+ end
+
+ context 'overriding a group variable at project level' do
+ before do
+ create(:ci_variable, project: project, key: "FILENAME", value: ".another-ci-file.yml")
+ end
+
+ it 'successfully overrides' do
+ filename = config.context.variables.to_hash[:FILENAME]
+
+ expect(filename).to eq('.another-ci-file.yml')
+ end
+ end
+ end
end
context "when gitlab_ci.yml has invalid 'include' defined" do
@@ -667,5 +723,33 @@ RSpec.describe Gitlab::Ci::Config do
expect(config.to_hash).to eq(composed_hash)
end
end
+
+ context "when an 'include' has rules" do
+ let(:gitlab_ci_yml) do
+ <<~HEREDOC
+ include:
+ - local: #{local_location}
+ rules:
+ - if: $CI_PROJECT_ID == "#{project_id}"
+ image: ruby:2.7
+ HEREDOC
+ end
+
+ context 'when the rules condition is satisfied' do
+ let(:project_id) { project.id }
+
+ it 'includes the file' do
+ expect(config.to_hash).to include(local_location_hash)
+ end
+ end
+
+ context 'when the rules condition is satisfied' do
+ let(:project_id) { non_existing_record_id }
+
+ it 'does not include the file' do
+ expect(config.to_hash).not_to include(local_location_hash)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index 77f6608eb85..1e433d7854a 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Lint do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:lint) { described_class.new(project: project, current_user: user) }
@@ -89,6 +89,15 @@ RSpec.describe Gitlab::Ci::Lint do
)
end
+ after do
+ project.repository.delete_file(
+ project.creator,
+ 'another-gitlab-ci.yml',
+ message: 'Remove another-gitlab-ci.yml',
+ branch_name: 'master'
+ )
+ end
+
it 'sets merged_config' do
root_config = YAML.safe_load(content, [Symbol])
included_config = YAML.safe_load(included_content, [Symbol])
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
new file mode 100644
index 00000000000..c6387bf615b
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -0,0 +1,350 @@
+# frozen_string_literal: true
+
+# TODO remove duplication from spec/lib/gitlab/ci/parsers/security/common_spec.rb and spec/lib/gitlab/ci/parsers/security/common_spec.rb
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/336589
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Security::Common do
+ describe '#parse!' do
+ where(vulnerability_finding_signatures_enabled: [true, false])
+ with_them do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ let(:artifact) { build(:ci_job_artifact, :common_security_report) }
+ let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) }
+ # The path 'yarn.lock' was initially used by DependencyScanning, it is okay for SAST locations to use it, but this could be made better
+ let(:location) { ::Gitlab::Ci::Reports::Security::Locations::Sast.new(file_path: 'yarn.lock', start_line: 1, end_line: 1) }
+ let(:tracking_data) { nil }
+
+ before do
+ allow_next_instance_of(described_class) do |parser|
+ allow(parser).to receive(:create_location).and_return(location)
+ allow(parser).to receive(:tracking_data).and_return(tracking_data)
+ end
+
+ artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
+ end
+
+ describe 'schema validation' do
+ let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator }
+ let(:parser) { described_class.new('{}', report, vulnerability_finding_signatures_enabled, validate: validate) }
+
+ subject(:parse_report) { parser.parse! }
+
+ before do
+ allow(validator_class).to receive(:new).and_call_original
+ end
+
+ context 'when the validate flag is set as `false`' do
+ let(:validate) { false }
+
+ it 'does not run the validation logic' do
+ parse_report
+
+ expect(validator_class).not_to have_received(:new)
+ end
+ end
+
+ context 'when the validate flag is set as `true`' do
+ let(:validate) { true }
+ let(:valid?) { false }
+
+ before do
+ allow_next_instance_of(validator_class) do |instance|
+ allow(instance).to receive(:valid?).and_return(valid?)
+ allow(instance).to receive(:errors).and_return(['foo'])
+ end
+
+ allow(parser).to receive_messages(create_scanner: true, create_scan: true)
+ end
+
+ it 'instantiates the validator with correct params' do
+ parse_report
+
+ expect(validator_class).to have_received(:new).with(report.type, {})
+ end
+
+ context 'when the report data is not valid according to the schema' do
+ it 'adds errors to the report' do
+ expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
+ end
+
+ it 'does not try to create report entities' do
+ parse_report
+
+ expect(parser).not_to have_received(:create_scanner)
+ expect(parser).not_to have_received(:create_scan)
+ end
+ end
+
+ context 'when the report data is valid according to the schema' do
+ let(:valid?) { true }
+
+ it 'does not add errors to the report' do
+ expect { parse_report }.not_to change { report.errors }.from([])
+ end
+
+ it 'keeps the execution flow as normal' do
+ parse_report
+
+ expect(parser).to have_received(:create_scanner)
+ expect(parser).to have_received(:create_scan)
+ end
+ end
+ end
+ end
+
+ describe 'parsing finding.name' do
+ let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) }
+
+ context 'when message is provided' do
+ it 'sets message from the report as a finding name' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
+ expected_name = Gitlab::Json.parse(finding.raw_metadata)['message']
+
+ expect(finding.name).to eq(expected_name)
+ end
+ end
+
+ context 'when message is not provided' do
+ context 'and name is provided' do
+ it 'sets name from the report as a name' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
+ expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
+
+ expect(finding.name).to eq(expected_name)
+ end
+ end
+
+ context 'and name is not provided' do
+ context 'when CVE identifier exists' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
+ end
+ end
+
+ context 'when CWE identifier exists' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
+ expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
+ end
+ end
+
+ context 'when neither CVE nor CWE identifier exist' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
+ expect(finding.name).to eq("other-2017-11429 in yarn.lock")
+ end
+ end
+ end
+ end
+ end
+
+ describe 'parsing finding.details' do
+ context 'when details are provided' do
+ it 'sets details from the report' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
+ expected_details = Gitlab::Json.parse(finding.raw_metadata)['details']
+
+ expect(finding.details).to eq(expected_details)
+ end
+ end
+
+ context 'when details are not provided' do
+ it 'sets empty hash' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
+ expect(finding.details).to eq({})
+ end
+ end
+ end
+
+ describe 'top-level scanner' do
+ it 'is the primary scanner' do
+ expect(report.primary_scanner.external_id).to eq('gemnasium')
+ expect(report.primary_scanner.name).to eq('Gemnasium')
+ expect(report.primary_scanner.vendor).to eq('GitLab')
+ expect(report.primary_scanner.version).to eq('2.18.0')
+ end
+
+ it 'returns nil report has no scanner' do
+ empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
+ described_class.parse!({}.to_json, empty_report)
+
+ expect(empty_report.primary_scanner).to be_nil
+ end
+ end
+
+ describe 'parsing scanners' do
+ subject(:scanner) { report.findings.first.scanner }
+
+ context 'when vendor is not missing in scanner' do
+ it 'returns scanner with parsed vendor value' do
+ expect(scanner.vendor).to eq('GitLab')
+ end
+ end
+ end
+
+ describe 'parsing scan' do
+ it 'returns scan object for each finding' do
+ scans = report.findings.map(&:scan)
+
+ expect(scans.map(&:status).all?('success')).to be(true)
+ expect(scans.map(&:start_time).all?('placeholder-value')).to be(true)
+ expect(scans.map(&:end_time).all?('placeholder-value')).to be(true)
+ expect(scans.size).to eq(3)
+ expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan)
+ end
+
+ it 'returns nil when scan is not a hash' do
+ empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
+ described_class.parse!({}.to_json, empty_report)
+
+ expect(empty_report.scan).to be(nil)
+ end
+ end
+
+ describe 'parsing schema version' do
+ it 'parses the version' do
+ expect(report.version).to eq('14.0.2')
+ end
+
+ it 'returns nil when there is no version' do
+ empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
+ described_class.parse!({}.to_json, empty_report)
+
+ expect(empty_report.version).to be_nil
+ end
+ end
+
+ describe 'parsing analyzer' do
+ it 'associates analyzer with report' do
+ expect(report.analyzer.id).to eq('common-analyzer')
+ expect(report.analyzer.name).to eq('Common Analyzer')
+ expect(report.analyzer.version).to eq('2.0.1')
+ expect(report.analyzer.vendor).to eq('Common')
+ end
+
+ it 'returns nil when analyzer data is not available' do
+ empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
+ described_class.parse!({}.to_json, empty_report)
+
+ expect(empty_report.analyzer).to be_nil
+ end
+ end
+
+ describe 'parsing links' do
+ it 'returns links object for each finding', :aggregate_failures do
+ links = report.findings.flat_map(&:links)
+
+ expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'])
+ expect(links.map(&:name)).to match_array([nil, 'CVE-1030'])
+ expect(links.size).to eq(2)
+ expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
+ end
+ end
+
+ describe 'setting the uuid' do
+ let(:finding_uuids) { report.findings.map(&:uuid) }
+ let(:uuid_1) do
+ Security::VulnerabilityUUID.generate(
+ report_type: "sast",
+ primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
+ location_fingerprint: location.fingerprint,
+ project_id: pipeline.project_id
+ )
+ end
+
+ let(:uuid_2) do
+ Security::VulnerabilityUUID.generate(
+ report_type: "sast",
+ primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
+ location_fingerprint: location.fingerprint,
+ project_id: pipeline.project_id
+ )
+ end
+
+ let(:expected_uuids) { [uuid_1, uuid_2, nil] }
+
+ it 'sets the UUIDv5 for findings', :aggregate_failures do
+ allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report|
+ allow(report).to receive(:type).and_return('sast')
+
+ expect(finding_uuids).to match_array(expected_uuids)
+ end
+ end
+ end
+
+ describe 'parsing tracking' do
+ let(:tracking_data) do
+ {
+ 'type' => 'source',
+ 'items' => [
+ 'signatures' => [
+ { 'algorithm' => 'hash', 'value' => 'hash_value' },
+ { 'algorithm' => 'location', 'value' => 'location_value' },
+ { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
+ ]
+ ]
+ }
+ end
+
+ context 'with valid tracking information' do
+ it 'creates signatures for each algorithm' do
+ finding = report.findings.first
+ expect(finding.signatures.size).to eq(3)
+ expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset'])
+ end
+ end
+
+ context 'with invalid tracking information' do
+ let(:tracking_data) do
+ {
+ 'type' => 'source',
+ 'items' => [
+ 'signatures' => [
+ { 'algorithm' => 'hash', 'value' => 'hash_value' },
+ { 'algorithm' => 'location', 'value' => 'location_value' },
+ { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' }
+ ]
+ ]
+ }
+ end
+
+ it 'ignores invalid algorithm types' do
+ finding = report.findings.first
+ expect(finding.signatures.size).to eq(2)
+ expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
+ end
+ end
+
+ context 'with valid tracking information' do
+ it 'creates signatures for each signature algorithm' do
+ finding = report.findings.first
+ expect(finding.signatures.size).to eq(3)
+ expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
+
+ signatures = finding.signatures.index_by(&:algorithm_type)
+ expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] }
+ expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value'])
+ expect(signatures['location'].signature_value).to eq(expected_values['location']['value'])
+ expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value'])
+ end
+
+ it 'sets the uuid according to the higest priority signature' do
+ finding = report.findings.first
+ highest_signature = finding.signatures.max_by(&:priority)
+
+ identifiers = if vulnerability_finding_signatures_enabled
+ "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
+ else
+ "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
+ end
+
+ expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb
new file mode 100644
index 00000000000..4bc48f6611a
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Security::Sast do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#parse!' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ let(:created_at) { 2.weeks.ago }
+
+ context "when parsing valid reports" do
+ where(:report_format, :report_version, :scanner_length, :finding_length, :identifier_length, :file_path, :line) do
+ :sast | '14.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47
+ :sast_deprecated | '1.2' | 3 | 33 | 17 | 'python/hardcoded/hardcoded-tmp.py' | 1
+ end
+
+ with_them do
+ let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) }
+ let(:artifact) { create(:ci_job_artifact, report_format) }
+
+ before do
+ artifact.each_blob { |blob| described_class.parse!(blob, report) }
+ end
+
+ it "parses all identifiers and findings" do
+ expect(report.findings.length).to eq(finding_length)
+ expect(report.identifiers.length).to eq(identifier_length)
+ expect(report.scanners.length).to eq(scanner_length)
+ end
+
+ it 'generates expected location' do
+ location = report.findings.first.location
+
+ expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast)
+ expect(location).to have_attributes(
+ file_path: file_path,
+ end_line: line,
+ start_line: line
+ )
+ end
+
+ it "generates expected metadata_version" do
+ expect(report.findings.first.metadata_version).to eq(report_version)
+ end
+ end
+ end
+
+ context "when parsing an empty report" do
+ let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', pipeline, created_at) }
+ let(:blob) { Gitlab::Json.generate({}) }
+
+ it { expect(described_class.parse!(blob, report)).to be_empty }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
new file mode 100644
index 00000000000..1d361e16aad
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do
+ describe '#parse!' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ let(:created_at) { 2.weeks.ago }
+
+ context "when parsing valid reports" do
+ where(report_format: %i(secret_detection))
+
+ with_them do
+ let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) }
+ let(:artifact) { create(:ci_job_artifact, report_format) }
+
+ before do
+ artifact.each_blob { |blob| described_class.parse!(blob, report) }
+ end
+
+ it "parses all identifiers and findings" do
+ expect(report.findings.length).to eq(1)
+ expect(report.identifiers.length).to eq(1)
+ expect(report.scanners.length).to eq(1)
+ end
+
+ it 'generates expected location' do
+ location = report.findings.first.location
+
+ expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::SecretDetection)
+ expect(location).to have_attributes(
+ file_path: 'aws-key.py',
+ start_line: nil,
+ end_line: nil,
+ class_name: nil,
+ method_name: nil
+ )
+ end
+
+ it "generates expected metadata_version" do
+ expect(report.findings.first.metadata_version).to eq('3.0')
+ end
+ end
+ end
+
+ context "when parsing an empty report" do
+ let(:report) { Gitlab::Ci::Reports::Security::Report.new('secret_detection', pipeline, created_at) }
+ let(:blob) { Gitlab::Json.generate({}) }
+
+ it { expect(described_class.parse!(blob, report)).to be_empty }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
new file mode 100644
index 00000000000..f434ffd12bf
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:report_type, :expected_errors, :valid_data) do
+ :sast | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] }
+ :secret_detection | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] }
+ end
+
+ with_them do
+ let(:validator) { described_class.new(report_type, report_data) }
+
+ describe '#valid?' do
+ subject { validator.valid? }
+
+ context 'when given data is invalid according to the schema' do
+ let(:report_data) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when given data is valid according to the schema' do
+ let(:report_data) { valid_data }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#errors' do
+ let(:report_data) { { 'version' => '10.0.0' } }
+
+ subject { validator.errors }
+
+ it { is_expected.to eq(expected_errors) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 2e73043e309..c22a0e23794 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -295,31 +295,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
- describe '#dangling_build?' do
- let(:project) { create(:project, :repository) }
- let(:command) { described_class.new(project: project, source: source) }
-
- subject { command.dangling_build? }
-
- context 'when source is :webide' do
- let(:source) { :webide }
-
- it { is_expected.to eq(true) }
- end
-
- context 'when source is :ondemand_dast_scan' do
- let(:source) { :ondemand_dast_scan }
-
- it { is_expected.to eq(true) }
- end
-
- context 'when source something else' do
- let(:source) { :web }
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#creates_child_pipeline?' do
let(:command) { described_class.new(bridge: bridge) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
index 499dc3554a3..1aa104310af 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
end
it 'logs the error' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
instance_of(Gitlab::Ci::Limit::LimitExceededError),
project_id: project.id, plan: namespace.actual_plan_name
)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index cc4aaffb0a4..83d47ae6819 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
let_it_be(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
- let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
+ let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project) }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
@@ -71,5 +71,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
expect(histogram).to have_received(:observe)
.with({ source: 'push' }, 0)
end
+
+ it 'records active jobs by pipeline plan in a histogram' do
+ allow(command.metrics)
+ .to receive(:active_jobs_histogram)
+ .and_return(histogram)
+
+ pipeline = create(:ci_pipeline, project: project, status: :running)
+ create(:ci_build, :finished, project: project, pipeline: pipeline)
+ create(:ci_build, :failed, project: project, pipeline: pipeline)
+ create(:ci_build, :running, project: project, pipeline: pipeline)
+ subject.build!
+
+ expect(histogram).to have_received(:observe)
+ .with(hash_including(plan: project.actual_plan_name), 3)
+ end
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb
new file mode 100644
index 00000000000..c56177a6453
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::AggregatedReport do
+ subject { described_class.new(reports, findings) }
+
+ let(:reports) { build_list(:ci_reports_security_report, 1) }
+ let(:findings) { build_list(:ci_reports_security_finding, 1) }
+
+ describe '#created_at' do
+ context 'no reports' do
+ let(:reports) { [] }
+
+ it 'has no created date' do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context 'report with no created date' do
+ let(:reports) { build_list(:ci_reports_security_report, 1, created_at: nil) }
+
+ it 'has no created date' do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context 'has reports' do
+ let(:a_long_time_ago) { 2.months.ago }
+ let(:a_while_ago) { 2.weeks.ago }
+ let(:yesterday) { 1.day.ago }
+
+ let(:reports) do
+ [build(:ci_reports_security_report, created_at: a_while_ago),
+ build(:ci_reports_security_report, created_at: a_long_time_ago),
+ build(:ci_reports_security_report, created_at: nil),
+ build(:ci_reports_security_report, created_at: yesterday)]
+ end
+
+ it 'has oldest created date' do
+ expect(subject.created_at).to eq(a_long_time_ago)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
new file mode 100644
index 00000000000..784c1183320
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::FindingKey do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#==' do
+ where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do
+ nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | nil | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | nil | false
+ nil | nil | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | nil | nil | false
+ nil | nil | nil | nil | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true
+ end
+
+ with_them do
+ let(:finding_key_1) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_1,
+ identifier_fingerprint: identifier_fp_1)
+ end
+
+ let(:finding_key_2) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_2,
+ identifier_fingerprint: identifier_fp_2)
+ end
+
+ subject { finding_key_1 == finding_key_2 }
+
+ it { is_expected.to be(equals?) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
new file mode 100644
index 00000000000..23e6b40a039
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
+ subject { described_class.new(params.with_indifferent_access) }
+
+ let(:params) do
+ {
+ algorithm_type: 'hash',
+ signature_value: 'SIGNATURE'
+ }
+ end
+
+ describe '#initialize' do
+ context 'when a supported algorithm type is given' do
+ it 'allows itself to be created' do
+ expect(subject.algorithm_type).to eq(params[:algorithm_type])
+ expect(subject.signature_value).to eq(params[:signature_value])
+ end
+
+ describe '#valid?' do
+ it 'returns true' do
+ expect(subject.valid?).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '#valid?' do
+ context 'when supported algorithm_type is given' do
+ it 'is valid' do
+ expect(subject.valid?).to eq(true)
+ end
+ end
+
+ context 'when an unsupported algorithm_type is given' do
+ let(:params) do
+ {
+ algorithm_type: 'INVALID',
+ signature_value: 'SIGNATURE'
+ }
+ end
+
+ it 'is not valid' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+ end
+
+ describe '#to_hash' do
+ it 'returns a hash representation of the signature' do
+ expect(subject.to_hash).to eq(
+ algorithm_type: params[:algorithm_type],
+ signature_sha: Digest::SHA1.digest(params[:signature_value])
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb
new file mode 100644
index 00000000000..effa7a60400
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Locations::Sast do
+ let(:params) do
+ {
+ file_path: 'src/main/App.java',
+ start_line: 29,
+ end_line: 31,
+ class_name: 'com.gitlab.security_products.tests.App',
+ method_name: 'insecureCypher'
+ }
+ end
+
+ let(:mandatory_params) { %i[file_path start_line] }
+ let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
+ let(:expected_fingerprint_path) { 'App.java' }
+
+ it_behaves_like 'vulnerability location'
+end
diff --git a/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb
new file mode 100644
index 00000000000..3b84a548713
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Locations::SecretDetection do
+ let(:params) do
+ {
+ file_path: 'src/main/App.java',
+ start_line: 29,
+ end_line: 31,
+ class_name: 'com.gitlab.security_products.tests.App',
+ method_name: 'insecureCypher'
+ }
+ end
+
+ let(:mandatory_params) { %i[file_path start_line] }
+ let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
+ let(:expected_fingerprint_path) { 'App.java' }
+
+ it_behaves_like 'vulnerability location'
+end
diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb
new file mode 100644
index 00000000000..5a85c3f19fc
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Report do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ let(:created_at) { 2.weeks.ago }
+
+ subject(:report) { described_class.new('sast', pipeline, created_at) }
+
+ it { expect(report.type).to eq('sast') }
+ it { is_expected.to delegate_method(:project_id).to(:pipeline) }
+
+ describe '#add_scanner' do
+ let(:scanner) { create(:ci_reports_security_scanner, external_id: 'find_sec_bugs') }
+
+ subject { report.add_scanner(scanner) }
+
+ it 'stores given scanner params in the map' do
+ subject
+
+ expect(report.scanners).to eq({ 'find_sec_bugs' => scanner })
+ end
+
+ it 'returns the added scanner' do
+ expect(subject).to eq(scanner)
+ end
+ end
+
+ describe '#add_identifier' do
+ let(:identifier) { create(:ci_reports_security_identifier) }
+
+ subject { report.add_identifier(identifier) }
+
+ it 'stores given identifier params in the map' do
+ subject
+
+ expect(report.identifiers).to eq({ identifier.fingerprint => identifier })
+ end
+
+ it 'returns the added identifier' do
+ expect(subject).to eq(identifier)
+ end
+ end
+
+ describe '#add_finding' do
+ let(:finding) { create(:ci_reports_security_finding) }
+
+ it 'enriches given finding and stores it in the collection' do
+ report.add_finding(finding)
+
+ expect(report.findings).to eq([finding])
+ end
+ end
+
+ describe '#clone_as_blank' do
+ let(:report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding)],
+ scanners: [create(:ci_reports_security_scanner)],
+ identifiers: [create(:ci_reports_security_identifier)]
+ )
+ end
+
+ it 'creates a blank report with copied type and pipeline' do
+ clone = report.clone_as_blank
+
+ expect(clone.type).to eq(report.type)
+ expect(clone.pipeline).to eq(report.pipeline)
+ expect(clone.created_at).to eq(report.created_at)
+ expect(clone.findings).to eq([])
+ expect(clone.scanners).to eq({})
+ expect(clone.identifiers).to eq({})
+ end
+ end
+
+ describe '#replace_with!' do
+ let(:report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding)],
+ scanners: [create(:ci_reports_security_scanner)],
+ identifiers: [create(:ci_reports_security_identifier)]
+ )
+ end
+
+ let(:other_report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')],
+ scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')],
+ identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')]
+ )
+ end
+
+ before do
+ report.replace_with!(other_report)
+ end
+
+ it 'replaces report contents with other reports contents' do
+ expect(report.findings).to eq(other_report.findings)
+ expect(report.scanners).to eq(other_report.scanners)
+ expect(report.identifiers).to eq(other_report.identifiers)
+ end
+ end
+
+ describe '#merge!' do
+ let(:merged_report) { double('Report') }
+
+ before do
+ merge_reports_service = double('MergeReportsService')
+
+ allow(::Security::MergeReportsService).to receive(:new).and_return(merge_reports_service)
+ allow(merge_reports_service).to receive(:execute).and_return(merged_report)
+ allow(report).to receive(:replace_with!)
+ end
+
+ subject { report.merge!(described_class.new('sast', pipeline, created_at)) }
+
+ it 'invokes the merge with other report and then replaces this report contents by merge result' do
+ subject
+
+ expect(report).to have_received(:replace_with!).with(merged_report)
+ end
+ end
+
+ describe '#primary_scanner' do
+ let(:scanner_1) { create(:ci_reports_security_scanner, external_id: 'external_id_1') }
+ let(:scanner_2) { create(:ci_reports_security_scanner, external_id: 'external_id_2') }
+
+ subject { report.primary_scanner }
+
+ before do
+ report.add_scanner(scanner_1)
+ report.add_scanner(scanner_2)
+ end
+
+ it { is_expected.to eq(scanner_1) }
+ end
+
+ describe '#add_error' do
+ context 'when the message is not given' do
+ it 'adds a new error to report with the generic error message' do
+ expect { report.add_error('foo') }.to change { report.errors }
+ .from([])
+ .to([{ type: 'foo', message: 'An unexpected error happened!' }])
+ end
+ end
+
+ context 'when the message is given' do
+ it 'adds a new error to report' do
+ expect { report.add_error('foo', 'bar') }.to change { report.errors }
+ .from([])
+ .to([{ type: 'foo', message: 'bar' }])
+ end
+ end
+ end
+
+ describe 'errored?' do
+ subject { report.errored? }
+
+ context 'when the report does not have any errors' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the report has errors' do
+ before do
+ report.add_error('foo', 'bar')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#primary_scanner_order_to' do
+ let(:scanner_1) { build(:ci_reports_security_scanner) }
+ let(:scanner_2) { build(:ci_reports_security_scanner) }
+ let(:report_1) { described_class.new('sast', pipeline, created_at) }
+ let(:report_2) { described_class.new('sast', pipeline, created_at) }
+
+ subject(:compare_based_on_primary_scanners) { report_1.primary_scanner_order_to(report_2) }
+
+ context 'when the primary scanner of the receiver is nil' do
+ context 'when the primary scanner of the other is nil' do
+ it { is_expected.to be(1) }
+ end
+
+ context 'when the primary scanner of the other is not nil' do
+ before do
+ report_2.add_scanner(scanner_2)
+ end
+
+ it { is_expected.to be(1) }
+ end
+ end
+
+ context 'when the primary scanner of the receiver is not nil' do
+ before do
+ report_1.add_scanner(scanner_1)
+ end
+
+ context 'when the primary scanner of the other is nil' do
+ let(:scanner_2) { nil }
+
+ it { is_expected.to be(-1) }
+ end
+
+ context 'when the primary scanner of the other is not nil' do
+ before do
+ report_2.add_scanner(scanner_2)
+
+ allow(scanner_1).to receive(:<=>).and_return(0)
+ end
+
+ it 'compares two scanners' do
+ expect(compare_based_on_primary_scanners).to be(0)
+ expect(scanner_1).to have_received(:<=>).with(scanner_2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
new file mode 100644
index 00000000000..9b1e02f1418
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Reports do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:artifact) { create(:ci_job_artifact, :sast) }
+
+ let(:security_reports) { described_class.new(pipeline) }
+
+ describe '#get_report' do
+ subject { security_reports.get_report(report_type, artifact) }
+
+ context 'when report type is sast' do
+ let(:report_type) { 'sast' }
+
+ it { expect(subject.type).to eq('sast') }
+ it { expect(subject.created_at).to eq(artifact.created_at) }
+
+ it 'initializes a new report and returns it' do
+ expect(Gitlab::Ci::Reports::Security::Report).to receive(:new)
+ .with('sast', pipeline, artifact.created_at).and_call_original
+
+ is_expected.to be_a(Gitlab::Ci::Reports::Security::Report)
+ end
+
+ context 'when report type is already allocated' do
+ before do
+ subject
+ end
+
+ it 'does not initialize a new report' do
+ expect(Gitlab::Ci::Reports::Security::Report).not_to receive(:new)
+
+ is_expected.to be_a(Gitlab::Ci::Reports::Security::Report)
+ end
+ end
+ end
+ end
+
+ describe '#findings' do
+ let(:finding_1) { build(:ci_reports_security_finding, severity: 'low') }
+ let(:finding_2) { build(:ci_reports_security_finding, severity: 'high') }
+ let!(:expected_findings) { [finding_1, finding_2] }
+
+ subject { security_reports.findings }
+
+ before do
+ security_reports.get_report('sast', artifact).add_finding(finding_1)
+ security_reports.get_report('dependency_scanning', artifact).add_finding(finding_2)
+ end
+
+ it { is_expected.to match_array(expected_findings) }
+ end
+
+ describe "#violates_default_policy_against?" do
+ let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
+ let(:vulnerabilities_allowed) { 0 }
+ let(:severity_levels) { %w(critical high) }
+
+ subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) }
+
+ before do
+ security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
+ end
+
+ context 'when the target_reports is `nil`' do
+ let(:target_reports) { nil }
+
+ context 'with severity levels matching the existing vulnerabilities' do
+ it { is_expected.to be(true) }
+ end
+
+ context "without any severity levels matching the existing vulnerabilities" do
+ let(:severity_levels) { %w(critical) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'when the target_reports is not `nil`' do
+ let(:target_reports) { described_class.new(pipeline) }
+
+ context "when a report has a new unsafe vulnerability" do
+ context 'with severity levels matching the existing vulnerabilities' do
+ it { is_expected.to be(true) }
+ end
+
+ it { is_expected.to be(true) }
+
+ context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do
+ let(:vulnerabilities_allowed) { 10000 }
+
+ it { is_expected.to be(false) }
+ end
+
+ context "without any severity levels matching the existing vulnerabilities" do
+ let(:severity_levels) { %w(critical) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context "when none of the reports have a new unsafe vulnerability" do
+ before do
+ target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
new file mode 100644
index 00000000000..44e66fd9028
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
+ let(:identifier) { build(:ci_reports_security_identifier) }
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:location_param) { build(:ci_reports_security_locations_sast, :dynamic) }
+ let(:vulnerability_params) { vuln_params(project.id, [identifier], confidence: :low, severity: :critical) }
+ let(:base_vulnerability) { build(:ci_reports_security_finding, location: location_param, **vulnerability_params) }
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability]) }
+
+ let(:head_vulnerability) { build(:ci_reports_security_finding, location: location_param, uuid: base_vulnerability.uuid, **vulnerability_params) }
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability]) }
+
+ shared_context 'comparing reports' do
+ let(:vul_params) { vuln_params(project.id, [identifier]) }
+ let(:base_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
+ let(:head_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
+ let(:head_vul_findings) { [head_vulnerability, vuln] }
+ end
+
+ subject { described_class.new(project, base_report, head_report) }
+
+ where(vulnerability_finding_signatures: [true, false])
+
+ with_them do
+ before do
+ stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_signatures)
+ end
+
+ describe '#base_report_out_of_date' do
+ context 'no base report' do
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be false
+ end
+ end
+
+ context 'base report older than one week' do
+ let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) }
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be true
+ end
+ end
+
+ context 'base report less than one week old' do
+ let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) }
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be false
+ end
+ end
+ end
+
+ describe '#added' do
+ let(:new_location) {build(:ci_reports_security_locations_sast, :dynamic) }
+ let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) }
+ let(:vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:critical], location: new_location, **vul_params) }
+ let(:low_vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:low], location: new_location, **vul_params) }
+
+ context 'with new vulnerability' do
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln]) }
+
+ it 'points to source tree' do
+ expect(subject.added).to eq([vuln])
+ end
+ end
+
+ context 'when comparing reports with different fingerprints' do
+ include_context 'comparing reports'
+
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings) }
+
+ it 'does not find any overlap' do
+ expect(subject.added).to eq(head_vul_findings)
+ end
+ end
+
+ context 'order' do
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln]) }
+
+ it 'does not change' do
+ expect(subject.added).to eq([vuln, low_vuln])
+ end
+ end
+ end
+
+ describe '#fixed' do
+ let(:vul_params) { vuln_params(project.id, [identifier]) }
+ let(:vuln) { build(:ci_reports_security_finding, :dynamic, **vul_params ) }
+ let(:medium_vuln) { build(:ci_reports_security_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) }
+
+ context 'with fixed vulnerability' do
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
+
+ it 'points to base tree' do
+ expect(subject.fixed).to eq([vuln])
+ end
+ end
+
+ context 'when comparing reports with different fingerprints' do
+ include_context 'comparing reports'
+
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
+
+ it 'does not find any overlap' do
+ expect(subject.fixed).to eq([base_vulnerability, vuln])
+ end
+ end
+
+ context 'order' do
+ let(:vul_findings) { [vuln, medium_vuln] }
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability]) }
+
+ it 'does not change' do
+ expect(subject.fixed).to eq(vul_findings)
+ end
+ end
+ end
+
+ describe 'with empty vulnerabilities' do
+ let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
+
+ it 'returns empty array when reports are not present' do
+ comparer = described_class.new(project, empty_report, empty_report)
+
+ expect(comparer.fixed).to eq([])
+ expect(comparer.added).to eq([])
+ end
+
+ it 'returns added vulnerability when base is empty and head is not empty' do
+ comparer = described_class.new(project, empty_report, head_report)
+
+ expect(comparer.fixed).to eq([])
+ expect(comparer.added).to eq([head_vulnerability])
+ end
+
+ it 'returns fixed vulnerability when head is empty and base is not empty' do
+ comparer = described_class.new(project, base_report, empty_report)
+
+ expect(comparer.fixed).to eq([base_vulnerability])
+ expect(comparer.added).to eq([])
+ end
+ end
+ end
+
+ def vuln_params(project_id, identifiers, confidence: :high, severity: :critical)
+ {
+ project_id: project_id,
+ report_type: :sast,
+ identifiers: identifiers,
+ confidence: ::Enums::Vulnerability.confidence_levels[confidence],
+ severity: ::Enums::Vulnerability.severity_levels[severity]
+ }
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
index 6bc8e261640..f8df2266689 100644
--- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
index e8aeb93a2ba..ca6f6872f89 100644
--- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
let(:platform_target) { 'ECS' }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
index 053499344e1..bd701aec8fc 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
context 'on merge request' do
let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request) }
+ let(:pipeline) { service.execute(merge_request).payload }
it 'has no jobs' do
expect(pipeline).to be_merge_request_event
diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
index b23457315cc..64243f2d205 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
context 'on merge request' do
let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request) }
+ let(:pipeline) { service.execute(merge_request).payload }
it 'has no jobs' do
expect(pipeline).to be_merge_request_event
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index 1d137ef89e1..d377cf0c735 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -210,7 +210,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
context 'on merge request' do
let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request) }
+ let(:pipeline) { service.execute(merge_request).payload }
it 'has no jobs' do
expect(pipeline).to be_merge_request_event
diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
index 7fa8d906d07..db9d7496251 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
context 'on merge request' do
let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request) }
+ let(:pipeline) { service.execute(merge_request).payload }
it 'has no jobs' do
expect(pipeline).to be_merge_request_event
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
index 0811c07e896..4685d843ce0 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
- subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') }
+RSpec.describe 'Terraform/Base.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base') }
describe 'the created pipeline' do
let(:default_branch) { 'master' }
@@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..e35f2eabe8e
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { 'master' }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ it 'does not create any jobs' do
+ expect(build_names).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
index e53d2f4f975..004261bc617 100644
--- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -64,7 +64,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do
context 'on merge request' do
let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request) }
+ let(:pipeline) { service.execute(merge_request).payload }
it 'has no jobs' do
expect(pipeline).to be_merge_request_event
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index b40b4f5645f..7602309627b 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -264,7 +264,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: files) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: default_branch ) }
- let(:pipeline) { service.execute(:push) }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
index 4e5fe622648..3d97b47473d 100644
--- a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Flutter.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb
index 151880e27a3..14aaf717453 100644
--- a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Managed-Cluster-Applications.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, namespace: user.namespace, files: { 'README.md' => '' }) }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb
index 2456c9ae545..ea954690133 100644
--- a/spec/lib/gitlab/ci/templates/npm_spec.rb
+++ b/spec/lib/gitlab/ci/templates/npm_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'npm.gitlab-ci.yml' do
let(:pipeline_tag) { 'v1.2.1' }
let(:pipeline_ref) { pipeline_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
def create_branch(name:)
diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..936cd6ac8aa
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform.gitlab-ci.yml' do
+ before do
+ allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([])
+ end
+
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform') }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { project.default_branch_or_main }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master branch' do
+ it 'creates init, validate and build jobs', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).to include('init', 'validate', 'build', 'deploy')
+ end
+ end
+
+ context 'outside the master branch' do
+ let(:pipeline_branch) { 'patch-1' }
+
+ before do
+ project.repository.create_branch(pipeline_branch, default_branch)
+ end
+
+ it 'does not creates a deploy and a test job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).not_to include('deploy')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
index 5ab3035486f..3d1306e82a5 100644
--- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push) }
+ let(:pipeline) { service.execute!(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -25,7 +25,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
end
context 'on master branch' do
- it 'creates init, validate and build jobs' do
+ it 'creates init, validate and build jobs', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
expect(build_names).to include('init', 'validate', 'build', 'deploy')
end
end
@@ -37,7 +38,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
project.repository.create_branch(pipeline_branch, default_branch)
end
- it 'does not creates a deploy and a test job' do
+ it 'does not creates a deploy and a test job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
expect(build_names).not_to include('deploy')
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 19c2e34a0f0..49a470f9e01 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -42,7 +42,6 @@ module Gitlab
interruptible: true,
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -75,7 +74,6 @@ module Gitlab
],
allow_failure: false,
when: 'on_success',
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -115,7 +113,6 @@ module Gitlab
tag_list: %w[A B],
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true
})
@@ -163,7 +160,6 @@ module Gitlab
interruptible: true,
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -354,7 +350,6 @@ module Gitlab
name: "rspec",
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage,
@@ -368,7 +363,6 @@ module Gitlab
name: "prod",
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage,
@@ -847,7 +841,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -882,7 +875,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -913,7 +905,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -942,7 +933,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -955,7 +945,6 @@ module Gitlab
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
let(:build) { subject.builds.first }
- let(:yaml_variables) { build[:yaml_variables] }
let(:job_variables) { build[:job_variables] }
let(:root_variables_inheritance) { build[:root_variables_inheritance] }
@@ -973,84 +962,11 @@ module Gitlab
end
it 'returns global variables' do
- expect(yaml_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
expect(job_variables).to eq([])
expect(root_variables_inheritance).to eq(true)
end
end
- context 'when job and global variables are defined' do
- let(:global_variables) do
- { 'VAR1' => 'global1', 'VAR3' => 'global3', 'VAR4' => 'global4' }
- end
-
- let(:build_variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
-
- let(:config) do
- {
- before_script: ['pwd'],
- variables: global_variables,
- rspec: { script: 'rspec', variables: build_variables, inherit: inherit }
- }
- end
-
- context 'when no inheritance is specified' do
- let(:inherit) { }
-
- it 'returns all variables' do
- expect(yaml_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true },
- { key: 'VAR3', value: 'global3', public: true },
- { key: 'VAR4', value: 'global4', public: true }
- )
- expect(job_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- expect(root_variables_inheritance).to eq(true)
- end
- end
-
- context 'when inheritance is disabled' do
- let(:inherit) { { variables: false } }
-
- it 'does not inherit variables' do
- expect(yaml_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- expect(job_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- expect(root_variables_inheritance).to eq(false)
- end
- end
-
- context 'when specific variables are to inherited' do
- let(:inherit) { { variables: %w[VAR1 VAR4] } }
-
- it 'returns all variables and inherits only specified variables' do
- expect(yaml_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true },
- { key: 'VAR4', value: 'global4', public: true }
- )
- expect(job_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- expect(root_variables_inheritance).to eq(%w[VAR1 VAR4])
- end
- end
- end
-
context 'when job variables are defined' do
let(:config) do
{
@@ -1065,10 +981,6 @@ module Gitlab
end
it 'returns job variables' do
- expect(yaml_variables).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
expect(job_variables).to contain_exactly(
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
@@ -1096,9 +1008,6 @@ module Gitlab
# When variables config is empty, we assume this is a valid
# configuration, see issue #18775
#
- expect(yaml_variables).to be_an_instance_of(Array)
- expect(yaml_variables).to be_empty
-
expect(job_variables).to eq([])
expect(root_variables_inheritance).to eq(true)
end
@@ -1115,9 +1024,6 @@ module Gitlab
end
it 'returns empty array' do
- expect(yaml_variables).to be_an_instance_of(Array)
- expect(yaml_variables).to be_empty
-
expect(job_variables).to eq([])
expect(root_variables_inheritance).to eq(true)
end
@@ -1246,6 +1152,10 @@ module Gitlab
end
it { is_expected.to be_valid }
+
+ it 'adds the job from the included file' do
+ expect(subject.builds.map { |build| build[:name] }).to contain_exactly('job1', 'rspec')
+ end
end
context "when the included internal file is not present" do
@@ -1349,7 +1259,7 @@ module Gitlab
end
it 'sets matrix variables' do
- build_variables = builds.map { |build| build[:yaml_variables] }
+ build_variables = builds.map { |build| build[:job_variables] }
expected_variables = [
[
{ key: 'VAR1', value: '1' },
@@ -1601,7 +1511,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -1972,7 +1881,6 @@ module Gitlab
},
when: 'on_success',
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -1988,7 +1896,6 @@ module Gitlab
],
when: 'on_success',
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
@@ -2011,7 +1918,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -2028,7 +1934,6 @@ module Gitlab
],
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
@@ -2057,7 +1962,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -2076,7 +1980,6 @@ module Gitlab
],
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
@@ -2101,7 +2004,6 @@ module Gitlab
],
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
@@ -2134,7 +2036,6 @@ module Gitlab
],
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
@@ -2342,7 +2243,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -2391,7 +2291,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -2406,7 +2305,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
@@ -2851,7 +2749,7 @@ module Gitlab
YAML
end
- it_behaves_like 'returns errors', 'The pipeline has circular dependencies.'
+ it_behaves_like 'returns errors', 'The pipeline has circular dependencies'
end
end
@@ -2883,7 +2781,7 @@ module Gitlab
expect(subject.valid?).to eq(false)
expect(subject.errors).to contain_exactly(
'jobs:rspec config contains unknown keys: bad_tags',
- 'jobs:rspec rules should be an array of hashes')
+ 'jobs:rspec rules should be an array containing hashes and arrays of hashes')
end
end
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 85bafc77553..5a4e9001ac9 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
context 'when database meets minimum supported version' do
before do
- allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ allow(Gitlab::Database.main).to receive(:postgresql_minimum_supported_version?).and_return(true)
end
it { is_expected.to be_empty }
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
context 'when database does not meet minimum supported version' do
before do
- allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(Gitlab::Database.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
end
let(:notice_deprecated_database) do
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
'%{pg_version_minimum} is required for this version of GitLab. ' \
'Please upgrade your environment to a supported PostgreSQL version, ' \
'see %{pg_requirements_url} for details.') % {
- pg_version_current: Gitlab::Database.version,
+ pg_version_current: Gitlab::Database.main.version,
pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
pg_requirements_url: '<a href="https://docs.gitlab.com/ee/install/requirements.html#database">database requirements</a>'
}
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index f8a007cdd75..aac4936b20e 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -17,6 +17,18 @@ RSpec.describe Gitlab::Conflict::File do
let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) }
let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
+ describe 'delegates' do
+ it { expect(conflict_file).to delegate_method(:type).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:content).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:path).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:ancestor_path).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:their_path).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:our_path).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:our_mode).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:our_blob).to(:raw) }
+ it { expect(conflict_file).to delegate_method(:repository).to(:raw) }
+ end
+
describe '#resolve_lines' do
let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
@@ -324,4 +336,27 @@ RSpec.describe Gitlab::Conflict::File do
end
end
end
+
+ describe '#conflict_type' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:rugged_conflict) { { ancestor: { path: ancestor_path }, theirs: { path: their_path }, ours: { path: our_path } } }
+ let(:diff_file) { double(renamed_file?: renamed_file?) }
+
+ subject(:conflict_type) { conflict_file.conflict_type(diff_file) }
+
+ where(:ancestor_path, :their_path, :our_path, :renamed_file?, :result) do
+ '/ancestor/path' | '/their/path' | '/our/path' | false | :both_modified
+ '/ancestor/path' | '' | '/our/path' | false | :modified_source_removed_target
+ '/ancestor/path' | '/their/path' | '' | false | :modified_target_removed_source
+ '' | '/their/path' | '/our/path' | false | :both_added
+ '' | '' | '/our/path' | false | :removed_target_renamed_source
+ '' | '' | '/our/path' | true | :renamed_same_file
+ '' | '/their/path' | '' | false | :removed_source_renamed_target
+ end
+
+ with_them do
+ it { expect(conflict_type).to eq(result) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 8e63e771caa..239eff11bf3 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -19,14 +19,28 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
}
end
- describe '.default_settings_hash' do
- let(:settings) { described_class.default_settings_hash }
+ describe '.default_enabled' do
+ let(:enabled) { described_class.default_enabled }
- it 'returns defaults for all keys' do
- expect(settings['enabled']).to be_truthy
- expect(settings['report_only']).to be_falsey
+ it 'is enabled' do
+ expect(enabled).to be_truthy
+ end
+
+ context 'when in production' do
+ before do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ end
+
+ it 'is disabled' do
+ expect(enabled).to be_falsey
+ end
+ end
+ end
+
+ describe '.default_directives' do
+ let(:directives) { described_class.default_directives }
- directives = settings['directives']
+ it 'returns default directives' do
directive_names = (described_class::DIRECTIVES - ['report_uri'])
directive_names.each do |directive|
expect(directives.has_key?(directive)).to be_truthy
@@ -38,27 +52,25 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['child_src']).to eq(directives['frame_src'])
end
- context 'when in production' do
+ context 'when CDN host is defined' do
before do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ stub_config_setting(cdn_host: 'https://example.com')
end
- it 'is disabled' do
- expect(settings['enabled']).to be_falsey
+ it 'adds CDN host to CSP' do
+ expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com https://example.com")
+ expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
+ expect(directives['font_src']).to eq("'self' https://example.com")
end
end
- context 'when GITLAB_CDN_HOST is set' do
+ context 'when sentry is configured' do
before do
- stub_env('GITLAB_CDN_HOST', 'https://example.com')
+ stub_sentry_settings
end
- it 'adds GITLAB_CDN_HOST to CSP' do
- directives = settings['directives']
-
- expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com https://example.com")
- expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
- expect(directives['font_src']).to eq("'self' https://example.com")
+ it 'adds sentry path to CSP without user' do
+ expect(directives['connect_src']).to eq("'self' dummy://example.com/43")
end
end
@@ -73,8 +85,6 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'does not add CUSTOMER_PORTAL_URL to CSP' do
- directives = settings['directives']
-
expect(directives['frame_src']).to eq("'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com")
end
end
@@ -85,8 +95,6 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
- directives = settings['directives']
-
expect(directives['frame_src']).to eq("'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://customers.example.com")
end
end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index d64dfc957ca..75741c52579 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:status]).to eq('failed')
expect(data[:status_changed_at]).to eq(status_changed_at)
+ expect(data[:deployment_id]).to eq(deployment.id)
expect(data[:deployable_id]).to eq(deployable.id)
expect(data[:deployable_url]).to eq(expected_deployable_url)
expect(data[:environment]).to eq("somewhere")
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index c05a044f0de..0e574c7aa84 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Pipeline do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
- let(:pipeline) do
+ let_it_be_with_reload(:pipeline) do
create(:ci_pipeline,
project: project,
status: 'success',
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
describe '.build' do
let(:data) { described_class.build(pipeline) }
let(:attributes) { data[:object_attributes] }
- let(:build_data) { data[:builds].first }
+ let(:build_data) { data[:builds].last }
let(:runner_data) { build_data[:runner] }
let(:project_data) { data[:project] }
@@ -51,9 +51,9 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
end
context 'build with runner' do
- let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
- let!(:tag_names) { %w(tag-1 tag-2) }
- let(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
+ let_it_be(:tag_names) { %w(tag-1 tag-2) }
+ let_it_be(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
it 'has runner attributes', :aggregate_failures do
expect(runner_data[:id]).to eq(ci_runner.id)
@@ -73,18 +73,15 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
end
context 'pipeline with variables' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { described_class.build(pipeline) }
- let(:attributes) { data[:object_attributes] }
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1') }
+ let_it_be(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1') }
it { expect(attributes[:variables]).to be_a(Array) }
it { expect(attributes[:variables]).to contain_exactly({ key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1' }) }
end
context 'when pipeline is a detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.all_pipelines.first }
+ let_it_be(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let_it_be(:pipeline) { merge_request.all_pipelines.first }
it 'returns a source ref' do
expect(attributes[:ref]).to eq(merge_request.source_branch)
@@ -108,21 +105,67 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
end
context 'when pipeline has retried builds' do
- before do
- create(:ci_build, :retried, pipeline: pipeline)
- end
+ let_it_be(:retried_build) { create(:ci_build, :retried, pipeline: pipeline) }
it 'does not contain retried builds in payload' do
- expect(data[:builds].count).to eq(1)
- expect(build_data[:id]).to eq(build.id)
+ builds = data[:builds]
+
+ expect(builds.pluck(:id)).to contain_exactly(build.id)
+ end
+
+ it 'contains retried builds if requested' do
+ builds = data.with_retried_builds[:builds]
+
+ expect(builds.pluck(:id)).to contain_exactly(build.id, retried_build.id)
end
end
context 'build with environment' do
- let!(:build) { create(:ci_build, :teardown_environment, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, :environment_with_deployment_tier, :with_deployment, pipeline: pipeline) }
+
+ let(:build_environment_data) { build_data[:environment] }
+
+ it 'has environment attributes', :aggregate_failures do
+ expect(build_environment_data[:name]).to eq(build.expanded_environment_name)
+ expect(build_environment_data[:action]).to eq(build.environment_action)
+ expect(build_environment_data[:deployment_tier]).to eq(build.persisted_environment.try(:tier))
+ end
+ end
- it { expect(build_data[:environment][:name]).to eq(build.expanded_environment_name) }
- it { expect(build_data[:environment][:action]).to eq(build.environment_action) }
+ context 'avoids N+1 database queries' do
+ it "with multiple builds" do
+ # Preparing the pipeline with the minimal builds
+ pipeline = create(:ci_pipeline, user: user, project: project)
+ create(:ci_build, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :deploy_to_production, :with_deployment, user: user, project: project, pipeline: pipeline)
+
+ # We need `.to_json` as the build hook data is wrapped within `Gitlab::Lazy`
+ control_count = ActiveRecord::QueryRecorder.new { described_class.build(pipeline.reload).to_json }.count
+
+ # Adding more builds to the pipeline and serializing the data again
+ create_list(:ci_build, 3, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :start_review_app, :with_deployment, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :stop_review_app, :with_deployment, user: user, project: project, pipeline: pipeline)
+
+ expect { described_class.build(pipeline.reload).to_json }.not_to exceed_query_limit(control_count)
+ end
+
+ it "with multiple retried builds" do
+ # Preparing the pipeline with the minimal builds
+ pipeline = create(:ci_pipeline, user: user, project: project)
+ create(:ci_build, :retried, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :deploy_to_production, :retried, :with_deployment, user: user, project: project, pipeline: pipeline)
+
+ # We need `.to_json` as the build hook data is wrapped within `Gitlab::Lazy`
+ control_count = ActiveRecord::QueryRecorder.new { described_class.build(pipeline.reload).with_retried_builds.to_json }.count
+
+ # Adding more builds to the pipeline and serializing the data again
+ create_list(:ci_build, 3, :retried, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :start_review_app, :retried, :with_deployment, user: user, project: project, pipeline: pipeline)
+ create(:ci_build, :stop_review_app, :retried, :with_deployment, user: user, project: project, pipeline: pipeline)
+
+ expect { described_class.build(pipeline.reload).with_retried_builds.to_json }.not_to exceed_query_limit(control_count)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
new file mode 100644
index 00000000000..b4010d0fe8d
--- /dev/null
+++ b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
+ describe '#perform' do
+ subject { described_class.new(async_index) }
+
+ let(:async_index) { create(:postgres_async_index) }
+
+ let(:index_model) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
+
+ let(:connection) { ApplicationRecord.connection }
+
+ context 'when the index already exists' do
+ before do
+ connection.execute(async_index.definition)
+ end
+
+ it 'skips index creation' do
+ expect(connection).not_to receive(:execute).with(/CREATE INDEX/)
+
+ subject.perform
+ end
+ end
+
+ it 'creates the index while controlling statement timeout' do
+ allow(connection).to receive(:execute).and_call_original
+ expect(connection).to receive(:execute).with("SET statement_timeout TO '32400s'").ordered.and_call_original
+ expect(connection).to receive(:execute).with(async_index.definition).ordered.and_call_original
+ expect(connection).to receive(:execute).with("RESET statement_timeout").ordered.and_call_original
+
+ subject.perform
+ end
+
+ it 'removes the index preparation record from postgres_async_indexes' do
+ expect(async_index).to receive(:destroy).and_call_original
+
+ expect { subject.perform }.to change { index_model.count }.by(-1)
+ end
+
+ it 'skips logic if not able to acquire exclusive lease' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ expect(connection).not_to receive(:execute).with(/CREATE INDEX/)
+ expect(async_index).not_to receive(:destroy)
+
+ expect { subject.perform }.not_to change { index_model.count }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
new file mode 100644
index 00000000000..ed15951dfb0
--- /dev/null
+++ b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncIndexes::MigrationHelpers do
+ let(:migration) { ActiveRecord::Migration.new.extend(described_class) }
+ let(:index_model) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
+ let(:connection) { ApplicationRecord.connection }
+ let(:table_name) { '_test_async_indexes' }
+ let(:index_name) { "index_#{table_name}_on_id" }
+
+ before do
+ allow(migration).to receive(:puts)
+ end
+
+ describe '#unprepare_async_index' do
+ let!(:async_index) { create(:postgres_async_index, name: index_name) }
+
+ context 'when the flag is enabled' do
+ before do
+ stub_feature_flags(database_async_index_creation: true)
+ end
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_index(table_name, 'id')
+ end.to change { index_model.where(name: index_name).count }.by(-1)
+ end
+
+ context 'when an explicit name is given' do
+ let(:index_name) { 'my_test_async_index' }
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_index(table_name, 'id', name: index_name)
+ end.to change { index_model.where(name: index_name).count }.by(-1)
+ end
+ end
+
+ context 'when the async index table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(:postgres_async_indexes)
+
+ expect(index_model).not_to receive(:find_by)
+
+ expect { migration.unprepare_async_index(table_name, 'id') }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ it 'does not destroy the record' do
+ stub_feature_flags(database_async_index_creation: false)
+
+ expect do
+ migration.unprepare_async_index(table_name, 'id')
+ end.not_to change { index_model.where(name: index_name).count }
+ end
+ end
+ end
+
+ describe '#unprepare_async_index_by_name' do
+ let(:index_name) { "index_#{table_name}_on_id" }
+ let!(:async_index) { create(:postgres_async_index, name: index_name) }
+
+ context 'when the flag is enabled' do
+ before do
+ stub_feature_flags(database_async_index_creation: true)
+ end
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_index_by_name(table_name, index_name)
+ end.to change { index_model.where(name: index_name).count }.by(-1)
+ end
+
+ context 'when the async index table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(:postgres_async_indexes)
+
+ expect(index_model).not_to receive(:find_by)
+
+ expect { migration.unprepare_async_index_by_name(table_name, index_name) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ it 'does not destroy the record' do
+ stub_feature_flags(database_async_index_creation: false)
+
+ expect do
+ migration.unprepare_async_index_by_name(table_name, index_name)
+ end.not_to change { index_model.where(name: index_name).count }
+ end
+ end
+ end
+
+ describe '#prepare_async_index' do
+ before do
+ connection.create_table(table_name)
+ end
+
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(database_async_index_creation: true)
+ end
+
+ it 'creates the record for the async index' do
+ expect do
+ migration.prepare_async_index(table_name, 'id')
+ end.to change { index_model.where(name: index_name).count }.by(1)
+
+ record = index_model.find_by(name: index_name)
+
+ expect(record.table_name).to eq(table_name)
+ expect(record.definition).to match(/CREATE INDEX CONCURRENTLY "#{index_name}"/)
+ end
+
+ context 'when an explicit name is given' do
+ let(:index_name) { 'my_async_index_name' }
+
+ it 'creates the record with the given name' do
+ expect do
+ migration.prepare_async_index(table_name, 'id', name: index_name)
+ end.to change { index_model.where(name: index_name).count }.by(1)
+
+ record = index_model.find_by(name: index_name)
+
+ expect(record.table_name).to eq(table_name)
+ expect(record.definition).to match(/CREATE INDEX CONCURRENTLY "#{index_name}"/)
+ end
+ end
+
+ context 'when the index already exists' do
+ it 'does not create the record' do
+ connection.add_index(table_name, 'id', name: index_name)
+
+ expect do
+ migration.prepare_async_index(table_name, 'id')
+ end.not_to change { index_model.where(name: index_name).count }
+ end
+ end
+
+ context 'when the record already exists' do
+ it 'does attempt to create the record' do
+ create(:postgres_async_index, table_name: table_name, name: index_name)
+
+ expect do
+ migration.prepare_async_index(table_name, 'id')
+ end.not_to change { index_model.where(name: index_name).count }
+ end
+ end
+
+ context 'when the async index table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(:postgres_async_indexes)
+
+ expect(index_model).not_to receive(:safe_find_or_create_by!)
+
+ expect { migration.prepare_async_index(table_name, 'id') }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ it 'does not create the record' do
+ stub_feature_flags(database_async_index_creation: false)
+
+ expect do
+ migration.prepare_async_index(table_name, 'id')
+ end.not_to change { index_model.where(name: index_name).count }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
new file mode 100644
index 00000000000..434cba4edde
--- /dev/null
+++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model do
+ describe 'validations' do
+ let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
+ let(:definition_limit) { described_class::MAX_DEFINITION_LENGTH }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_presence_of(:table_name) }
+ it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_presence_of(:definition) }
+ it { is_expected.to validate_length_of(:definition).is_at_most(definition_limit) }
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes_spec.rb b/spec/lib/gitlab/database/async_indexes_spec.rb
new file mode 100644
index 00000000000..74e30ea2c4e
--- /dev/null
+++ b/spec/lib/gitlab/database/async_indexes_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncIndexes do
+ describe '.create_pending_indexes!' do
+ subject { described_class.create_pending_indexes! }
+
+ before do
+ create_list(:postgres_async_index, 4)
+ end
+
+ it 'takes 2 pending indexes and creates those' do
+ Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.order(:id).limit(2).each do |index|
+ creator = double('index creator')
+ expect(Gitlab::Database::AsyncIndexes::IndexCreator).to receive(:new).with(index).and_return(creator)
+ expect(creator).to receive(:perform)
+ end
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/connection_spec.rb b/spec/lib/gitlab/database/connection_spec.rb
new file mode 100644
index 00000000000..5e0e6039afc
--- /dev/null
+++ b/spec/lib/gitlab/database/connection_spec.rb
@@ -0,0 +1,467 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Connection do
+ let(:connection) { described_class.new }
+
+ describe '#default_pool_size' do
+ before do
+ allow(Gitlab::Runtime).to receive(:max_threads).and_return(7)
+ end
+
+ it 'returns the max thread size plus a fixed headroom of 10' do
+ expect(connection.default_pool_size).to eq(17)
+ end
+
+ it 'returns the max thread size plus a DB_POOL_HEADROOM if this env var is present' do
+ stub_env('DB_POOL_HEADROOM', '7')
+
+ expect(connection.default_pool_size).to eq(14)
+ end
+ end
+
+ describe '#config' do
+ it 'returns a HashWithIndifferentAccess' do
+ expect(connection.config).to be_an_instance_of(HashWithIndifferentAccess)
+ end
+
+ it 'returns a default pool size' do
+ expect(connection.config).to include(pool: connection.default_pool_size)
+ end
+
+ it 'does not cache its results' do
+ a = connection.config
+ b = connection.config
+
+ expect(a).not_to equal(b)
+ end
+ end
+
+ describe '#pool_size' do
+ context 'when no explicit size is configured' do
+ it 'returns the default pool size' do
+ expect(connection).to receive(:config).and_return({ pool: nil })
+
+ expect(connection.pool_size).to eq(connection.default_pool_size)
+ end
+ end
+
+ context 'when an explicit pool size is set' do
+ it 'returns the pool size' do
+ expect(connection).to receive(:config).and_return({ pool: 4 })
+
+ expect(connection.pool_size).to eq(4)
+ end
+ end
+ end
+
+ describe '#username' do
+ context 'when a username is set' do
+ it 'returns the username' do
+ allow(connection).to receive(:config).and_return(username: 'bob')
+
+ expect(connection.username).to eq('bob')
+ end
+ end
+
+ context 'when a username is not set' do
+ it 'returns the value of the USER environment variable' do
+ allow(connection).to receive(:config).and_return(username: nil)
+ allow(ENV).to receive(:[]).with('USER').and_return('bob')
+
+ expect(connection.username).to eq('bob')
+ end
+ end
+ end
+
+ describe '#database_name' do
+ it 'returns the name of the database' do
+ allow(connection).to receive(:config).and_return(database: 'test')
+
+ expect(connection.database_name).to eq('test')
+ end
+ end
+
+ describe '#adapter_name' do
+ it 'returns the database adapter name' do
+ allow(connection).to receive(:config).and_return(adapter: 'test')
+
+ expect(connection.adapter_name).to eq('test')
+ end
+ end
+
+ describe '#human_adapter_name' do
+ context 'when the adapter is PostgreSQL' do
+ it 'returns PostgreSQL' do
+ allow(connection).to receive(:config).and_return(adapter: 'postgresql')
+
+ expect(connection.human_adapter_name).to eq('PostgreSQL')
+ end
+ end
+
+ context 'when the adapter is not PostgreSQL' do
+ it 'returns Unknown' do
+ allow(connection).to receive(:config).and_return(adapter: 'kittens')
+
+ expect(connection.human_adapter_name).to eq('Unknown')
+ end
+ end
+ end
+
+ describe '#postgresql?' do
+ context 'when using PostgreSQL' do
+ it 'returns true' do
+ allow(connection).to receive(:adapter_name).and_return('PostgreSQL')
+
+ expect(connection.postgresql?).to eq(true)
+ end
+ end
+
+ context 'when not using PostgreSQL' do
+ it 'returns false' do
+ allow(connection).to receive(:adapter_name).and_return('MySQL')
+
+ expect(connection.postgresql?).to eq(false)
+ end
+ end
+ end
+
+ describe '#db_config_with_default_pool_size' do
+ it 'returns db_config with our default pool size' do
+ allow(connection).to receive(:default_pool_size).and_return(9)
+
+ expect(connection.db_config_with_default_pool_size.pool).to eq(9)
+ end
+
+ it 'returns db_config with the correct database name' do
+ db_name = connection.scope.connection.pool.db_config.name
+
+ expect(connection.db_config_with_default_pool_size.name).to eq(db_name)
+ end
+ end
+
+ describe '#disable_prepared_statements' do
+ around do |example|
+ original_config = ::Gitlab::Database.main.config
+
+ example.run
+
+ connection.scope.establish_connection(original_config)
+ end
+
+ it 'disables prepared statements' do
+ connection.scope.establish_connection(
+ ::Gitlab::Database.main.config.merge(prepared_statements: true)
+ )
+
+ expect(connection.scope.connection.prepared_statements).to eq(true)
+
+ connection.disable_prepared_statements
+
+ expect(connection.scope.connection.prepared_statements).to eq(false)
+ end
+
+ context 'with dynamic connection pool size' do
+ before do
+ connection.scope.establish_connection(connection.config.merge(pool: 7))
+ end
+
+ it 'retains the set pool size' do
+ connection.disable_prepared_statements
+
+ expect(connection.scope.connection.prepared_statements).to eq(false)
+ expect(connection.scope.connection.pool.size).to eq(7)
+ end
+ end
+ end
+
+ describe '#db_read_only?' do
+ it 'detects a read-only database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(connection.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read-only database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => true }])
+
+ expect(connection.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read-write database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(connection.db_read_only?).to be_falsey
+ end
+
+ it 'detects a read-write database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => false }])
+
+ expect(connection.db_read_only?).to be_falsey
+ end
+ end
+
+ describe '#db_read_write?' do
+ it 'detects a read-only database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(connection.db_read_write?).to eq(false)
+ end
+
+ it 'detects a read-only database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => true }])
+
+ expect(connection.db_read_write?).to eq(false)
+ end
+
+ it 'detects a read-write database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(connection.db_read_write?).to eq(true)
+ end
+
+ it 'detects a read-write database' do
+ allow(connection.scope.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => false }])
+
+ expect(connection.db_read_write?).to eq(true)
+ end
+ end
+
+ describe '#version' do
+ around do |example|
+ connection.instance_variable_set(:@version, nil)
+ example.run
+ connection.instance_variable_set(:@version, nil)
+ end
+
+ context "on postgresql" do
+ it "extracts the version number" do
+ allow(connection)
+ .to receive(:database_version)
+ .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0")
+
+ expect(connection.version).to eq '9.4.4'
+ end
+ end
+
+ it 'memoizes the result' do
+ count = ActiveRecord::QueryRecorder
+ .new { 2.times { connection.version } }
+ .count
+
+ expect(count).to eq(1)
+ end
+ end
+
+ describe '#postgresql_minimum_supported_version?' do
+ it 'returns false when using PostgreSQL 10' do
+ allow(connection).to receive(:version).and_return('10')
+
+ expect(connection.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns false when using PostgreSQL 11' do
+ allow(connection).to receive(:version).and_return('11')
+
+ expect(connection.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 12' do
+ allow(connection).to receive(:version).and_return('12')
+
+ expect(connection.postgresql_minimum_supported_version?).to eq(true)
+ end
+ end
+
+ describe '#bulk_insert' do
+ before do
+ allow(connection).to receive(:connection).and_return(dummy_connection)
+ allow(dummy_connection).to receive(:quote_column_name, &:itself)
+ allow(dummy_connection).to receive(:quote, &:itself)
+ allow(dummy_connection).to receive(:execute)
+ end
+
+ let(:dummy_connection) { double(:connection) }
+
+ let(:rows) do
+ [
+ { a: 1, b: 2, c: 3 },
+ { c: 6, a: 4, b: 5 }
+ ]
+ end
+
+ it 'does nothing with empty rows' do
+ expect(dummy_connection).not_to receive(:execute)
+
+ connection.bulk_insert('test', [])
+ end
+
+ it 'uses the ordering from the first row' do
+ expect(dummy_connection).to receive(:execute) do |sql|
+ expect(sql).to include('(1, 2, 3)')
+ expect(sql).to include('(4, 5, 6)')
+ end
+
+ connection.bulk_insert('test', rows)
+ end
+
+ it 'quotes column names' do
+ expect(dummy_connection).to receive(:quote_column_name).with(:a)
+ expect(dummy_connection).to receive(:quote_column_name).with(:b)
+ expect(dummy_connection).to receive(:quote_column_name).with(:c)
+
+ connection.bulk_insert('test', rows)
+ end
+
+ it 'quotes values' do
+ 1.upto(6) do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ connection.bulk_insert('test', rows)
+ end
+
+ it 'does not quote values of a column in the disable_quote option' do
+ [1, 2, 4, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ connection.bulk_insert('test', rows, disable_quote: :c)
+ end
+
+ it 'does not quote values of columns in the disable_quote option' do
+ [2, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ connection.bulk_insert('test', rows, disable_quote: [:a, :c])
+ end
+
+ it 'handles non-UTF-8 data' do
+ expect { connection.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
+ end
+
+ context 'when using PostgreSQL' do
+ it 'allows the returning of the IDs of the inserted rows' do
+ result = double(:result, values: [['10']])
+
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/RETURNING id/)
+ .and_return(result)
+
+ ids = connection
+ .bulk_insert('test', [{ number: 10 }], return_ids: true)
+
+ expect(ids).to eq([10])
+ end
+
+ it 'allows setting the upsert to do nothing' do
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/ON CONFLICT DO NOTHING/)
+
+ connection
+ .bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing)
+ end
+ end
+ end
+
+ describe '#cached_column_exists?' do
+ it 'only retrieves data once' do
+ expect(connection.scope.connection)
+ .to receive(:columns)
+ .once.and_call_original
+
+ 2.times do
+ expect(connection.cached_column_exists?(:projects, :id)).to be_truthy
+ expect(connection.cached_column_exists?(:projects, :bogus_column)).to be_falsey
+ end
+ end
+ end
+
+ describe '#cached_table_exists?' do
+ it 'only retrieves data once per table' do
+ expect(connection.scope.connection)
+ .to receive(:data_source_exists?)
+ .with(:projects)
+ .once.and_call_original
+
+ expect(connection.scope.connection)
+ .to receive(:data_source_exists?)
+ .with(:bogus_table_name)
+ .once.and_call_original
+
+ 2.times do
+ expect(connection.cached_table_exists?(:projects)).to be_truthy
+ expect(connection.cached_table_exists?(:bogus_table_name)).to be_falsey
+ end
+ end
+
+ it 'returns false when database does not exist' do
+ expect(connection.scope).to receive(:connection) do
+ raise ActiveRecord::NoDatabaseError, 'broken'
+ end
+
+ expect(connection.cached_table_exists?(:projects)).to be(false)
+ end
+ end
+
+ describe '#exists?' do
+ it 'returns true if `ActiveRecord::Base.connection` succeeds' do
+ expect(connection.scope).to receive(:connection)
+
+ expect(connection.exists?).to be(true)
+ end
+
+ it 'returns false if `ActiveRecord::Base.connection` fails' do
+ expect(connection.scope).to receive(:connection) do
+ raise ActiveRecord::NoDatabaseError, 'broken'
+ end
+
+ expect(connection.exists?).to be(false)
+ end
+ end
+
+ describe '#system_id' do
+ it 'returns the PostgreSQL system identifier' do
+ expect(connection.system_id).to be_an_instance_of(Integer)
+ end
+ end
+
+ describe '#get_write_location' do
+ it 'returns a string' do
+ expect(connection.get_write_location(connection.scope.connection))
+ .to be_a(String)
+ end
+
+ it 'returns nil if there are no results' do
+ expect(connection.get_write_location(double(select_all: []))).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
index 015dd2ba8d2..0ca99ec9acf 100644
--- a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
describe '#select' do
it 'performs a read' do
- expect(proxy).to receive(:read_using_load_balancer).with(:select, ['foo'])
+ expect(proxy).to receive(:read_using_load_balancer).with(:select, 'foo')
proxy.select('foo')
end
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
arel = double(:arel)
expect(proxy).to receive(:read_using_load_balancer)
- .with(:select_all, [arel, 'foo', []])
+ .with(:select_all, arel, 'foo', [])
proxy.select_all(arel, 'foo')
end
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
arel = double(:arel, locked: true)
expect(proxy).to receive(:write_using_load_balancer)
- .with(:select_all, [arel, 'foo', []], sticky: true)
+ .with(:select_all, arel, 'foo', [], sticky: true)
proxy.select_all(arel, 'foo')
end
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
describe "#{name}" do
it 'runs the query on the replica' do
expect(proxy).to receive(:read_using_load_balancer)
- .with(name, ['foo'])
+ .with(name, 'foo')
proxy.send(name, 'foo')
end
@@ -59,7 +59,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
describe "#{name}" do
it 'runs the query on the primary and sticks to it' do
expect(proxy).to receive(:write_using_load_balancer)
- .with(name, ['foo'], sticky: true)
+ .with(name, 'foo', sticky: true)
proxy.send(name, 'foo')
end
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
describe '#method_missing' do
it 'runs the query on the primary without sticking to it' do
expect(proxy).to receive(:write_using_load_balancer)
- .with(:foo, ['foo'])
+ .with(:foo, 'foo')
proxy.foo('foo')
end
@@ -197,7 +197,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(proxy).to receive(:write_using_load_balancer).and_call_original
- expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
+ expect { proxy.case_sensitive_comparison(:table, :attribute, :column, value: :value, format: :format) }
.not_to raise_error
end
@@ -212,7 +212,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
it 'runs the query on the replica' do
- expect(proxy).to receive(:read_using_load_balancer).with(:foo, ['foo'])
+ expect(proxy).to receive(:read_using_load_balancer).with(:foo, 'foo')
proxy.foo('foo')
end
@@ -222,7 +222,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(proxy).to receive(:read_using_load_balancer).and_call_original
- expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
+ expect { proxy.case_sensitive_comparison(:table, :attribute, :column, value: :value, format: :format) }
.not_to raise_error
end
end
@@ -245,7 +245,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
expect(proxy.load_balancer).to receive(:read).and_yield(connection)
- proxy.read_using_load_balancer(:foo, ['foo'])
+ proxy.read_using_load_balancer(:foo, 'foo')
end
end
@@ -257,7 +257,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
expect(proxy.load_balancer).to receive(:read).and_yield(connection)
- proxy.read_using_load_balancer(:foo, ['foo'])
+ proxy.read_using_load_balancer(:foo, 'foo')
end
end
@@ -269,7 +269,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
expect(proxy.load_balancer).to receive(:read).and_yield(connection)
- proxy.read_using_load_balancer(:foo, ['foo'])
+ proxy.read_using_load_balancer(:foo, 'foo')
end
end
@@ -283,7 +283,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(proxy.load_balancer).to receive(:read_write)
.and_yield(connection)
- proxy.read_using_load_balancer(:foo, ['foo'])
+ proxy.read_using_load_balancer(:foo, 'foo')
end
end
end
@@ -302,7 +302,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
expect(session).not_to receive(:write!)
- proxy.write_using_load_balancer(:foo, ['foo'])
+ proxy.write_using_load_balancer(:foo, 'foo')
end
it 'sticks to the primary when sticking is enabled' do
@@ -310,7 +310,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
expect(session).to receive(:write!)
- proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
+ proxy.write_using_load_balancer(:foo, 'foo', sticky: true)
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/host_list_spec.rb b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb
index 873b599f84d..ad4ca18d5e6 100644
--- a/spec/lib/gitlab/database/load_balancing/host_list_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb
@@ -3,25 +3,17 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::HostList do
- def expect_metrics(hosts)
- expect(Gitlab::Metrics.registry.get(:db_load_balancing_hosts).get({})).to eq(hosts)
- end
-
- before do
- allow(Gitlab::Database)
- .to receive(:create_connection_pool)
- .and_return(ActiveRecord::Base.connection_pool)
- end
-
+ let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host }
let(:load_balancer) { double(:load_balancer) }
let(:host_count) { 2 }
+ let(:hosts) { Array.new(host_count) { Gitlab::Database::LoadBalancing::Host.new(db_host, load_balancer, port: 5432) } }
+ let(:host_list) { described_class.new(hosts) }
- let(:host_list) do
- hosts = Array.new(host_count) do
- Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer, port: 5432)
+ before do
+ # each call generate a new replica pool
+ allow(load_balancer).to receive(:create_replica_connection_pool) do
+ double(:replica_connection_pool)
end
-
- described_class.new(hosts)
end
describe '#initialize' do
@@ -42,8 +34,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do
context 'with ports' do
it 'returns the host names of all hosts' do
hosts = [
- ['localhost', 5432],
- ['localhost', 5432]
+ [db_host, 5432],
+ [db_host, 5432]
]
expect(host_list.host_names_and_ports).to eq(hosts)
@@ -51,18 +43,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do
end
context 'without ports' do
- let(:host_list) do
- hosts = Array.new(2) do
- Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
- end
-
- described_class.new(hosts)
- end
+ let(:hosts) { Array.new(2) { Gitlab::Database::LoadBalancing::Host.new(db_host, load_balancer) } }
it 'returns the host names of all hosts' do
hosts = [
- ['localhost', nil],
- ['localhost', nil]
+ [db_host, nil],
+ [db_host, nil]
]
expect(host_list.host_names_and_ports).to eq(hosts)
@@ -70,48 +56,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do
end
end
- describe '#manage_pool?' do
- before do
- allow(Gitlab::Database).to receive(:create_connection_pool) { double(:connection) }
- end
-
- context 'when the testing pool belongs to one host of the host list' do
- it 'returns true' do
- pool = host_list.hosts.first.pool
-
- expect(host_list.manage_pool?(pool)).to be(true)
- end
- end
-
- context 'when the testing pool belongs to a former host of the host list' do
- it 'returns false' do
- pool = host_list.hosts.first.pool
- host_list.hosts = [
- Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
- ]
-
- expect(host_list.manage_pool?(pool)).to be(false)
- end
- end
-
- context 'when the testing pool belongs to a new host of the host list' do
- it 'returns true' do
- host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
- host_list.hosts = [host]
-
- expect(host_list.manage_pool?(host.pool)).to be(true)
- end
- end
-
- context 'when the testing pool does not have any relation with the host list' do
- it 'returns false' do
- host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
-
- expect(host_list.manage_pool?(host.pool)).to be(false)
- end
- end
- end
-
describe '#hosts' do
it 'returns a copy of the host' do
first = host_list.hosts
@@ -185,4 +129,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do
end
end
end
+
+ def expect_metrics(hosts)
+ expect(Gitlab::Metrics.registry.get(:db_load_balancing_hosts).get({})).to eq(hosts)
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb
index 4dfddef68c8..f42ac8be1bb 100644
--- a/spec/lib/gitlab/database/load_balancing/host_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb
@@ -3,15 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Host do
- let(:load_balancer) do
- Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[localhost])
- end
+ let(:load_balancer) { Gitlab::Database::LoadBalancing::LoadBalancer.new }
- let(:host) { load_balancer.host_list.hosts.first }
+ let(:host) do
+ Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
+ end
before do
- allow(Gitlab::Database).to receive(:create_connection_pool)
- .and_return(ActiveRecord::Base.connection_pool)
+ allow(load_balancer).to receive(:create_replica_connection_pool) do
+ ActiveRecord::Base.connection_pool
+ end
end
def raise_and_wrap(wrapper, original)
@@ -63,7 +64,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
expect(host.pool)
.to receive(:disconnect!)
- host.disconnect!(1)
+ host.disconnect!(timeout: 1)
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
index b82b8d9a311..c647f5a8f5d 100644
--- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -3,20 +3,22 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
- let(:pool) { Gitlab::Database.create_connection_pool(2) }
let(:conflict_error) { Class.new(RuntimeError) }
-
- let(:lb) { described_class.new(%w(localhost localhost)) }
+ let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host }
+ let(:lb) { described_class.new([db_host, db_host]) }
+ let(:request_cache) { lb.send(:request_cache) }
before do
- allow(Gitlab::Database).to receive(:create_connection_pool)
- .and_return(pool)
stub_const(
'Gitlab::Database::LoadBalancing::LoadBalancer::PG::TRSerializationFailure',
conflict_error
)
end
+ after do |example|
+ lb.disconnect!(timeout: 0) unless example.metadata[:skip_disconnect]
+ end
+
def raise_and_wrap(wrapper, original)
raise original
rescue original.class
@@ -123,8 +125,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
describe '#read_write' do
it 'yields a connection for a write' do
- expect { |b| lb.read_write(&b) }
- .to yield_with_args(ActiveRecord::Base.retrieve_connection)
+ connection = ActiveRecord::Base.connection_pool.connection
+
+ expect { |b| lb.read_write(&b) }.to yield_with_args(connection)
end
it 'uses a retry with exponential backoffs' do
@@ -134,140 +137,30 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
end
end
- describe '#db_role_for_connection' do
- context 'when the load balancer creates the connection with #read' do
- it 'returns :replica' do
- role = nil
- lb.read do |connection|
- role = lb.db_role_for_connection(connection)
- end
-
- expect(role).to be(:replica)
- end
- end
-
- context 'when the load balancer uses nested #read' do
- it 'returns :replica' do
- roles = []
- lb.read do |connection_1|
- lb.read do |connection_2|
- roles << lb.db_role_for_connection(connection_2)
- end
- roles << lb.db_role_for_connection(connection_1)
- end
-
- expect(roles).to eq([:replica, :replica])
- end
- end
-
- context 'when the load balancer creates the connection with #read_write' do
- it 'returns :primary' do
- role = nil
- lb.read_write do |connection|
- role = lb.db_role_for_connection(connection)
- end
-
- expect(role).to be(:primary)
- end
- end
-
- context 'when the load balancer uses nested #read_write' do
- it 'returns :primary' do
- roles = []
- lb.read_write do |connection_1|
- lb.read_write do |connection_2|
- roles << lb.db_role_for_connection(connection_2)
- end
- roles << lb.db_role_for_connection(connection_1)
- end
-
- expect(roles).to eq([:primary, :primary])
- end
- end
-
- context 'when the load balancer falls back the connection creation to primary' do
- it 'returns :primary' do
- allow(lb).to receive(:serialization_failure?).and_return(true)
-
- role = nil
- raised = 7 # 2 hosts = 6 retries
-
- lb.read do |connection|
- if raised > 0
- raised -= 1
- raise
- end
-
- role = lb.db_role_for_connection(connection)
- end
-
- expect(role).to be(:primary)
- end
- end
-
- context 'when the load balancer uses replica after recovery from a failure' do
- it 'returns :replica' do
- allow(lb).to receive(:connection_error?).and_return(true)
-
- role = nil
- raised = false
-
- lb.read do |connection|
- unless raised
- raised = true
- raise
- end
-
- role = lb.db_role_for_connection(connection)
- end
-
- expect(role).to be(:replica)
- end
- end
-
- context 'when the connection comes from a pool managed by the host list' do
- it 'returns :replica' do
- connection = double(:connection)
- allow(connection).to receive(:pool).and_return(lb.host_list.hosts.first.pool)
-
- expect(lb.db_role_for_connection(connection)).to be(:replica)
- end
- end
-
- context 'when the connection comes from the primary pool' do
- it 'returns :primary' do
- connection = double(:connection)
- allow(connection).to receive(:pool).and_return(ActiveRecord::Base.connection_pool)
-
- expect(lb.db_role_for_connection(connection)).to be(:primary)
- end
- end
-
- context 'when the connection does not come from any known pool' do
- it 'returns nil' do
- connection = double(:connection)
- pool = double(:connection_pool)
- allow(connection).to receive(:pool).and_return(pool)
-
- expect(lb.db_role_for_connection(connection)).to be(nil)
- end
- end
- end
-
describe '#host' do
it 'returns the secondary host to use' do
expect(lb.host).to be_an_instance_of(Gitlab::Database::LoadBalancing::Host)
end
it 'stores the host in a thread-local variable' do
- RequestStore.delete(described_class::CACHE_KEY)
- RequestStore.delete(described_class::VALID_HOSTS_CACHE_KEY)
+ request_cache.delete(described_class::CACHE_KEY)
expect(lb.host_list).to receive(:next).once.and_call_original
lb.host
lb.host
end
+
+ it 'does not create conflicts with other load balancers when caching hosts' do
+ lb1 = described_class.new([db_host, db_host], ActiveRecord::Base)
+ lb2 = described_class.new([db_host, db_host], Ci::CiDatabaseRecord)
+
+ host1 = lb1.host
+ host2 = lb2.host
+
+ expect(lb1.send(:request_cache)[described_class::CACHE_KEY]).to eq(host1)
+ expect(lb2.send(:request_cache)[described_class::CACHE_KEY]).to eq(host2)
+ end
end
describe '#release_host' do
@@ -278,8 +171,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
lb.release_host
- expect(RequestStore[described_class::CACHE_KEY]).to be_nil
- expect(RequestStore[described_class::VALID_HOSTS_CACHE_KEY]).to be_nil
+ expect(request_cache[described_class::CACHE_KEY]).to be_nil
end
end
@@ -414,89 +306,76 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
end
end
- describe '#select_caught_up_hosts' do
+ describe '#select_up_to_date_host' do
let(:location) { 'AB/12345'}
let(:hosts) { lb.host_list.hosts }
- let(:valid_host_list) { RequestStore[described_class::VALID_HOSTS_CACHE_KEY] }
- let(:valid_hosts) { valid_host_list.hosts }
+ let(:set_host) { request_cache[described_class::CACHE_KEY] }
- subject { lb.select_caught_up_hosts(location) }
-
- context 'when all replicas are caught up' do
- before do
- expect(hosts).to all(receive(:caught_up?).with(location).and_return(true))
- end
-
- it 'returns true and sets all hosts to valid' do
- expect(subject).to be true
- expect(valid_host_list).to be_a(Gitlab::Database::LoadBalancing::HostList)
- expect(valid_hosts).to contain_exactly(*hosts)
- end
- end
+ subject { lb.select_up_to_date_host(location) }
context 'when none of the replicas are caught up' do
before do
expect(hosts).to all(receive(:caught_up?).with(location).and_return(false))
end
- it 'returns false and does not set the valid hosts' do
+ it 'returns false and does not update the host thread-local variable' do
expect(subject).to be false
- expect(valid_host_list).to be_nil
+ expect(set_host).to be_nil
end
end
- context 'when one of the replicas is caught up' do
+ context 'when any of the replicas is caught up' do
before do
- expect(hosts[0]).to receive(:caught_up?).with(location).and_return(false)
+ # `allow` for non-caught up host, because we may not even check it, if will find the caught up one earlier
+ allow(hosts[0]).to receive(:caught_up?).with(location).and_return(false)
expect(hosts[1]).to receive(:caught_up?).with(location).and_return(true)
end
- it 'returns true and sets one host to valid' do
+ it 'returns true and sets host thread-local variable' do
expect(subject).to be true
- expect(valid_host_list).to be_a(Gitlab::Database::LoadBalancing::HostList)
- expect(valid_hosts).to contain_exactly(hosts[1])
- end
-
- it 'host always returns the caught-up replica' do
- subject
-
- 3.times do
- expect(lb.host).to eq(hosts[1])
- RequestStore.delete(described_class::CACHE_KEY)
- end
+ expect(set_host).to eq(hosts[1])
end
end
end
- describe '#select_up_to_date_host' do
- let(:location) { 'AB/12345'}
- let(:hosts) { lb.host_list.hosts }
- let(:set_host) { RequestStore[described_class::CACHE_KEY] }
+ describe '#create_replica_connection_pool' do
+ it 'creates a new connection pool with specific pool size and name' do
+ with_replica_pool(5, 'other_host') do |replica_pool|
+ expect(replica_pool)
+ .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
- subject { lb.select_up_to_date_host(location) }
-
- context 'when none of the replicas are caught up' do
- before do
- expect(hosts).to all(receive(:caught_up?).with(location).and_return(false))
+ expect(replica_pool.db_config.host).to eq('other_host')
+ expect(replica_pool.db_config.pool).to eq(5)
+ expect(replica_pool.db_config.name).to end_with("_replica")
end
+ end
- it 'returns false and does not update the host thread-local variable' do
- expect(subject).to be false
- expect(set_host).to be_nil
+ it 'allows setting of a custom hostname and port' do
+ with_replica_pool(5, 'other_host', 5432) do |replica_pool|
+ expect(replica_pool.db_config.host).to eq('other_host')
+ expect(replica_pool.db_config.configuration_hash[:port]).to eq(5432)
end
end
- context 'when any of the replicas is caught up' do
- before do
- # `allow` for non-caught up host, because we may not even check it, if will find the caught up one earlier
- allow(hosts[0]).to receive(:caught_up?).with(location).and_return(false)
- expect(hosts[1]).to receive(:caught_up?).with(location).and_return(true)
- end
+ it 'does not modify connection class pool' do
+ expect { with_replica_pool(5) { } }.not_to change { ActiveRecord::Base.connection_pool }
+ end
- it 'returns true and sets host thread-local variable' do
- expect(subject).to be true
- expect(set_host).to eq(hosts[1])
+ def with_replica_pool(*args)
+ pool = lb.create_replica_connection_pool(*args)
+ yield pool
+ ensure
+ pool&.disconnect!
+ end
+ end
+
+ describe '#disconnect!' do
+ it 'calls disconnect on all hosts with a timeout', :skip_disconnect do
+ expect_next_instances_of(Gitlab::Database::LoadBalancing::Host, 2) do |host|
+ expect(host).to receive(:disconnect!).with(timeout: 30)
end
+
+ lb.disconnect!(timeout: 30)
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
index 9381ffa59fe..ea0c7f781fd 100644
--- a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
@@ -183,18 +183,17 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
describe '#clear' do
it 'clears the currently used host and session' do
lb = double(:lb)
- session = double(:session)
+ session = spy(:session)
allow(middleware).to receive(:load_balancer).and_return(lb)
expect(lb).to receive(:release_host)
- stub_const('Gitlab::Database::LoadBalancing::RackMiddleware::Session',
- session)
-
- expect(session).to receive(:clear_session)
+ stub_const('Gitlab::Database::LoadBalancing::Session', session)
middleware.clear
+
+ expect(session).to have_received(:clear_session)
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
index 7fc7b5e8d11..a27341a3324 100644
--- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
@@ -3,8 +3,14 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
+ let(:load_balancer) { Gitlab::Database::LoadBalancing::LoadBalancer.new([]) }
let(:service) do
- described_class.new(nameserver: 'localhost', port: 8600, record: 'foo')
+ described_class.new(
+ nameserver: 'localhost',
+ port: 8600,
+ record: 'foo',
+ load_balancer: load_balancer
+ )
end
before do
@@ -18,7 +24,15 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
describe '#initialize' do
describe ':record_type' do
- subject { described_class.new(nameserver: 'localhost', port: 8600, record: 'foo', record_type: record_type) }
+ subject do
+ described_class.new(
+ nameserver: 'localhost',
+ port: 8600,
+ record: 'foo',
+ record_type: record_type,
+ load_balancer: load_balancer
+ )
+ end
context 'with a supported type' do
let(:record_type) { 'SRV' }
@@ -44,21 +58,17 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
end
it 'starts service discovery in a new thread' do
- expect(service)
- .to receive(:refresh_if_necessary)
- .and_return(5)
-
- expect(service)
- .to receive(:rand)
- .and_return(2)
+ expect(Thread).to receive(:new).ordered.and_call_original # Thread starts
- expect(service)
- .to receive(:sleep)
- .with(7)
+ expect(service).to receive(:perform_service_discovery).ordered.and_return(5)
+ expect(service).to receive(:rand).ordered.and_return(2)
+ expect(service).to receive(:sleep).ordered.with(7) # Sleep runs after thread starts
service.start.join
end
+ end
+ describe '#perform_service_discovery' do
it 'reports exceptions to Sentry' do
error = StandardError.new
@@ -70,15 +80,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
.to receive(:track_exception)
.with(error)
- expect(service)
- .to receive(:rand)
- .and_return(2)
-
- expect(service)
- .to receive(:sleep)
- .with(62)
-
- service.start.join
+ service.perform_service_discovery
end
end
@@ -155,14 +157,23 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
expect(host)
.to receive(:disconnect!)
- .with(2)
+ .with(timeout: 2)
service.replace_hosts([address_bar])
end
end
describe '#addresses_from_dns' do
- let(:service) { described_class.new(nameserver: 'localhost', port: 8600, record: 'foo', record_type: record_type) }
+ let(:service) do
+ described_class.new(
+ nameserver: 'localhost',
+ port: 8600,
+ record: 'foo',
+ record_type: record_type,
+ load_balancer: load_balancer
+ )
+ end
+
let(:packet) { double(:packet, answer: [res1, res2]) }
before do
@@ -234,13 +245,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
end
describe '#addresses_from_load_balancer' do
- it 'returns the ordered host names of the load balancer' do
- load_balancer = Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[b a])
-
- allow(service)
- .to receive(:load_balancer)
- .and_return(load_balancer)
+ let(:load_balancer) do
+ Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[b a])
+ end
+ it 'returns the ordered host names of the load balancer' do
addresses = [
described_class::Address.new('a'),
described_class::Address.new('b')
diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
index 53445d73756..cf52e59db3a 100644
--- a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
@@ -237,7 +237,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
context 'when write location is nil' do
before do
- allow(Gitlab::Database).to receive(:get_write_location).and_return(nil)
+ allow(Gitlab::Database.main).to receive(:get_write_location).and_return(nil)
end
it 'does not update the write location' do
@@ -313,7 +313,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end
it 'returns false and does not try to find caught up hosts' do
- expect(described_class).not_to receive(:select_caught_up_hosts)
+ expect(lb).not_to receive(:select_up_to_date_host)
expect(described_class.select_caught_up_replicas(:project, 42)).to be false
end
end
@@ -329,18 +329,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
expect(described_class).to receive(:unstick).with(:project, 42)
expect(described_class.select_caught_up_replicas(:project, 42)).to be true
end
-
- context 'when :load_balancing_refine_load_balancer_methods FF is disabled' do
- before do
- stub_feature_flags(load_balancing_refine_load_balancer_methods: false)
- end
-
- it 'returns true, selects hosts, and unsticks if any secondary has caught up' do
- expect(lb).to receive(:select_caught_up_hosts).and_return(true)
- expect(described_class).to receive(:unstick).with(:project, 42)
- expect(described_class.select_caught_up_replicas(:project, 42)).to be true
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb
index 94717a10492..6ec8e0516f6 100644
--- a/spec/lib/gitlab/database/load_balancing_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing_spec.rb
@@ -3,25 +3,28 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing do
- include_context 'clear DB Load Balancing configuration'
+ describe '.proxy' do
+ before do
+ @previous_proxy = ActiveRecord::Base.load_balancing_proxy
- before do
- stub_env('ENABLE_LOAD_BALANCING_FOR_FOSS', 'true')
- end
+ ActiveRecord::Base.load_balancing_proxy = connection_proxy
+ end
+
+ after do
+ ActiveRecord::Base.load_balancing_proxy = @previous_proxy
+ end
- describe '.proxy' do
context 'when configured' do
- before do
- allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
- subject.configure_proxy
- end
+ let(:connection_proxy) { double(:connection_proxy) }
it 'returns the connection proxy' do
- expect(subject.proxy).to be_an_instance_of(subject::ConnectionProxy)
+ expect(subject.proxy).to eq(connection_proxy)
end
end
context 'when not configured' do
+ let(:connection_proxy) { nil }
+
it 'returns nil' do
expect(subject.proxy).to be_nil
end
@@ -40,9 +43,9 @@ RSpec.describe Gitlab::Database::LoadBalancing do
it 'returns a Hash' do
lb_config = { 'hosts' => %w(foo) }
- original_db_config = Gitlab::Database.config
+ original_db_config = Gitlab::Database.main.config
modified_db_config = original_db_config.merge(load_balancing: lb_config)
- expect(Gitlab::Database).to receive(:config).and_return(modified_db_config)
+ expect(Gitlab::Database.main).to receive(:config).and_return(modified_db_config)
expect(described_class.configuration).to eq(lb_config)
end
@@ -132,7 +135,6 @@ RSpec.describe Gitlab::Database::LoadBalancing do
describe '.enable?' do
before do
- clear_load_balancing_configuration
allow(described_class).to receive(:hosts).and_return(%w(foo))
end
@@ -173,10 +175,6 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
describe '.configured?' do
- before do
- clear_load_balancing_configuration
- end
-
it 'returns true when Sidekiq is being used' do
allow(described_class).to receive(:hosts).and_return(%w(foo))
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
@@ -207,12 +205,27 @@ RSpec.describe Gitlab::Database::LoadBalancing do
describe '.configure_proxy' do
it 'configures the connection proxy' do
- allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ allow(ActiveRecord::Base).to receive(:load_balancing_proxy=)
described_class.configure_proxy
- expect(ActiveRecord::Base.singleton_class).to have_received(:prepend)
- .with(Gitlab::Database::LoadBalancing::ActiveRecordProxy)
+ expect(ActiveRecord::Base).to have_received(:load_balancing_proxy=)
+ .with(Gitlab::Database::LoadBalancing::ConnectionProxy)
+ end
+
+ context 'when service discovery is enabled' do
+ let(:service_discovery) { double(Gitlab::Database::LoadBalancing::ServiceDiscovery) }
+
+ it 'runs initial service discovery when configuring the connection proxy' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return('discover' => { 'record' => 'foo' })
+
+ expect(Gitlab::Database::LoadBalancing::ServiceDiscovery).to receive(:new).and_return(service_discovery)
+ expect(service_discovery).to receive(:perform_service_discovery)
+
+ described_class.configure_proxy
+ end
end
end
@@ -298,59 +311,46 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
describe '.db_role_for_connection' do
- let(:connection) { double(:conneciton) }
-
context 'when the load balancing is not configured' do
- before do
- allow(described_class).to receive(:enable?).and_return(false)
- end
+ let(:connection) { ActiveRecord::Base.connection }
it 'returns primary' do
- expect(described_class.db_role_for_connection(connection)).to be(:primary)
+ expect(described_class.db_role_for_connection(connection)).to eq(:primary)
end
end
- context 'when the load balancing is configured' do
- let(:proxy) { described_class::ConnectionProxy.new(%w(foo)) }
- let(:load_balancer) { described_class::LoadBalancer.new(%w(foo)) }
-
- before do
- allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ context 'when the NullPool is used for connection' do
+ let(:pool) { ActiveRecord::ConnectionAdapters::NullPool.new }
+ let(:connection) { double(:connection, pool: pool) }
- allow(described_class).to receive(:enable?).and_return(true)
- allow(described_class).to receive(:proxy).and_return(proxy)
- allow(proxy).to receive(:load_balancer).and_return(load_balancer)
-
- subject.configure_proxy(proxy)
+ it 'returns unknown' do
+ expect(described_class.db_role_for_connection(connection)).to eq(:unknown)
end
+ end
- context 'when the load balancer returns :replica' do
- it 'returns :replica' do
- allow(load_balancer).to receive(:db_role_for_connection).and_return(:replica)
-
- expect(described_class.db_role_for_connection(connection)).to be(:replica)
+ context 'when the load balancing is configured' do
+ let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host }
+ let(:proxy) { described_class::ConnectionProxy.new([db_host]) }
- expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ context 'when a proxy connection is used' do
+ it 'returns :unknown' do
+ expect(described_class.db_role_for_connection(proxy)).to eq(:unknown)
end
end
- context 'when the load balancer returns :primary' do
- it 'returns :primary' do
- allow(load_balancer).to receive(:db_role_for_connection).and_return(:primary)
-
- expect(described_class.db_role_for_connection(connection)).to be(:primary)
-
- expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ context 'when a read connection is used' do
+ it 'returns :replica' do
+ proxy.load_balancer.read do |connection|
+ expect(described_class.db_role_for_connection(connection)).to eq(:replica)
+ end
end
end
- context 'when the load balancer returns nil' do
- it 'returns nil' do
- allow(load_balancer).to receive(:db_role_for_connection).and_return(nil)
-
- expect(described_class.db_role_for_connection(connection)).to be(nil)
-
- expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ context 'when a read_write connection is used' do
+ it 'returns :primary' do
+ proxy.load_balancer.read_write do |connection|
+ expect(described_class.db_role_for_connection(connection)).to eq(:primary)
+ end
end
end
end
@@ -366,7 +366,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
# - In each test, we listen to the SQL queries (via sql.active_record
# instrumentation) while triggering real queries from the defined model.
# - We assert the desinations (replica/primary) of the queries in order.
- describe 'LoadBalancing integration tests', :delete do
+ describe 'LoadBalancing integration tests', :db_load_balancing, :delete do
before(:all) do
ActiveRecord::Schema.define do
create_table :load_balancing_test, force: true do |t|
@@ -381,30 +381,14 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
end
- shared_context 'LoadBalancing setup' do
- let(:development_db_config) { ActiveRecord::Base.configurations.configs_for(env_name: 'development').first.configuration_hash }
- let(:hosts) { [development_db_config[:host]] }
- let(:model) do
- Class.new(ApplicationRecord) do
- self.table_name = "load_balancing_test"
- end
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = "load_balancing_test"
end
+ end
- before do
- # Preloading testing class
- model.singleton_class.prepend ::Gitlab::Database::LoadBalancing::ActiveRecordProxy
-
- # Setup load balancing
- clear_load_balancing_configuration
- allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
- subject.configure_proxy(::Gitlab::Database::LoadBalancing::ConnectionProxy.new(hosts))
-
- original_db_config = Gitlab::Database.config
- modified_db_config = original_db_config.merge(load_balancing: { hosts: hosts })
- allow(Gitlab::Database).to receive(:config).and_return(modified_db_config)
-
- ::Gitlab::Database::LoadBalancing::Session.clear_session
- end
+ before do
+ model.singleton_class.prepend ::Gitlab::Database::LoadBalancing::ActiveRecordProxy
end
where(:queries, :include_transaction, :expected_results) do
@@ -715,8 +699,6 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
with_them do
- include_context 'LoadBalancing setup'
-
it 'redirects queries to the right roles' do
roles = []
@@ -785,8 +767,6 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
with_them do
- include_context 'LoadBalancing setup'
-
it 'redirects queries to the right roles' do
roles = []
@@ -805,8 +785,6 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end
context 'a write inside a transaction inside fallback_to_replicas_for_ambiguous_queries block' do
- include_context 'LoadBalancing setup'
-
it 'raises an exception' do
expect do
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 8e25f9249fe..9f9aef77de7 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -278,6 +278,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo, unique: true)
end
+
+ it 'unprepares the async index creation' do
+ expect(model).to receive(:add_index)
+ .with(:users, :foo, algorithm: :concurrently)
+
+ expect(model).to receive(:unprepare_async_index)
+ .with(:users, :foo, algorithm: :concurrently)
+
+ model.add_concurrent_index(:users, :foo)
+ end
end
context 'inside a transaction' do
@@ -314,6 +324,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.remove_concurrent_index(:users, :foo, unique: true)
end
+ it 'unprepares the async index creation' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, column: :foo })
+
+ expect(model).to receive(:unprepare_async_index)
+ .with(:users, :foo, { algorithm: :concurrently })
+
+ model.remove_concurrent_index(:users, :foo)
+ end
+
describe 'by index name' do
before do
allow(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(true)
@@ -345,6 +365,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.remove_concurrent_index_by_name(:users, wrong_key: "index_x_by_y")
end.to raise_error 'remove_concurrent_index_by_name must get an index name as the second argument'
end
+
+ it 'unprepares the async index creation' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+
+ expect(model).to receive(:unprepare_async_index_by_name)
+ .with(:users, "index_x_by_y", { algorithm: :concurrently })
+
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
end
end
end
@@ -384,9 +414,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
expect(model).to receive(:execute).with(/REFERENCES users \(id\)/)
@@ -398,9 +428,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
expect(model).to receive(:execute).with(/REFERENCES users \(id_convert_to_bigint\)/)
@@ -416,9 +446,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
@@ -433,9 +463,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
expect(model).to receive(:execute).with(/ON DELETE CASCADE/)
@@ -450,9 +480,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
expect(model).not_to receive(:execute).with(/ON DELETE/)
@@ -468,10 +498,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
@@ -497,10 +527,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+foo/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo)
end
@@ -527,10 +557,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+bar/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :bar)
end
@@ -556,6 +586,22 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it_behaves_like 'performs validation', {}
end
end
+
+ context 'when the reverse_lock_order flag is set' do
+ it 'explicitly locks the tables in target-source order', :aggregate_failures do
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
+
+ expect(model).to receive(:execute).with('LOCK TABLE users, projects IN SHARE ROW EXCLUSIVE MODE')
+ expect(model).to receive(:execute).with(/REFERENCES users \(id\)/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id, reverse_lock_order: true)
+ end
+ end
end
end
@@ -568,9 +614,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).not_to receive(:concurrent_foreign_key_name)
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
end
model.validate_foreign_key(:projects, :user_id, name: :foo)
@@ -585,9 +631,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:concurrent_foreign_key_name)
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
end
model.validate_foreign_key(:projects, :user_id)
@@ -702,7 +748,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
after do
- model.execute('RESET ALL')
+ model.execute('RESET statement_timeout')
end
it 'defines statement to 0 only for current transaction' do
@@ -719,7 +765,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
context 'when passing a blocks' do
it 'disables statement timeouts on session level and executes the block' do
expect(model).to receive(:execute).with('SET statement_timeout TO 0')
- expect(model).to receive(:execute).with('RESET ALL').at_least(:once)
+ expect(model).to receive(:execute).with('RESET statement_timeout').at_least(:once)
expect { |block| model.disable_statement_timeout(&block) }.to yield_control
end
@@ -731,7 +777,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
after do
- model.execute('RESET ALL')
+ model.execute('RESET statement_timeout')
end
it 'defines statement to 0 for any code run inside the block' do
@@ -758,12 +804,12 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
after do
# Use ActiveRecord::Base.connection instead of model.execute
# so that this call is not counted below
- ActiveRecord::Base.connection.execute('RESET ALL')
+ ActiveRecord::Base.connection.execute('RESET statement_timeout')
end
it 'yields control without disabling the timeout or resetting' do
expect(model).not_to receive(:execute).with('SET statement_timeout TO 0')
- expect(model).not_to receive(:execute).with('RESET ALL')
+ expect(model).not_to receive(:execute).with('RESET statement_timeout')
expect { |block| model.disable_statement_timeout(&block) }.to yield_control
end
@@ -2486,7 +2532,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
@@ -2496,7 +2542,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
.and_return(true).exactly(1)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_check_constraint(
:test_table,
@@ -2530,7 +2576,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
@@ -2539,7 +2585,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
.and_return(true).exactly(1)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_check_constraint(
:test_table,
@@ -2572,9 +2618,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:check_constraint_exists?).and_return(true)
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(validate_sql)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.validate_check_constraint(:test_table, 'check_name')
end
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index e096e7f6e91..1a7116e75e5 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -581,4 +581,101 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
model.delete_queued_jobs('BackgroundMigrationClassName')
end
end
+
+ describe '#finalized_background_migration' do
+ include_context 'background migration job class'
+
+ let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
+ let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
+
+ before do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker.perform_async(job_class_name, [1, 2])
+ BackgroundMigrationWorker.perform_async(job_class_name, [3, 4])
+ BackgroundMigrationWorker.perform_in(10, job_class_name, [5, 6])
+ BackgroundMigrationWorker.perform_in(20, job_class_name, [7, 8])
+ end
+ end
+
+ it_behaves_like 'finalized tracked background migration' do
+ before do
+ model.finalize_background_migration(job_class_name)
+ end
+ end
+
+ context 'when removing all tracked job records' do
+ # Force pending jobs to remain pending.
+ let!(:job_perform_method) { ->(*arguments) { } }
+
+ before do
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded])
+ end
+
+ it_behaves_like 'finalized tracked background migration'
+ it_behaves_like 'removed tracked jobs', 'pending'
+ it_behaves_like 'removed tracked jobs', 'succeeded'
+ end
+
+ context 'when retaining all tracked job records' do
+ before do
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: false)
+ end
+
+ it_behaves_like 'finalized background migration'
+ include_examples 'retained tracked jobs', 'succeeded'
+ end
+
+ context 'during retry race condition' do
+ let(:queue_items_added) { [] }
+ let!(:job_perform_method) do
+ ->(*arguments) do
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ RSpec.current_example.example_group_instance.job_class_name,
+ arguments
+ )
+
+ # Mock another process pushing queue jobs.
+ queue_items_added = RSpec.current_example.example_group_instance.queue_items_added
+ if queue_items_added.count < 10
+ Sidekiq::Testing.disable! do
+ job_class_name = RSpec.current_example.example_group_instance.job_class_name
+ queue_items_added << BackgroundMigrationWorker.perform_async(job_class_name, [Time.current])
+ queue_items_added << BackgroundMigrationWorker.perform_in(10, job_class_name, [Time.current])
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'finalized tracked background migration' do
+ before do
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: ['succeeded'])
+ end
+ end
+ end
+ end
+
+ describe '#delete_job_tracking' do
+ let!(:job_class_name) { 'TestJob' }
+
+ let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
+ let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
+
+ context 'with default status' do
+ before do
+ model.delete_job_tracking(job_class_name)
+ end
+
+ include_examples 'retained tracked jobs', 'pending'
+ include_examples 'removed tracked jobs', 'succeeded'
+ end
+
+ context 'with explicit status' do
+ before do
+ model.delete_job_tracking(job_class_name, status: %w[pending succeeded])
+ end
+
+ include_examples 'removed tracked jobs', 'pending'
+ include_examples 'removed tracked jobs', 'succeeded'
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
index 6d047eed3bb..5945e5a2039 100644
--- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -5,24 +5,35 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
describe '#observe' do
subject { described_class.new }
- let(:migration) { 1234 }
+ let(:migration_name) { 'test' }
+ let(:migration_version) { '12345' }
it 'executes the given block' do
- expect { |b| subject.observe(migration, &b) }.to yield_control
+ expect { |b| subject.observe(version: migration_version, name: migration_name, &b) }.to yield_control
end
context 'behavior with observers' do
- subject { described_class.new(observers).observe(migration) {} }
+ subject { described_class.new([Gitlab::Database::Migrations::Observers::MigrationObserver]).observe(version: migration_version, name: migration_name) {} }
- let(:observers) { [observer] }
let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) }
+ before do
+ allow(Gitlab::Database::Migrations::Observers::MigrationObserver).to receive(:new).and_return(observer)
+ end
+
+ it 'instantiates observer with observation' do
+ expect(Gitlab::Database::Migrations::Observers::MigrationObserver)
+ .to receive(:new)
+ .with(instance_of(Gitlab::Database::Migrations::Observation)) { |observation| expect(observation.version).to eq(migration_version) }
+ .and_return(observer)
+
+ subject
+ end
+
it 'calls #before, #after, #record on given observers' do
expect(observer).to receive(:before).ordered
expect(observer).to receive(:after).ordered
- expect(observer).to receive(:record).ordered do |observation|
- expect(observation.migration).to eq(migration)
- end
+ expect(observer).to receive(:record).ordered
subject
end
@@ -47,7 +58,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
end
context 'on successful execution' do
- subject { described_class.new.observe(migration) {} }
+ subject { described_class.new.observe(version: migration_version, name: migration_name) {} }
it 'records walltime' do
expect(subject.walltime).not_to be_nil
@@ -58,12 +69,16 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
end
it 'records the migration version' do
- expect(subject.migration).to eq(migration)
+ expect(subject.version).to eq(migration_version)
+ end
+
+ it 'records the migration name' do
+ expect(subject.name).to eq(migration_name)
end
end
context 'upon failure' do
- subject { described_class.new.observe(migration) { raise 'something went wrong' } }
+ subject { described_class.new.observe(version: migration_version, name: migration_name) { raise 'something went wrong' } }
it 'raises the exception' do
expect { subject }.to raise_error(/something went wrong/)
@@ -73,7 +88,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
subject { instance.observations.first }
before do
- instance.observe(migration) { raise 'something went wrong' }
+ instance.observe(version: migration_version, name: migration_name) { raise 'something went wrong' }
rescue StandardError
# ignore
end
@@ -89,7 +104,11 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
end
it 'records the migration version' do
- expect(subject.migration).to eq(migration)
+ expect(subject.version).to eq(migration_version)
+ end
+
+ it 'records the migration name' do
+ expect(subject.name).to eq(migration_name)
end
end
end
@@ -101,8 +120,8 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
let(:migration2) { double('migration2', call: nil) }
it 'records observations for all migrations' do
- subject.observe('migration1') {}
- subject.observe('migration2') { raise 'something went wrong' } rescue nil
+ subject.observe(version: migration_version, name: migration_name) {}
+ subject.observe(version: migration_version, name: migration_name) { raise 'something went wrong' } rescue nil
expect(subject.observations.size).to eq(2)
end
diff --git a/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
index 8aac3ed67c6..36885a1594f 100644
--- a/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/query_details_spec.rb
@@ -2,16 +2,17 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryDetails do
- subject { described_class.new }
+ subject { described_class.new(observation) }
- let(:observation) { Gitlab::Database::Migrations::Observation.new(migration) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
let(:connection) { ActiveRecord::Base.connection }
let(:query) { "select date_trunc('day', $1::timestamptz) + $2 * (interval '1 hour')" }
let(:query_binds) { [Time.current, 3] }
let(:directory_path) { Dir.mktmpdir }
- let(:log_file) { "#{directory_path}/#{migration}-query-details.json" }
+ let(:log_file) { "#{directory_path}/#{migration_version}_#{migration_name}-query-details.json" }
let(:query_details) { Gitlab::Json.parse(File.read(log_file)) }
- let(:migration) { 20210422152437 }
+ let(:migration_version) { 20210422152437 }
+ let(:migration_name) { 'test' }
before do
stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', directory_path)
@@ -49,7 +50,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryDetails do
subject.before
run_query
subject.after
- subject.record(observation)
+ subject.record
end
def run_query
diff --git a/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
index 195e7114582..2a49d8e8b73 100644
--- a/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/query_log_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do
- subject { described_class.new }
+ subject { described_class.new(observation) }
- let(:observation) { Gitlab::Database::Migrations::Observation.new(migration) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
let(:connection) { ActiveRecord::Base.connection }
let(:query) { 'select 1' }
let(:directory_path) { Dir.mktmpdir }
- let(:log_file) { "#{directory_path}/current.log" }
- let(:migration) { 20210422152437 }
+ let(:migration_version) { 20210422152437 }
+ let(:migration_name) { 'test' }
before do
stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', directory_path)
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do
it 'writes a file with the query log' do
observe
- expect(File.read("#{directory_path}/#{migration}.log")).to include(query)
+ expect(File.read("#{directory_path}/#{migration_version}_#{migration_name}.log")).to include(query)
end
it 'does not change the default logger' do
@@ -33,6 +33,6 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do
subject.before
connection.execute(query)
subject.after
- subject.record(observation)
+ subject.record
end
end
diff --git a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb
index a3b03050b33..32a25fdaa28 100644
--- a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do
- subject { described_class.new }
+ subject { described_class.new(observation) }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:connection) { ActiveRecord::Base.connection }
def mock_pgss(enabled: true)
@@ -37,7 +38,6 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do
end
describe '#record' do
- let(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:result) { double }
let(:pgss_query) do
<<~SQL
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do
mock_pgss(enabled: true)
expect(connection).to receive(:execute).with(pgss_query).once.and_return(result)
- expect { subject.record(observation) }.to change { observation.query_statistics }.from(nil).to(result)
+ expect { subject.record }.to change { observation.query_statistics }.from(nil).to(result)
end
end
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do
mock_pgss(enabled: false)
expect(connection).not_to receive(:execute)
- expect { subject.record(observation) }.not_to change { observation.query_statistics }
+ expect { subject.record }.not_to change { observation.query_statistics }
end
end
end
diff --git a/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
index 73466471944..61e28003e66 100644
--- a/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
+++ b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do
- subject { described_class.new }
+ subject { described_class.new(observation) }
let(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:connection) { ActiveRecord::Base.connection }
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange
subject.before
subject.after
- subject.record(observation)
+ subject.record
expect(observation.total_database_size_change).to eq(256 - 1024)
end
@@ -27,13 +27,13 @@ RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange
it 'does not record anything if before size is unknown' do
subject.after
- expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ expect { subject.record }.not_to change { observation.total_database_size_change }
end
it 'does not record anything if after size is unknown' do
subject.before
- expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ expect { subject.record }.not_to change { observation.total_database_size_change }
end
end
end
diff --git a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
deleted file mode 100644
index 78dd9e88064..00000000000
--- a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::MultiThreadedMigration do
- let(:migration) do
- Class.new { include Gitlab::Database::MultiThreadedMigration }.new
- end
-
- describe '#connection' do
- after do
- Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = nil
- end
-
- it 'returns the thread-local connection if present' do
- Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = 10
-
- expect(migration.connection).to eq(10)
- end
-
- it 'returns the global connection if no thread-local connection was set' do
- expect(migration.connection).to eq(ActiveRecord::Base.connection)
- end
- end
-
- describe '#with_multiple_threads' do
- it 'starts multiple threads and yields the supplied block in every thread' do
- output = Queue.new
-
- migration.with_multiple_threads(2) do
- output << migration.connection.execute('SELECT 1')
- end
-
- expect(output.size).to eq(2)
- end
-
- it 'joins the threads when the join parameter is set' do
- expect_any_instance_of(Thread).to receive(:join).and_call_original
-
- migration.with_multiple_threads(1) { }
- end
- end
-end
diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
new file mode 100644
index 00000000000..8523b7104f0
--- /dev/null
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
+ include Database::TableSchemaHelpers
+
+ let(:connection) { ActiveRecord::Base.connection }
+
+ def expect_partition_present(name)
+ aggregate_failures do
+ expect(table_oid(name)).not_to be_nil
+ expect(Postgresql::DetachedPartition.find_by(table_name: name)).not_to be_nil
+ end
+ end
+
+ def expect_partition_removed(name)
+ aggregate_failures do
+ expect(table_oid(name)).to be_nil
+ expect(Postgresql::DetachedPartition.find_by(table_name: name)).to be_nil
+ end
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE parent_table (
+ id bigserial not null,
+ created_at timestamptz not null,
+ primary key (id, created_at)
+ ) PARTITION BY RANGE(created_at)
+ SQL
+ end
+
+ def create_partition(name:, table: 'parent_table', from:, to:, attached:, drop_after:)
+ from = from.beginning_of_month
+ to = to.beginning_of_month
+ full_name = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{name}"
+ connection.execute(<<~SQL)
+ CREATE TABLE #{full_name}
+ PARTITION OF #{table}
+ FOR VALUES FROM ('#{from.strftime('%Y-%m-%d')}') TO ('#{to.strftime('%Y-%m-%d')}')
+ SQL
+
+ unless attached
+ connection.execute(<<~SQL)
+ ALTER TABLE #{table} DETACH PARTITION #{full_name}
+ SQL
+ end
+
+ Postgresql::DetachedPartition.create!(table_name: name,
+ drop_after: drop_after)
+ end
+
+ describe '#perform' do
+ context 'when the partition should not be dropped yet' do
+ it 'does not drop the partition' do
+ create_partition(name: 'test_partition',
+ from: 2.months.ago, to: 1.month.ago,
+ attached: false,
+ drop_after: 1.day.from_now)
+
+ subject.perform
+
+ expect_partition_present('test_partition')
+ end
+ end
+
+ context 'with a partition to drop' do
+ before do
+ create_partition(name: 'test_partition',
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: false,
+ drop_after: 1.second.ago)
+ end
+
+ it 'drops the partition' do
+ subject.perform
+
+ expect(table_oid('test_partition')).to be_nil
+ end
+
+ context 'when the drop_detached_partitions feature flag is disabled' do
+ before do
+ stub_feature_flags(drop_detached_partitions: false)
+ end
+ it 'does not drop the partition' do
+ subject.perform
+
+ expect(table_oid('test_partition')).not_to be_nil
+ end
+ end
+
+ context 'when another process drops the table while the first waits for a lock' do
+ it 'skips the table' do
+ # Rspec's receive_method_chain does not support .and_wrap_original, so we need to nest here.
+ expect(Postgresql::DetachedPartition).to receive(:lock).and_wrap_original do |lock_meth|
+ locked = lock_meth.call
+ expect(locked).to receive(:find_by).and_wrap_original do |find_meth, *find_args|
+ # Another process drops the table then deletes this entry
+ Postgresql::DetachedPartition.where(*find_args).delete_all
+ find_meth.call(*find_args)
+ end
+
+ locked
+ end
+
+ expect(subject).not_to receive(:drop_one)
+
+ subject.perform
+ end
+ end
+ end
+
+ context 'when the partition to drop is still attached to its table' do
+ before do
+ create_partition(name: 'test_partition',
+ from: 2.months.ago,
+ to: 1.month.ago.beginning_of_month,
+ attached: true,
+ drop_after: 1.second.ago)
+ end
+
+ it 'does not drop the partition, but does remove the DetachedPartition entry' do
+ subject.perform
+ aggregate_failures do
+ expect(table_oid('test_partition')).not_to be_nil
+ expect(Postgresql::DetachedPartition.find_by(table_name: 'test_partition')).to be_nil
+ end
+ end
+
+ it 'removes the detached_partition entry' do
+ detached_partition = Postgresql::DetachedPartition.find_by!(table_name: 'test_partition')
+
+ subject.perform
+
+ expect(Postgresql::DetachedPartition.exists?(id: detached_partition.id)).to be_falsey
+ end
+ end
+
+ context 'with multiple partitions to drop' do
+ before do
+ create_partition(name: 'partition_1',
+ from: 3.months.ago,
+ to: 2.months.ago,
+ attached: false,
+ drop_after: 1.second.ago)
+
+ create_partition(name: 'partition_2',
+ from: 2.months.ago,
+ to: 1.month.ago,
+ attached: false,
+ drop_after: 1.second.ago)
+ end
+
+ it 'drops both partitions' do
+ subject.perform
+
+ expect_partition_removed('partition_1')
+ expect_partition_removed('partition_2')
+ end
+
+ context 'when the first drop returns an error' do
+ it 'still drops the second partition' do
+ expect(subject).to receive(:drop_one).ordered.and_raise('injected error')
+ expect(subject).to receive(:drop_one).ordered.and_call_original
+
+ subject.perform
+
+ # We don't know which partition we tried to drop first, so the tests here have to work with either one
+ expect(Postgresql::DetachedPartition.count).to eq(1)
+ errored_partition_name = Postgresql::DetachedPartition.first!.table_name
+
+ dropped_partition_name = (%w[partition_1 partition_2] - [errored_partition_name]).first
+ expect_partition_present(errored_partition_name)
+ expect_partition_removed(dropped_partition_name)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
index f9dca371398..c4fbf53d1c2 100644
--- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
@@ -237,16 +237,6 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
expect(subject).to contain_exactly(min_value_to_may)
end
-
- context 'when the feature flag is toggled off' do
- before do
- stub_feature_flags(partition_pruning_dry_run: false)
- end
-
- it 'is empty' do
- expect(subject).to eq([])
- end
- end
end
context 'with a time retention policy of 2 months' do
@@ -258,16 +248,6 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005')
)
end
-
- context 'when the feature flag is toggled off' do
- before do
- stub_feature_flags(partition_pruning_dry_run: false)
- end
-
- it 'is empty' do
- expect(subject).to eq([])
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 903a41d6dd2..3d60457c3a9 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -4,9 +4,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
include Database::PartitioningHelpers
- include Database::TableSchemaHelpers
include ExclusiveLeaseHelpers
+ def has_partition(model, month)
+ Gitlab::Database::PostgresPartition.for_parent_table(model.table_name).any? do |partition|
+ Gitlab::Database::Partitioning::TimePartition.from_sql(model.table_name, partition.name, partition.condition).from == month
+ end
+ end
+
describe '.register' do
let(:model) { double(partitioning_strategy: nil) }
@@ -111,14 +116,14 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
let(:extra_partitions) do
[
- instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo1'),
- instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo2')
+ instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo1', to_detach_sql: 'SELECT 1'),
+ instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo2', to_detach_sql: 'SELECT 2')
]
end
- context 'with the partition_pruning_dry_run feature flag enabled' do
+ context 'with the partition_pruning feature flag enabled' do
before do
- stub_feature_flags(partition_pruning_dry_run: true)
+ stub_feature_flags(partition_pruning: true)
end
it 'detaches each extra partition' do
@@ -146,9 +151,9 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
end
- context 'with the partition_pruning_dry_run feature flag disabled' do
+ context 'with the partition_pruning feature flag disabled' do
before do
- stub_feature_flags(partition_pruning_dry_run: false)
+ stub_feature_flags(partition_pruning: false)
end
it 'returns immediately' do
@@ -158,4 +163,128 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
end
end
+
+ describe '#detach_partitions' do
+ around do |ex|
+ travel_to(Date.parse('2021-06-23')) do
+ ex.run
+ end
+ end
+
+ subject { described_class.new([my_model]).sync_partitions }
+
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:my_model) do
+ Class.new(ApplicationRecord) do
+ include PartitionedTable
+
+ self.table_name = 'my_model_example_table'
+
+ partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
+ end
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE my_model_example_table
+ (id serial not null, created_at timestamptz not null, primary key (id, created_at))
+ PARTITION BY RANGE (created_at);
+
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.my_model_example_table_202104
+ PARTITION OF my_model_example_table
+ FOR VALUES FROM ('2021-04-01') TO ('2021-05-01');
+
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.my_model_example_table_202105
+ PARTITION OF my_model_example_table
+ FOR VALUES FROM ('2021-05-01') TO ('2021-06-01');
+ SQL
+
+ # Also create all future partitions so that the sync is only trying to detach old partitions
+ my_model.partitioning_strategy.missing_partitions.each do |p|
+ connection.execute p.to_sql
+ end
+ end
+
+ def num_tables
+ connection.select_value(<<~SQL)
+ SELECT COUNT(*)
+ FROM pg_class
+ where relkind IN ('r', 'p')
+ SQL
+ end
+
+ it 'detaches exactly one partition' do
+ expect { subject }.to change { find_partitions(my_model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size }.from(9).to(8)
+ end
+
+ it 'detaches the old partition' do
+ expect { subject }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false)
+ end
+
+ it 'deletes zero tables' do
+ expect { subject }.not_to change { num_tables }
+ end
+
+ it 'creates the appropriate PendingPartitionDrop entry' do
+ subject
+
+ pending_drop = Postgresql::DetachedPartition.find_by!(table_name: 'my_model_example_table_202104')
+ expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR)
+ end
+
+ # Postgres 11 does not support foreign keys to partitioned tables
+ if Gitlab::Database.main.version.to_f >= 12
+ context 'when the model is the target of a foreign key' do
+ before do
+ connection.execute(<<~SQL)
+ create unique index idx_for_fk ON my_model_example_table(created_at);
+
+ create table referencing_table (
+ id bigserial primary key not null,
+ referencing_created_at timestamptz references my_model_example_table(created_at)
+ );
+ SQL
+ end
+
+ it 'does not detach partitions with a referenced foreign key' do
+ expect { subject }.not_to change { find_partitions(my_model.table_name).size }
+ end
+ end
+ end
+ end
+
+ context 'creating and then detaching partitions for a table' do
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:my_model) do
+ Class.new(ApplicationRecord) do
+ include PartitionedTable
+
+ self.table_name = 'my_model_example_table'
+
+ partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
+ end
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE my_model_example_table
+ (id serial not null, created_at timestamptz not null, primary key (id, created_at))
+ PARTITION BY RANGE (created_at);
+ SQL
+ end
+
+ def num_partitions(model)
+ find_partitions(model.table_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA).size
+ end
+
+ it 'creates partitions for the future then drops the oldest one after a month' do
+ # 1 month for the current month, 1 month for the old month that we're retaining data for, headroom
+ expected_num_partitions = (Gitlab::Database::Partitioning::MonthlyStrategy::HEADROOM + 2.months) / 1.month
+ expect { described_class.new([my_model]).sync_partitions }.to change { num_partitions(my_model) }.from(0).to(expected_num_partitions)
+
+ travel 1.month
+
+ expect { described_class.new([my_model]).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0))
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb b/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
index 67596211f71..7024cbd55ff 100644
--- a/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionMonitoring do
let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) }
- let(:partitioning_strategy) { double(missing_partitions: missing_partitions, current_partitions: current_partitions) }
+ let(:partitioning_strategy) { double(missing_partitions: missing_partitions, current_partitions: current_partitions, extra_partitions: extra_partitions) }
let(:table) { "some_table" }
let(:missing_partitions) do
@@ -19,6 +19,10 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionMonitoring do
[double, double]
end
+ let(:extra_partitions) do
+ [double, double, double]
+ end
+
it 'reports number of present partitions' do
subject
@@ -30,5 +34,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionMonitoring do
expect(Gitlab::Metrics.registry.get(:db_partitions_missing).get({ table: table })).to eq(missing_partitions.size)
end
+
+ it 'reports number of extra partitions' do
+ subject
+
+ expect(Gitlab::Metrics.registry.get(:db_partitions_extra).get({ table: table })).to eq(extra_partitions.size)
+ end
end
end
diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
new file mode 100644
index 00000000000..ec39e5bfee7
--- /dev/null
+++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model do
+ # PostgresForeignKey does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry
+ # in pg_class
+
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE TABLE public.referenced_table (
+ id bigserial primary key not null
+ );
+
+ CREATE TABLE public.other_referenced_table (
+ id bigserial primary key not null
+ );
+
+ CREATE TABLE public.constrained_table (
+ id bigserial primary key not null,
+ referenced_table_id bigint not null,
+ other_referenced_table_id bigint not null,
+ CONSTRAINT fk_constrained_to_referenced FOREIGN KEY(referenced_table_id) REFERENCES referenced_table(id),
+ CONSTRAINT fk_constrained_to_other_referenced FOREIGN KEY(other_referenced_table_id)
+ REFERENCES other_referenced_table(id)
+ );
+ SQL
+ end
+
+ describe '#by_referenced_table_identifier' do
+ it 'throws an error when the identifier name is not fully qualified' do
+ expect { described_class.by_referenced_table_identifier('referenced_table') }.to raise_error(ArgumentError, /not fully qualified/)
+ end
+
+ it 'finds the foreign keys for the referenced table' do
+ expected = described_class.find_by!(name: 'fk_constrained_to_referenced')
+
+ expect(described_class.by_referenced_table_identifier('public.referenced_table')).to contain_exactly(expected)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index e1832219ebf..9088719d5a4 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -40,6 +40,37 @@ RSpec.describe Gitlab::Database::PostgresIndex do
expect(types & %w(btree gist)).to eq(types)
end
+
+ context 'with leftover indexes' do
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE INDEX foobar_ccnew ON users (id);
+ CREATE INDEX foobar_ccnew1 ON users (id);
+ SQL
+ end
+
+ subject { described_class.reindexing_support.map(&:name) }
+
+ it 'excludes temporary indexes from reindexing' do
+ expect(subject).not_to include('foobar_ccnew')
+ expect(subject).not_to include('foobar_ccnew1')
+ end
+ end
+ end
+
+ describe '.reindexing_leftovers' do
+ subject { described_class.reindexing_leftovers }
+
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE INDEX foobar_ccnew ON users (id);
+ CREATE INDEX foobar_ccnew1 ON users (id);
+ SQL
+ end
+
+ it 'retrieves leftover indexes matching the /_ccnew[0-9]*$/ pattern' do
+ expect(subject.map(&:name)).to eq(%w(foobar_ccnew foobar_ccnew1))
+ end
end
describe '.not_match' do
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 8aff99544ca..550f9db2b5b 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -26,14 +26,31 @@ RSpec.describe Gitlab::Database::Reindexing do
end
end
- describe '.candidate_indexes' do
- subject { described_class.candidate_indexes }
+ describe '.cleanup_leftovers!' do
+ subject { described_class.cleanup_leftovers! }
+
+ before do
+ ApplicationRecord.connection.execute(<<~SQL)
+ CREATE INDEX foobar_ccnew ON users (id);
+ CREATE INDEX foobar_ccnew1 ON users (id);
+ SQL
+ end
- it 'retrieves regular indexes that are no left-overs from previous runs' do
- result = double
- expect(Gitlab::Database::PostgresIndex).to receive_message_chain('not_match.reindexing_support').with('\_ccnew[0-9]*$').with(no_args).and_return(result)
+ it 'drops both leftover indexes' do
+ expect_query("SET lock_timeout TO '60000ms'")
+ expect_query("DROP INDEX CONCURRENTLY IF EXISTS \"public\".\"foobar_ccnew\"")
+ expect_query("RESET idle_in_transaction_session_timeout; RESET lock_timeout")
+ expect_query("SET lock_timeout TO '60000ms'")
+ expect_query("DROP INDEX CONCURRENTLY IF EXISTS \"public\".\"foobar_ccnew1\"")
+ expect_query("RESET idle_in_transaction_session_timeout; RESET lock_timeout")
- expect(subject).to eq(result)
+ subject
+ end
+
+ def expect_query(sql)
+ expect(ApplicationRecord.connection).to receive(:execute).ordered.with(sql).and_wrap_original do |method, sql|
+ method.call(sql.sub(/CONCURRENTLY/, ''))
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/schema_migrations/context_spec.rb b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
index f3bed9b40d6..1f1943d00a3 100644
--- a/spec/lib/gitlab/database/schema_migrations/context_spec.rb
+++ b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaMigrations::Context do
- let(:connection) { ActiveRecord::Base.connection }
+ let(:connection_class) { ActiveRecord::Base }
+ let(:connection) { connection_class.connection }
let(:context) { described_class.new(connection) }
@@ -12,13 +13,65 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
expect(context.schema_directory).to eq(File.join(Rails.root, 'db/schema_migrations'))
end
- context 'multiple databases' do
- let(:connection) { Ci::BaseModel.connection }
+ context 'CI database' do
+ let(:connection_class) { Ci::CiDatabaseRecord }
it 'returns a directory path that is database specific' do
skip_if_multiple_databases_not_setup
- expect(context.schema_directory).to eq(File.join(Rails.root, 'db/ci_schema_migrations'))
+ expect(context.schema_directory).to eq(File.join(Rails.root, 'db/schema_migrations'))
+ end
+ end
+
+ context 'multiple databases' do
+ let(:connection_class) do
+ Class.new(::ApplicationRecord) do
+ self.abstract_class = true
+
+ def self.name
+ 'Gitlab::Database::SchemaMigrations::Context::TestConnection'
+ end
+ end
+ end
+
+ let(:configuration_overrides) { {} }
+
+ before do
+ connection_class.establish_connection(
+ ActiveRecord::Base
+ .connection_pool
+ .db_config
+ .configuration_hash
+ .merge(configuration_overrides)
+ )
+ end
+
+ after do
+ connection_class.remove_connection
+ end
+
+ context 'when `schema_migrations_path` is configured as string' do
+ let(:configuration_overrides) do
+ { "schema_migrations_path" => "db/ci_schema_migrations" }
+ end
+
+ it 'returns a configured directory path that' do
+ skip_if_multiple_databases_not_setup
+
+ expect(context.schema_directory).to eq(File.join(Rails.root, 'db/ci_schema_migrations'))
+ end
+ end
+
+ context 'when `schema_migrations_path` is configured as symbol' do
+ let(:configuration_overrides) do
+ { schema_migrations_path: "db/ci_schema_migrations" }
+ end
+
+ it 'returns a configured directory path that' do
+ skip_if_multiple_databases_not_setup
+
+ expect(context.schema_directory).to eq(File.join(Rails.root, 'db/ci_schema_migrations'))
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/transaction/context_spec.rb b/spec/lib/gitlab/database/transaction/context_spec.rb
new file mode 100644
index 00000000000..65d52b4d099
--- /dev/null
+++ b/spec/lib/gitlab/database/transaction/context_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Transaction::Context do
+ subject { described_class.new }
+
+ let(:data) { subject.context }
+
+ before do
+ stub_const("#{described_class}::LOG_THROTTLE", 100)
+ end
+
+ describe '#set_start_time' do
+ before do
+ subject.set_start_time
+ end
+
+ it 'sets start_time' do
+ expect(data).to have_key(:start_time)
+ end
+ end
+
+ describe '#increment_savepoints' do
+ before do
+ 2.times { subject.increment_savepoints }
+ end
+
+ it { expect(data[:savepoints]).to eq(2) }
+ end
+
+ describe '#increment_rollbacks' do
+ before do
+ 3.times { subject.increment_rollbacks }
+ end
+
+ it { expect(data[:rollbacks]).to eq(3) }
+ end
+
+ describe '#increment_releases' do
+ before do
+ 4.times { subject.increment_releases }
+ end
+
+ it { expect(data[:releases]).to eq(4) }
+ end
+
+ describe '#set_depth' do
+ before do
+ subject.set_depth(2)
+ end
+
+ it { expect(data[:depth]).to eq(2) }
+ end
+
+ describe '#track_sql' do
+ before do
+ subject.track_sql('SELECT 1')
+ subject.track_sql('SELECT * FROM users')
+ end
+
+ it { expect(data[:queries]).to eq(['SELECT 1', 'SELECT * FROM users']) }
+ end
+
+ describe '#duration' do
+ before do
+ subject.set_start_time
+ end
+
+ it { expect(subject.duration).to be >= 0 }
+ end
+
+ context 'when depth is low' do
+ it 'does not log data upon COMMIT' do
+ expect(subject).not_to receive(:application_info)
+
+ subject.commit
+ end
+
+ it 'does not log data upon ROLLBACK' do
+ expect(subject).not_to receive(:application_info)
+
+ subject.rollback
+ end
+
+ it '#should_log? returns false' do
+ expect(subject.should_log?).to be false
+ end
+ end
+
+ shared_examples 'logs transaction data' do
+ it 'logs once upon COMMIT' do
+ expect(subject).to receive(:application_info).and_call_original
+
+ 2.times { subject.commit }
+ end
+
+ it 'logs once upon ROLLBACK' do
+ expect(subject).to receive(:application_info).once
+
+ 2.times { subject.rollback }
+ end
+
+ it 'logs again when log throttle duration passes' do
+ expect(subject).to receive(:application_info).twice.and_call_original
+
+ 2.times { subject.commit }
+
+ data[:last_log_timestamp] -= (described_class::LOG_THROTTLE_DURATION + 1)
+
+ subject.commit
+ end
+
+ it '#should_log? returns true' do
+ expect(subject.should_log?).to be true
+ end
+ end
+
+ context 'when depth exceeds threshold' do
+ before do
+ subject.set_depth(described_class::LOG_DEPTH_THRESHOLD + 1)
+ end
+
+ it_behaves_like 'logs transaction data'
+ end
+
+ context 'when savepoints count exceeds threshold' do
+ before do
+ data[:savepoints] = described_class::LOG_SAVEPOINTS_THRESHOLD + 1
+ end
+
+ it_behaves_like 'logs transaction data'
+ end
+
+ context 'when duration exceeds threshold' do
+ before do
+ subject.set_start_time
+
+ data[:start_time] -= (described_class::LOG_DURATION_S_THRESHOLD + 1)
+ end
+
+ it_behaves_like 'logs transaction data'
+ end
+end
diff --git a/spec/lib/gitlab/database/transaction/observer_spec.rb b/spec/lib/gitlab/database/transaction/observer_spec.rb
new file mode 100644
index 00000000000..7aa24217dc3
--- /dev/null
+++ b/spec/lib/gitlab/database/transaction/observer_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Transaction::Observer do
+ # Use the delete DB strategy so that the test won't be wrapped in a transaction
+ describe '.instrument_transactions', :delete do
+ let(:transaction_context) { ActiveRecord::Base.connection.transaction_manager.transaction_context }
+ let(:context) { transaction_context.context }
+
+ around do |example|
+ # Emulate production environment when SQL comments come first to avoid truncation
+ Marginalia::Comment.prepend_comment = true
+ subscriber = described_class.register!
+
+ example.run
+
+ ActiveSupport::Notifications.unsubscribe(subscriber)
+ Marginalia::Comment.prepend_comment = false
+ end
+
+ it 'tracks transaction data', :aggregate_failures do
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction(requires_new: true) do
+ User.first
+
+ expect(transaction_context).to be_a(::Gitlab::Database::Transaction::Context)
+ expect(context.keys).to match_array(%i(start_time depth savepoints queries))
+ expect(context[:depth]).to eq(2)
+ expect(context[:savepoints]).to eq(1)
+ expect(context[:queries].length).to eq(1)
+ end
+ end
+
+ expect(context[:depth]).to eq(2)
+ expect(context[:savepoints]).to eq(1)
+ expect(context[:releases]).to eq(1)
+ end
+
+ describe '.extract_sql_command' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sql, :expected) do
+ 'SELECT 1' | 'SELECT 1'
+ '/* test comment */ SELECT 1' | 'SELECT 1'
+ '/* test comment */ ROLLBACK TO SAVEPOINT point1' | 'ROLLBACK TO SAVEPOINT '
+ 'SELECT 1 /* trailing comment */' | 'SELECT 1 /* trailing comment */'
+ end
+
+ with_them do
+ it do
+ expect(described_class.extract_sql_command(sql)).to eq(expected)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
index ff8e76311ae..0282a7af0df 100644
--- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -37,22 +37,20 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'when lock retry is enabled' do
let(:lock_fiber) do
Fiber.new do
- configuration = ActiveRecordSecond.configurations.find_db_config(Rails.env).configuration_hash
+ # Initiating a separate DB connection for the lock
+ conn = ActiveRecord::Base.connection_pool.checkout
- # Initiating a second DB connection for the lock
- conn = ActiveRecordSecond.establish_connection(configuration).connection
conn.transaction do
conn.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
Fiber.yield
end
- ActiveRecordSecond.remove_connection # force disconnect
+ # Releasing the connection we requested
+ ActiveRecord::Base.connection_pool.checkin(conn)
end
end
before do
- stub_const('ActiveRecordSecond', Class.new(ActiveRecord::Base))
-
lock_fiber.resume # start the transaction and lock the table
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 367f793b117..72074f06210 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -37,22 +37,19 @@ RSpec.describe Gitlab::Database::WithLockRetries do
context 'when lock retry is enabled' do
let(:lock_fiber) do
Fiber.new do
- configuration = ActiveRecordSecond.configurations.find_db_config(Rails.env).configuration_hash
-
- # Initiating a second DB connection for the lock
- conn = ActiveRecordSecond.establish_connection(configuration).connection
+ # Initiating a separate DB connection for the lock
+ conn = ActiveRecord::Base.connection_pool.checkout
conn.transaction do
conn.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
Fiber.yield
end
- ActiveRecordSecond.remove_connection # force disconnect
+ # Releasing the connection we requested
+ ActiveRecord::Base.connection_pool.checkin(conn)
end
end
before do
- stub_const('ActiveRecordSecond', Class.new(ActiveRecord::Base))
-
lock_fiber.resume # start the transaction and lock the table
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index a834e41c019..c67b5af5e3c 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -15,32 +15,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.default_pool_size' do
- before do
- allow(Gitlab::Runtime).to receive(:max_threads).and_return(7)
- end
-
- it 'returns the max thread size plus a fixed headroom of 10' do
- expect(described_class.default_pool_size).to eq(17)
- end
-
- it 'returns the max thread size plus a DB_POOL_HEADROOM if this env var is present' do
- stub_env('DB_POOL_HEADROOM', '7')
-
- expect(described_class.default_pool_size).to eq(14)
- end
- end
-
- describe '.config' do
- it 'returns a HashWithIndifferentAccess' do
- expect(described_class.config).to be_an_instance_of(HashWithIndifferentAccess)
- end
-
- it 'returns a default pool size' do
- expect(described_class.config).to include(pool: described_class.default_pool_size)
- end
- end
-
describe '.has_config?' do
context 'two tier database config' do
before do
@@ -114,108 +88,11 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.adapter_name' do
- it 'returns the name of the adapter' do
- expect(described_class.adapter_name).to be_an_instance_of(String)
- end
-
- it 'returns Unknown when using anything else' do
- allow(described_class).to receive(:postgresql?).and_return(false)
-
- expect(described_class.human_adapter_name).to eq('Unknown')
- end
- end
-
- describe '.human_adapter_name' do
- it 'returns PostgreSQL when using PostgreSQL' do
- expect(described_class.human_adapter_name).to eq('PostgreSQL')
- end
- end
-
- describe '.system_id' do
- it 'returns the PostgreSQL system identifier' do
- expect(described_class.system_id).to be_an_instance_of(Integer)
- end
- end
-
- describe '.disable_prepared_statements' do
- around do |example|
- original_config = ::Gitlab::Database.config
-
- example.run
-
- ActiveRecord::Base.establish_connection(original_config)
- end
-
- it 'disables prepared statements' do
- ActiveRecord::Base.establish_connection(::Gitlab::Database.config.merge(prepared_statements: true))
- expect(ActiveRecord::Base.connection.prepared_statements).to eq(true)
-
- expect(ActiveRecord::Base).to receive(:establish_connection)
- .with(a_hash_including({ 'prepared_statements' => false })).and_call_original
-
- described_class.disable_prepared_statements
-
- expect(ActiveRecord::Base.connection.prepared_statements).to eq(false)
- end
- end
-
- describe '.postgresql?' do
- subject { described_class.postgresql? }
-
- it { is_expected.to satisfy { |val| val == true || val == false } }
- end
-
- describe '.version' do
- around do |example|
- described_class.instance_variable_set(:@version, nil)
- example.run
- described_class.instance_variable_set(:@version, nil)
- end
-
- context "on postgresql" do
- it "extracts the version number" do
- allow(described_class).to receive(:database_version)
- .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0")
-
- expect(described_class.version).to eq '9.4.4'
- end
- end
-
- it 'memoizes the result' do
- count = ActiveRecord::QueryRecorder
- .new { 2.times { described_class.version } }
- .count
-
- expect(count).to eq(1)
- end
- end
-
- describe '.postgresql_minimum_supported_version?' do
- it 'returns false when using PostgreSQL 10' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.postgresql_minimum_supported_version?).to eq(false)
- end
-
- it 'returns false when using PostgreSQL 11' do
- allow(described_class).to receive(:version).and_return('11')
-
- expect(described_class.postgresql_minimum_supported_version?).to eq(false)
- end
-
- it 'returns true when using PostgreSQL 12' do
- allow(described_class).to receive(:version).and_return('12')
-
- expect(described_class.postgresql_minimum_supported_version?).to eq(true)
- end
- end
-
describe '.check_postgres_version_and_print_warning' do
subject { described_class.check_postgres_version_and_print_warning }
it 'prints a warning if not compliant with minimum postgres version' do
- allow(described_class).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
expect(Kernel).to receive(:warn).with(/You are using PostgreSQL/)
@@ -223,7 +100,7 @@ RSpec.describe Gitlab::Database do
end
it 'doesnt print a warning if compliant with minimum postgres version' do
- allow(described_class).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(true)
expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/)
@@ -231,7 +108,7 @@ RSpec.describe Gitlab::Database do
end
it 'doesnt print a warning in Rails runner environment' do
- allow(described_class).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
allow(Gitlab::Runtime).to receive(:rails_runner?).and_return(true)
expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/)
@@ -240,13 +117,13 @@ RSpec.describe Gitlab::Database do
end
it 'ignores ActiveRecord errors' do
- allow(described_class).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError)
+ allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError)
expect { subject }.not_to raise_error
end
it 'ignores Postgres errors' do
- allow(described_class).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error)
+ allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error)
expect { subject }.not_to raise_error
end
@@ -262,244 +139,19 @@ RSpec.describe Gitlab::Database do
it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'}
end
- describe '.with_connection_pool' do
- it 'creates a new connection pool and disconnect it after used' do
- closed_pool = nil
-
- described_class.with_connection_pool(1) do |pool|
- pool.with_connection do |connection|
- connection.execute('SELECT 1 AS value')
- end
-
- expect(pool).to be_connected
-
- closed_pool = pool
- end
-
- expect(closed_pool).not_to be_connected
- end
-
- it 'disconnects the pool even an exception was raised' do
- error = Class.new(RuntimeError)
- closed_pool = nil
-
- begin
- described_class.with_connection_pool(1) do |pool|
- pool.with_connection do |connection|
- connection.execute('SELECT 1 AS value')
- end
-
- closed_pool = pool
-
- raise error, 'boom'
- end
- rescue error
- end
-
- expect(closed_pool).not_to be_connected
- end
- end
-
- describe '.bulk_insert' do
- before do
- allow(described_class).to receive(:connection).and_return(connection)
- allow(connection).to receive(:quote_column_name, &:itself)
- allow(connection).to receive(:quote, &:itself)
- allow(connection).to receive(:execute)
- end
-
- let(:connection) { double(:connection) }
-
- let(:rows) do
- [
- { a: 1, b: 2, c: 3 },
- { c: 6, a: 4, b: 5 }
- ]
- end
-
- it 'does nothing with empty rows' do
- expect(connection).not_to receive(:execute)
-
- described_class.bulk_insert('test', [])
- end
-
- it 'uses the ordering from the first row' do
- expect(connection).to receive(:execute) do |sql|
- expect(sql).to include('(1, 2, 3)')
- expect(sql).to include('(4, 5, 6)')
- end
-
- described_class.bulk_insert('test', rows)
- end
-
- it 'quotes column names' do
- expect(connection).to receive(:quote_column_name).with(:a)
- expect(connection).to receive(:quote_column_name).with(:b)
- expect(connection).to receive(:quote_column_name).with(:c)
-
- described_class.bulk_insert('test', rows)
- end
-
- it 'quotes values' do
- 1.upto(6) do |i|
- expect(connection).to receive(:quote).with(i)
- end
-
- described_class.bulk_insert('test', rows)
- end
-
- it 'does not quote values of a column in the disable_quote option' do
- [1, 2, 4, 5].each do |i|
- expect(connection).to receive(:quote).with(i)
- end
-
- described_class.bulk_insert('test', rows, disable_quote: :c)
- end
-
- it 'does not quote values of columns in the disable_quote option' do
- [2, 5].each do |i|
- expect(connection).to receive(:quote).with(i)
- end
-
- described_class.bulk_insert('test', rows, disable_quote: [:a, :c])
- end
-
- it 'handles non-UTF-8 data' do
- expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
- end
-
- context 'when using PostgreSQL' do
- it 'allows the returning of the IDs of the inserted rows' do
- result = double(:result, values: [['10']])
-
- expect(connection)
- .to receive(:execute)
- .with(/RETURNING id/)
- .and_return(result)
-
- ids = described_class
- .bulk_insert('test', [{ number: 10 }], return_ids: true)
-
- expect(ids).to eq([10])
- end
-
- it 'allows setting the upsert to do nothing' do
- expect(connection)
- .to receive(:execute)
- .with(/ON CONFLICT DO NOTHING/)
-
- described_class
- .bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing)
- end
- end
- end
-
- describe '.create_connection_pool' do
- it 'creates a new connection pool with specific pool size' do
- pool = described_class.create_connection_pool(5)
-
- begin
- expect(pool)
- .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
-
- expect(pool.db_config.pool).to eq(5)
- ensure
- pool.disconnect!
- end
- end
-
- it 'allows setting of a custom hostname' do
- pool = described_class.create_connection_pool(5, '127.0.0.1')
-
- begin
- expect(pool.db_config.host).to eq('127.0.0.1')
- ensure
- pool.disconnect!
- end
- end
-
- it 'allows setting of a custom hostname and port' do
- pool = described_class.create_connection_pool(5, '127.0.0.1', 5432)
-
- begin
- expect(pool.db_config.host).to eq('127.0.0.1')
- expect(pool.db_config.configuration_hash[:port]).to eq(5432)
- ensure
- pool.disconnect!
- end
- end
- end
-
- describe '.cached_column_exists?' do
- it 'only retrieves data once' do
- expect(ActiveRecord::Base.connection).to receive(:columns).once.and_call_original
-
- 2.times do
- expect(described_class.cached_column_exists?(:projects, :id)).to be_truthy
- expect(described_class.cached_column_exists?(:projects, :bogus_column)).to be_falsey
- end
- end
- end
-
- describe '.cached_table_exists?' do
- it 'only retrieves data once per table' do
- expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:projects).once.and_call_original
- expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:bogus_table_name).once.and_call_original
-
- 2.times do
- expect(described_class.cached_table_exists?(:projects)).to be_truthy
- expect(described_class.cached_table_exists?(:bogus_table_name)).to be_falsey
- end
- end
-
- it 'returns false when database does not exist' do
- expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, 'broken' }
-
- expect(described_class.cached_table_exists?(:projects)).to be(false)
- end
- end
-
- describe '.exists?' do
- it 'returns true if `ActiveRecord::Base.connection` succeeds' do
- expect(ActiveRecord::Base).to receive(:connection)
-
- expect(described_class.exists?).to be(true)
- end
-
- it 'returns false if `ActiveRecord::Base.connection` fails' do
- expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, 'broken' }
-
- expect(described_class.exists?).to be(false)
- end
- end
-
- describe '.get_write_location' do
- it 'returns a string' do
+ describe '.db_config_name' do
+ it 'returns the db_config name for the connection' do
connection = ActiveRecord::Base.connection
- expect(described_class.get_write_location(connection)).to be_a(String)
- end
-
- it 'returns nil if there are no results' do
- connection = double(select_all: [])
-
- expect(described_class.get_write_location(connection)).to be_nil
- end
- end
-
- describe '.dbname' do
- it 'returns the dbname for the connection' do
- connection = ActiveRecord::Base.connection
-
- expect(described_class.dbname(connection)).to be_a(String)
- expect(described_class.dbname(connection)).to eq(connection.pool.db_config.database)
+ expect(described_class.db_config_name(connection)).to be_a(String)
+ expect(described_class.db_config_name(connection)).to eq(connection.pool.db_config.name)
end
context 'when the pool is a NullPool' do
it 'returns unknown' do
connection = double(:active_record_connection, pool: ActiveRecord::ConnectionAdapters::NullPool.new)
- expect(described_class.dbname(connection)).to eq('unknown')
+ expect(described_class.db_config_name(connection)).to eq('unknown')
end
end
end
@@ -516,42 +168,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.read_only?' do
- it 'returns false' do
- expect(described_class.read_only?).to be_falsey
- end
- end
-
- describe '.db_read_only?' do
- before do
- allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
- end
-
- it 'detects a read-only database' do
- allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }])
-
- expect(described_class.db_read_only?).to be_truthy
- end
-
- it 'detects a read-only database' do
- allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => true }])
-
- expect(described_class.db_read_only?).to be_truthy
- end
-
- it 'detects a read-write database' do
- allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
-
- expect(described_class.db_read_only?).to be_falsey
- end
-
- it 'detects a read-write database' do
- allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }])
-
- expect(described_class.db_read_only?).to be_falsey
- end
- end
-
describe '#sanitize_timestamp' do
let(:max_timestamp) { Time.at((1 << 31) - 1) }
@@ -574,6 +190,18 @@ RSpec.describe Gitlab::Database do
end
end
+ describe '.read_only?' do
+ it 'returns false' do
+ expect(described_class.read_only?).to eq(false)
+ end
+ end
+
+ describe '.read_write' do
+ it 'returns true' do
+ expect(described_class.read_write?).to eq(true)
+ end
+ end
+
describe 'ActiveRecordBaseTransactionMetrics' do
def subscribe_events
events = []
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index e76a5d3fe32..c0ac40e3249 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -110,6 +110,60 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
end
end
+ context 'when email contains reply' do
+ shared_examples 'no content message' do
+ context 'when email contains quoted text only' do
+ let(:email_raw) { fixture_file('emails/no_content_with_quote.eml') }
+
+ it 'raises an EmptyEmailError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
+ end
+ end
+
+ context 'when email contains quoted text and quick commands only' do
+ let(:email_raw) { fixture_file('emails/commands_only_reply.eml') }
+
+ it 'does not create a discussion' do
+ expect { receiver.execute }.not_to change { noteable.notes.count }
+ end
+ end
+ end
+
+ context 'when noteable is not an issue' do
+ let_it_be(:note) { create(:note_on_merge_request, project: project) }
+
+ it_behaves_like 'no content message'
+
+ context 'when email contains text, quoted text and quick commands' do
+ let(:email_raw) { fixture_file('emails/commands_in_reply.eml') }
+
+ it 'creates a discussion without appended reply' do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ new_note = noteable.notes.last
+
+ expect(new_note.note).not_to include('<details><summary>...</summary>')
+ end
+ end
+ end
+
+ context 'when noteable is an issue' do
+ let_it_be(:note) { create(:note_on_issue, project: project) }
+
+ it_behaves_like 'no content message'
+
+ context 'when email contains text, quoted text and quick commands' do
+ let(:email_raw) { fixture_file('emails/commands_in_reply.eml') }
+
+ it 'creates a discussion with appended reply' do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ new_note = noteable.notes.last
+
+ expect(new_note.note).to include('<details><summary>...</summary>')
+ end
+ end
+ end
+ end
+
context 'when note is not a discussion' do
let(:note) { create(:note_on_merge_request, project: project) }
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb
new file mode 100644
index 00000000000..b5c3415fe12
--- /dev/null
+++ b/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Message::InProductMarketing::AdminVerify do
+ let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+
+ let(:series) { 0 }
+
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
+
+ describe 'public methods' do
+ it 'returns value for series', :aggregate_failures do
+ expect(message.subject_line).to eq 'Create a custom CI runner with just a few clicks'
+ expect(message.tagline).to be_nil
+ expect(message.title).to eq 'Spin up an autoscaling runner in GitLab'
+ expect(message.subtitle).to eq 'Use our AWS cloudformation template to spin up your runners in just a few clicks!'
+ expect(message.body_line1).to be_empty
+ expect(message.body_line2).to be_empty
+ expect(message.cta_text).to eq 'Create a custom runner'
+ expect(message.logo_path).to eq 'mailers/in_product_marketing/admin_verify-0.png'
+ end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to eq('This is email 1 of 1 in the Admin series.') }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include('This is email 1 of 1 in the Admin series', Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb
new file mode 100644
index 00000000000..daeacef53f6
--- /dev/null
+++ b/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Message::InProductMarketing::TeamShort do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+
+ let(:series) { 0 }
+
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
+
+ describe 'public methods' do
+ it 'returns value for series', :aggregate_failures do
+ expect(message.subject_line).to eq 'Team up in GitLab for greater efficiency'
+ expect(message.tagline).to be_nil
+ expect(message.title).to eq 'Turn coworkers into collaborators'
+ expect(message.subtitle).to eq 'Invite your team today to build better code (and processes) together'
+ expect(message.body_line1).to be_empty
+ expect(message.body_line2).to be_empty
+ expect(message.cta_text).to eq 'Invite your colleagues today'
+ expect(message.logo_path).to eq 'mailers/in_product_marketing/team-0.png'
+ end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to include('This is email 1 of 4 in the Team series') }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include('This is email 1 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
index f72994fcce1..eca8ba1df00 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
@@ -23,6 +23,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
expect(message.body_line2).to be_present
expect(message.cta_text).to be_present
end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to include("This is email #{series + 2} of 4 in the Team series") }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include("This is email #{series + 2} of 4 in the Team series", Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
end
context 'with series 2' do
@@ -37,6 +57,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
expect(message.body_line2).to be_present
expect(message.cta_text).to be_present
end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to include('This is email 4 of 4 in the Team series') }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include('This is email 4 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb
new file mode 100644
index 00000000000..ebad4672eb3
--- /dev/null
+++ b/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Message::InProductMarketing::TrialShort do
+ let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+
+ let(:series) { 0 }
+
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
+
+ describe 'public methods' do
+ it 'returns value for series', :aggregate_failures do
+ expect(message.subject_line).to eq 'Be a DevOps hero'
+ expect(message.tagline).to be_nil
+ expect(message.title).to eq 'Expand your DevOps journey with a free GitLab trial'
+ expect(message.subtitle).to eq 'Start your trial today to experience single application success and discover all the features of GitLab Ultimate for free!'
+ expect(message.body_line1).to be_empty
+ expect(message.body_line2).to be_empty
+ expect(message.cta_text).to eq 'Start a trial'
+ expect(message.logo_path).to eq 'mailers/in_product_marketing/trial-0.png'
+ end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to eq('This is email 1 of 4 in the Trial series.') }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include('This is email 1 of 4 in the Trial series', Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
index 5f7639a9ed6..3e18b8e35b6 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
@@ -23,6 +23,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do
expect(message.body_line2).to be_present
expect(message.cta_text).to be_present
end
+
+ describe '#progress' do
+ subject { message.progress }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
+ end
+
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it { is_expected.to eq("This is email #{series + 2} of 4 in the Trial series.") }
+ end
+
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it { is_expected.to include("This is email #{series + 2} of 4 in the Trial series", Gitlab::Routing.url_helpers.profile_notifications_url) }
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index bc4c6cf007d..3b01b568fb4 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -228,5 +228,21 @@ RSpec.describe Gitlab::Email::ReplyParser do
BODY
)
end
+
+ it "appends trimmed reply when when append_reply option is true" do
+ body = <<-BODY.strip_heredoc.chomp
+ The reply by email functionality should be extended to allow creating a new issue by email.
+ even when the email is forwarded to the project which may include lines that begin with ">"
+
+ there should be a quote below this line:
+ BODY
+
+ reply = <<-BODY.strip_heredoc.chomp
+ > this is a quote
+ BODY
+
+ expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { append_reply: true }))
+ .to contain_exactly(body, reply)
+ end
end
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index cf0d1577314..268ac5dcc21 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe Gitlab::EncodingHelper do
+ using RSpec::Parameterized::TableSyntax
+
let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } }
let(:binary_string) { File.read(Rails.root + "spec/fixtures/dk.png") }
@@ -90,6 +92,22 @@ RSpec.describe Gitlab::EncodingHelper do
end
end
+ describe '#encode_utf8_no_detect' do
+ where(:input, :expected) do
+ "abcd" | "abcd"
+ "DzDzDz" | "DzDzDz"
+ "\xC7\xB2\xC7DzDzDz" | "Dz�DzDzDz"
+ "🐤🐤🐤🐤\xF0\x9F\x90" | "🐤🐤🐤🐤�"
+ end
+
+ with_them do
+ it 'drops invalid UTF-8' do
+ expect(ext_class.encode_utf8_no_detect(input.dup.force_encoding(Encoding::ASCII_8BIT))).to eq(expected)
+ expect(ext_class.encode_utf8_no_detect(input)).to eq(expected)
+ end
+ end
+ end
+
describe '#encode_utf8' do
[
["nil", nil, nil],
diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
index 5c496d653b2..577d59798da 100644
--- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do
- let(:error_event) { build(:error_tracking_error_event) }
+ let(:error_event) { build(:error_tracking_sentry_error_event) }
describe '.decorate' do
subject(:decorate) { described_class.decorate(error_event) }
diff --git a/spec/lib/gitlab/etag_caching/router/restful_spec.rb b/spec/lib/gitlab/etag_caching/router/restful_spec.rb
index 877789b320f..1f5cac09b6d 100644
--- a/spec/lib/gitlab/etag_caching/router/restful_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router/restful_spec.rb
@@ -87,12 +87,18 @@ RSpec.describe Gitlab::EtagCaching::Router::Restful do
end
it 'matches the environments path' do
- result = match_route('/my-group/my-project/environments.json')
+ result = match_route('/my-group/my-project/-/environments.json')
expect(result).to be_present
expect(result.name).to eq 'environments'
end
+ it 'does not match the operations environments list path' do
+ result = match_route('/-/operations/environments.json')
+
+ expect(result).not_to be_present
+ end
+
it 'matches pipeline#show endpoint' do
result = match_route('/my-group/my-project/-/pipelines/2.json')
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
index 7a619c9f155..8535d72a61f 100644
--- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
+ include TrackingHelpers
+
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
backwards_compatible_test_experiment: {
@@ -43,7 +45,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
let(:cookie_value) { nil }
before do
- request.headers['DNT'] = do_not_track if do_not_track.present?
+ stub_do_not_track(do_not_track) if do_not_track.present?
request.cookies[:experimentation_subject_id] = cookie_value if cookie_value
get :index
@@ -242,7 +244,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'do not track is disabled' do
before do
- request.headers['DNT'] = '0'
+ stub_do_not_track('0')
end
it 'does track the event' do
@@ -260,7 +262,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'do not track enabled' do
before do
- request.headers['DNT'] = '1'
+ stub_do_not_track('1')
end
it 'does not track the event' do
@@ -396,7 +398,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'do not track disabled' do
before do
- request.headers['DNT'] = '0'
+ stub_do_not_track('0')
end
it 'pushes the right parameters to gon' do
@@ -414,7 +416,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'do not track enabled' do
before do
- request.headers['DNT'] = '1'
+ stub_do_not_track('1')
end
it 'does not push data to gon' do
@@ -525,7 +527,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'is disabled' do
before do
- request.headers['DNT'] = '0'
+ stub_do_not_track('0')
stub_experiment_for_subject(test_experiment: false)
end
@@ -538,7 +540,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'is enabled' do
before do
- request.headers['DNT'] = '1'
+ stub_do_not_track('1')
end
it 'does not call add_user on the Experiment model' do
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 10bfa9e8d0e..c486538a260 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -2,22 +2,6 @@
require 'spec_helper'
-# As each associated, backwards-compatible experiment gets cleaned up and removed from the EXPERIMENTS list, its key will also get removed from this list. Once the list here is empty, we can remove the backwards compatibility code altogether.
-# Originally created as part of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733 for https://gitlab.com/gitlab-org/gitlab/-/issues/270858.
-RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
- it 'temporarily ensures we know what experiments exist for backwards compatibility' do
- expected_experiment_keys = [
- :invite_members_empty_group_version_a,
- :contact_sales_btn_in_app
- ]
-
- backwards_compatible_experiment_keys = described_class.filter { |_, v| v[:use_backwards_compatible_subject_index] }.keys
-
- expect(backwards_compatible_experiment_keys).not_to be_empty, "Oh, hey! Let's clean up that :use_backwards_compatible_subject_index stuff now :D"
- expect(backwards_compatible_experiment_keys).to match(expected_experiment_keys)
- end
-end
-
RSpec.describe Gitlab::Experimentation do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb
index ec32afcfb7b..b300498e898 100644
--- a/spec/lib/gitlab/fake_application_settings_spec.rb
+++ b/spec/lib/gitlab/fake_application_settings_spec.rb
@@ -6,27 +6,35 @@ RSpec.describe Gitlab::FakeApplicationSettings do
let(:defaults) do
described_class.defaults.merge(
foobar: 'asdf',
- 'test?' => 123
+ 'test?'.to_sym => 123,
+ # these two settings have no default in ApplicationSettingImplementation,
+ # so we need to set one here
+ domain_denylist: [],
+ archive_builds_in_seconds: nil
)
end
let(:setting) { described_class.new(defaults) }
- it 'wraps OpenStruct variables properly' do
+ it 'defines methods for default attributes' do
expect(setting.password_authentication_enabled_for_web).to be_truthy
expect(setting.signup_enabled).to be_truthy
expect(setting.foobar).to eq('asdf')
end
- it 'defines predicate methods' do
+ it 'defines predicate methods for boolean properties' do
expect(setting.password_authentication_enabled_for_web?).to be_truthy
expect(setting.signup_enabled?).to be_truthy
end
- it 'does not define a predicate method' do
+ it 'does not define a predicate method for non-boolean properties' do
expect(setting.foobar?).to be_nil
end
+ it 'returns nil for undefined attributes' do
+ expect(setting.does_not_exist).to be_nil
+ end
+
it 'does not override an existing predicate method' do
expect(setting.test?).to eq(123)
end
diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
new file mode 100644
index 00000000000..a46846e9820
--- /dev/null
+++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
+ let_it_be(:user) { build(:user) }
+ let_it_be(:fake_template) do
+ Object.new.tap do |template|
+ template.extend ActionView::Helpers::FormHelper
+ template.extend ActionView::Helpers::FormOptionsHelper
+ template.extend ActionView::Helpers::TagHelper
+ template.extend ActionView::Context
+ end
+ end
+
+ let_it_be(:form_builder) { described_class.new(:user, user, fake_template, {}) }
+
+ describe '#gitlab_ui_checkbox_component' do
+ let(:optional_args) { {} }
+
+ subject(:checkbox_html) { form_builder.gitlab_ui_checkbox_component(:view_diffs_file_by_file, "Show one file at a time on merge request's Changes tab", **optional_args) }
+
+ context 'without optional arguments' do
+ it 'renders correct html' do
+ expected_html = <<~EOS
+ <div class="gl-form-checkbox custom-control custom-checkbox">
+ <input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
+ <input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
+ <label class="custom-control-label" for="user_view_diffs_file_by_file">
+ Show one file at a time on merge request&#39;s Changes tab
+ </label>
+ </div>
+ EOS
+
+ expect(checkbox_html).to eq(html_strip_whitespace(expected_html))
+ end
+ end
+
+ context 'with optional arguments' do
+ let(:optional_args) do
+ {
+ help_text: 'Instead of all the files changed, show only one file at a time.',
+ checkbox_options: { class: 'checkbox-foo-bar' },
+ label_options: { class: 'label-foo-bar' },
+ checked_value: '3',
+ unchecked_value: '1'
+ }
+ end
+
+ it 'renders help text' do
+ expected_html = <<~EOS
+ <div class="gl-form-checkbox custom-control custom-checkbox">
+ <input name="user[view_diffs_file_by_file]" type="hidden" value="1" />
+ <input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
+ <label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file">
+ <span>Show one file at a time on merge request&#39;s Changes tab</span>
+ <p class="help-text">Instead of all the files changed, show only one file at a time.</p>
+ </label>
+ </div>
+ EOS
+
+ expect(checkbox_html).to eq(html_strip_whitespace(expected_html))
+ end
+
+ it 'passes arguments to `check_box` method' do
+ allow(fake_template).to receive(:check_box).and_return('')
+
+ checkbox_html
+
+ expect(fake_template).to have_received(:check_box).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-input checkbox-foo-bar), object: user }, '3', '1')
+ end
+
+ it 'passes arguments to `label` method' do
+ allow(fake_template).to receive(:label).and_return('')
+
+ checkbox_html
+
+ expect(fake_template).to have_received(:label).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-label label-foo-bar), object: user })
+ end
+ end
+ end
+
+ private
+
+ def html_strip_whitespace(html)
+ html.lines.map(&:strip).join('')
+ end
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 49f1e6e994f..f58bab52cfa 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -369,11 +369,15 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
commits.map { |c| c.id }
end
- it 'has 1 element' do
- expect(subject.size).to eq(1)
+ it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
+
+ context 'between_uses_list_commits FF disabled' do
+ before do
+ stub_feature_flags(between_uses_list_commits: false)
+ end
+
+ it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
end
- it { is_expected.to include(SeedRepo::Commit::ID) }
- it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
describe '.shas_with_signatures' do
diff --git a/spec/lib/gitlab/git/commit_stats_spec.rb b/spec/lib/gitlab/git/commit_stats_spec.rb
new file mode 100644
index 00000000000..29d3909efec
--- /dev/null
+++ b/spec/lib/gitlab/git/commit_stats_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::Git::CommitStats, :seed_helper do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
+ let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+
+ def verify_stats!
+ stats = described_class.new(repository, commit)
+
+ expect(stats).to have_attributes(
+ additions: eq(11),
+ deletions: eq(6),
+ total: eq(17)
+ )
+ end
+
+ it 'returns commit stats and caches them', :use_clean_rails_redis_caching do
+ expect(repository.gitaly_commit_client).to receive(:commit_stats).with(commit.id).and_call_original
+
+ verify_stats!
+
+ expect(Rails.cache.fetch("commit_stats:group/project:#{commit.id}")).to eq([11, 6])
+
+ expect(repository.gitaly_commit_client).not_to receive(:commit_stats)
+
+ verify_stats!
+ end
+end
diff --git a/spec/lib/gitlab/git/conflict/file_spec.rb b/spec/lib/gitlab/git/conflict/file_spec.rb
index 454a48a1d3a..6eb7a7e394e 100644
--- a/spec/lib/gitlab/git/conflict/file_spec.rb
+++ b/spec/lib/gitlab/git/conflict/file_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Git::Conflict::File do
- let(:conflict) { { theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } }
+ let(:conflict) { { ancestor: { path: 'ancestor' }, theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } }
let(:invalid_content) { described_class.new(nil, nil, conflict, (+"a\xC4\xFC").force_encoding(Encoding::ASCII_8BIT)) }
let(:valid_content) { described_class.new(nil, nil, conflict, (+"Espa\xC3\xB1a").force_encoding(Encoding::ASCII_8BIT)) }
@@ -48,4 +48,18 @@ RSpec.describe Gitlab::Git::Conflict::File do
end
end
end
+
+ describe '#path' do
+ it 'returns our_path' do
+ expect(valid_content.path).to eq(conflict[:ours][:path])
+ end
+
+ context 'when our_path is not present' do
+ let(:conflict) { { ancestor: { path: 'ancestor' }, theirs: { path: 'theirs', mode: 33188 }, ours: { path: '', mode: 0 } } }
+
+ it 'returns their_path' do
+ expect(valid_content.path).to eq(conflict[:theirs][:path])
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/remote_mirror_spec.rb b/spec/lib/gitlab/git/remote_mirror_spec.rb
index 0954879f6bd..4b827e5d2d0 100644
--- a/spec/lib/gitlab/git/remote_mirror_spec.rb
+++ b/spec/lib/gitlab/git/remote_mirror_spec.rb
@@ -6,30 +6,17 @@ RSpec.describe Gitlab::Git::RemoteMirror do
describe '#update' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:ref_name) { 'foo' }
let(:url) { 'https://example.com' }
let(:options) { { only_branches_matching: ['master'], ssh_key: 'KEY', known_hosts: 'KNOWN HOSTS', keep_divergent_refs: true } }
- subject(:remote_mirror) { described_class.new(repository, ref_name, url, **options) }
+ subject(:remote_mirror) { described_class.new(repository, url, **options) }
- shared_examples 'an update' do
- it 'delegates to the Gitaly client' do
- expect(repository.gitaly_remote_client)
- .to receive(:update_remote_mirror)
- .with(ref_name, url, ['master'], ssh_key: 'KEY', known_hosts: 'KNOWN HOSTS', keep_divergent_refs: true)
-
- remote_mirror.update # rubocop:disable Rails/SaveBang
- end
- end
-
- context 'with url' do
- it_behaves_like 'an update'
- end
-
- context 'without url' do
- let(:url) { nil }
+ it 'delegates to the Gitaly client' do
+ expect(repository.gitaly_remote_client)
+ .to receive(:update_remote_mirror)
+ .with(url, ['master'], ssh_key: 'KEY', known_hosts: 'KNOWN HOSTS', keep_divergent_refs: true)
- it_behaves_like 'an update'
+ remote_mirror.update # rubocop:disable Rails/SaveBang
end
it 'wraps gitaly errors' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 706bcdea291..29e7a1dce1d 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -491,6 +491,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
describe '#fetch_remote' do
+ let(:url) { 'http://example.clom' }
+
it 'delegates to the gitaly RepositoryService' do
ssh_auth = double(:ssh_auth)
expected_opts = {
@@ -500,17 +502,17 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
timeout: described_class::GITLAB_PROJECTS_TIMEOUT,
prune: false,
check_tags_changed: false,
- url: nil,
- refmap: nil
+ refmap: nil,
+ http_authorization_header: ""
}
- expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts)
+ expect(repository.gitaly_repository_client).to receive(:fetch_remote).with(url, expected_opts)
- repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false, check_tags_changed: false)
+ repository.fetch_remote(url, ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false, check_tags_changed: false)
end
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do
- subject { repository.fetch_remote('remote-name') }
+ subject { repository.fetch_remote(url) }
end
end
@@ -584,29 +586,29 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.to receive(:find_remote_root_ref).and_call_original
- expect(repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to eq 'master'
+ expect(repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to eq 'master'
end
it 'returns UTF-8' do
- expect(repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to be_utf8
+ expect(repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to be_utf8
end
it 'returns nil when remote name is nil' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.not_to receive(:find_remote_root_ref)
- expect(repository.find_remote_root_ref(nil, nil)).to be_nil
+ expect(repository.find_remote_root_ref(nil)).to be_nil
end
it 'returns nil when remote name is empty' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.not_to receive(:find_remote_root_ref)
- expect(repository.find_remote_root_ref('', '')).to be_nil
+ expect(repository.find_remote_root_ref('')).to be_nil
end
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RemoteService, :find_remote_root_ref do
- subject { repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL) }
+ subject { repository.find_remote_root_ref(SeedHelper::GITLAB_GIT_TEST_REPO_URL) }
end
end
@@ -950,44 +952,23 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:expected_commits) { 1 }
let(:revisions) { [new_commit] }
- shared_examples 'an enumeration of new commits' do
- it 'enumerates commits' do
- commits = repository.new_commits(revisions).to_a
-
- expect(commits.size).to eq(expected_commits)
- commits.each do |commit|
- expect(commit.id).to eq(new_commit)
- expect(commit.message).to eq("Message")
- end
+ before do
+ expect_next_instance_of(Gitlab::GitalyClient::CommitService) do |service|
+ expect(service)
+ .to receive(:list_commits)
+ .with([new_commit, '--not', '--all'])
+ .and_call_original
end
end
- context 'with list_commits disabled' do
- before do
- stub_feature_flags(list_commits: false)
+ it 'enumerates commits' do
+ commits = repository.new_commits(revisions).to_a
- expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
- expect(service)
- .to receive(:list_new_commits)
- .with(new_commit)
- .and_call_original
- end
+ expect(commits.size).to eq(expected_commits)
+ commits.each do |commit|
+ expect(commit.id).to eq(new_commit)
+ expect(commit.message).to eq("Message")
end
-
- it_behaves_like 'an enumeration of new commits'
- end
-
- context 'with list_commits enabled' do
- before do
- expect_next_instance_of(Gitlab::GitalyClient::CommitService) do |service|
- expect(service)
- .to receive(:list_commits)
- .with([new_commit, '--not', '--all'])
- .and_call_original
- end
- end
-
- it_behaves_like 'an enumeration of new commits'
end
end
@@ -1750,43 +1731,61 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- describe '#write_config' do
- before do
- repository_rugged.config["gitlab.fullpath"] = repository_path
- end
+ describe '#set_full_path' do
+ shared_examples '#set_full_path' do
+ before do
+ repository_rugged.config["gitlab.fullpath"] = repository_path
+ end
- context 'is given a path' do
- it 'writes it to disk' do
- repository.write_config(full_path: "not-the/real-path.git")
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.set_full_path(full_path: "not-the/real-path.git")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = not-the/real-path.git")
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
+ end
end
- end
- context 'it is given an empty path' do
- it 'does not write it to disk' do
- repository.write_config(full_path: "")
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.set_full_path(full_path: "")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = #{repository_path}")
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository_path}")
+ end
+ end
+
+ context 'repository does not exist' do
+ it 'raises NoRepository and does not call Gitaly WriteConfig' do
+ repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project')
+
+ expect(repository.gitaly_repository_client).not_to receive(:set_full_path)
+
+ expect do
+ repository.set_full_path(full_path: 'foo/bar.git')
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
end
end
- context 'repository does not exist' do
- it 'raises NoRepository and does not call Gitaly WriteConfig' do
- repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project')
+ context 'with :set_full_path enabled' do
+ before do
+ stub_feature_flags(set_full_path: true)
+ end
- expect(repository.gitaly_repository_client).not_to receive(:write_config)
+ it_behaves_like '#set_full_path'
+ end
- expect do
- repository.write_config(full_path: 'foo/bar.git')
- end.to raise_error(Gitlab::Git::Repository::NoRepository)
+ context 'with :set_full_path disabled' do
+ before do
+ stub_feature_flags(set_full_path: false)
end
+
+ it_behaves_like '#set_full_path'
end
end
@@ -1813,34 +1812,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- describe '#delete_config' do
- let(:repository) { mutable_repository }
- let(:entries) do
- {
- 'test.foo1' => 'bla bla',
- 'test.foo2' => 1234,
- 'test.foo3' => true
- }
- end
-
- it 'can delete config settings' do
- entries.each do |key, value|
- repository_rugged.config[key] = value
- end
-
- expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil
-
- # Workaround for https://github.com/libgit2/rugged/issues/785: If
- # Gitaly changes .gitconfig while Rugged has the file loaded
- # Rugged::Repository#each_key will report stale values unless a
- # lookup is done first.
- expect(repository_rugged.config['test.foo1']).to be_nil
- config_keys = repository_rugged.config.each_key.to_a
- expect(config_keys).not_to include('test.foo1')
- expect(config_keys).not_to include('test.foo2')
- end
- end
-
describe '#merge_to_ref' do
let(:repository) { mutable_repository }
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
@@ -2001,47 +1972,6 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
- describe 'remotes' do
- let(:repository) { mutable_repository }
- let(:remote_name) { 'my-remote' }
- let(:url) { 'http://my-repo.git' }
-
- after do
- ensure_seeds
- end
-
- describe '#add_remote' do
- let(:mirror_refmap) { '+refs/*:refs/*' }
-
- it 'added the remote' do
- begin
- repository_rugged.remotes.delete(remote_name)
- rescue Rugged::ConfigError
- end
-
- repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
-
- expect(repository_rugged.remotes[remote_name]).not_to be_nil
- expect(repository_rugged.config["remote.#{remote_name}.mirror"]).to eq('true')
- expect(repository_rugged.config["remote.#{remote_name}.prune"]).to eq('true')
- expect(repository_rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap)
- end
- end
-
- describe '#remove_remote' do
- it 'removes the remote' do
- repository_rugged.remotes.create(remote_name, url)
-
- expect(repository.remove_remote(remote_name)).to be true
-
- # Since we deleted the remote via Gitaly, Rugged doesn't know
- # this changed underneath it. Let's refresh the Rugged repo.
- repository_rugged = Rugged::Repository.new(repository_path)
- expect(repository_rugged.remotes[remote_name]).to be_nil
- end
- end
- end
-
describe '#bundle_to_disk' do
let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index b6ff76c5e1c..79ae47f8a7b 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do
it { expect(tag.tagger.timezone).to eq("+0200") }
end
- describe 'signed tag' do
+ shared_examples 'signed tag' do
let(:project) { create(:project, :repository) }
let(:tag) { project.repository.find_tag('v1.1.1') }
@@ -54,6 +54,18 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do
it { expect(tag.tagger.timezone).to eq("+0100") }
end
+ context 'with :get_tag_signatures enabled' do
+ it_behaves_like 'signed tag'
+ end
+
+ context 'with :get_tag_signatures disabled' do
+ before do
+ stub_feature_flags(get_tag_signatures: false)
+ end
+
+ it_behaves_like 'signed tag'
+ end
+
it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
end
@@ -77,6 +89,75 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do
end
end
+ describe '.extract_signature_lazily' do
+ let(:project) { create(:project, :repository) }
+
+ subject { described_class.extract_signature_lazily(project.repository, tag_id).itself }
+
+ context 'when the tag is signed' do
+ let(:tag_id) { project.repository.find_tag('v1.1.1').id }
+
+ it 'returns signature and signed text' do
+ signature, signed_text = subject
+
+ expect(signature).to eq(X509Helpers::User1.signed_tag_signature.chomp)
+ expect(signature).to be_a_binary_string
+ expect(signed_text).to eq(X509Helpers::User1.signed_tag_base_data)
+ expect(signed_text).to be_a_binary_string
+ end
+ end
+
+ context 'when the tag has no signature' do
+ let(:tag_id) { project.repository.find_tag('v1.0.0').id }
+
+ it 'returns empty signature and message as signed text' do
+ signature, signed_text = subject
+
+ expect(signature).to be_empty
+ expect(signed_text).to eq(X509Helpers::User1.unsigned_tag_base_data)
+ expect(signed_text).to be_a_binary_string
+ end
+ end
+
+ context 'when the tag cannot be found' do
+ let(:tag_id) { Gitlab::Git::BLANK_SHA }
+
+ it 'raises GRPC::Internal' do
+ expect { subject }.to raise_error(GRPC::Internal)
+ end
+ end
+
+ context 'when the tag ID is invalid' do
+ let(:tag_id) { '4b4918a572fa86f9771e5ba40fbd48e' }
+
+ it 'raises GRPC::Internal' do
+ expect { subject }.to raise_error(GRPC::Internal)
+ end
+ end
+
+ context 'when loading signatures in batch once' do
+ it 'fetches signatures in batch once' do
+ tag_ids = [project.repository.find_tag('v1.1.1').id, project.repository.find_tag('v1.0.0').id]
+ signatures = tag_ids.map do |tag_id|
+ described_class.extract_signature_lazily(repository, tag_id)
+ end
+
+ other_repository = double(:repository)
+ described_class.extract_signature_lazily(other_repository, tag_ids.first)
+
+ expect(described_class).to receive(:batch_signature_extraction)
+ .with(repository, tag_ids)
+ .once
+ .and_return({})
+
+ expect(described_class).not_to receive(:batch_signature_extraction)
+ .with(other_repository, tag_ids.first)
+
+ 2.times { signatures.each(&:itself) }
+ end
+ end
+ end
+
describe 'tag into from Gitaly tag' do
context 'message_size != message.size' do
let(:gitaly_tag) { build(:gitaly_tag, message: ''.b, message_size: message_size) }
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index c44d7e44751..f11d84bd8d3 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -6,29 +6,44 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
shared_examples :repo do
- let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
+ subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, pagination_params) }
- it { expect(tree).to be_kind_of Array }
- it { expect(tree.empty?).to be_falsey }
- it { expect(tree.count(&:dir?)).to eq(2) }
- it { expect(tree.count(&:file?)).to eq(10) }
- it { expect(tree.count(&:submodule?)).to eq(2) }
+ let(:sha) { SeedRepo::Commit::ID }
+ let(:path) { nil }
+ let(:recursive) { false }
+ let(:pagination_params) { nil }
- it 'returns an empty array when called with an invalid ref' do
- expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([])
+ let(:entries) { tree.first }
+ let(:cursor) { tree.second }
+
+ it { expect(entries).to be_kind_of Array }
+ it { expect(entries.empty?).to be_falsey }
+ it { expect(entries.count(&:dir?)).to eq(2) }
+ it { expect(entries.count(&:file?)).to eq(10) }
+ it { expect(entries.count(&:submodule?)).to eq(2) }
+ it { expect(cursor&.next_cursor).to be_blank }
+
+ context 'with an invalid ref' do
+ let(:sha) { 'foobar-does-not-exist' }
+
+ it { expect(entries).to eq([]) }
+ it { expect(cursor).to be_nil }
end
- it 'returns a list of tree objects' do
- entries = described_class.where(repository, SeedRepo::Commit::ID, 'files', true)
+ context 'when path is provided' do
+ let(:path) { 'files' }
+ let(:recursive) { true }
- expect(entries.map(&:path)).to include('files/html',
- 'files/markdown/ruby-style-guide.md')
- expect(entries.count).to be >= 10
- expect(entries).to all(be_a(Gitlab::Git::Tree))
+ it 'returns a list of tree objects' do
+ expect(entries.map(&:path)).to include('files/html',
+ 'files/markdown/ruby-style-guide.md')
+ expect(entries.count).to be >= 10
+ expect(entries).to all(be_a(Gitlab::Git::Tree))
+ end
end
describe '#dir?' do
- let(:dir) { tree.select(&:dir?).first }
+ let(:dir) { entries.select(&:dir?).first }
it { expect(dir).to be_kind_of Gitlab::Git::Tree }
it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') }
@@ -41,7 +56,8 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
context :subdir do
# rubocop: disable Rails/FindBy
# This is not ActiveRecord where..first
- let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
+ let(:path) { 'files' }
+ let(:subdir) { entries.first }
# rubocop: enable Rails/FindBy
it { expect(subdir).to be_kind_of Gitlab::Git::Tree }
@@ -55,7 +71,8 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
context :subdir_file do
# rubocop: disable Rails/FindBy
# This is not ActiveRecord where..first
- let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
+ let(:path) { 'files/ruby' }
+ let(:subdir_file) { entries.first }
# rubocop: enable Rails/FindBy
it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree }
@@ -68,10 +85,11 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
context :flat_path do
let(:filename) { 'files/flat/path/correct/content.txt' }
- let(:oid) { create_file(filename) }
+ let(:sha) { create_file(filename) }
+ let(:path) { 'files/flat' }
# rubocop: disable Rails/FindBy
# This is not ActiveRecord where..first
- let(:subdir_file) { Gitlab::Git::Tree.where(repository, oid, 'files/flat').first }
+ let(:subdir_file) { entries.first }
# rubocop: enable Rails/FindBy
let(:repository_rugged) { Rugged::Repository.new(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) }
@@ -116,7 +134,7 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
end
describe '#file?' do
- let(:file) { tree.select(&:file?).first }
+ let(:file) { entries.select(&:file?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') }
@@ -125,21 +143,21 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
end
describe '#readme?' do
- let(:file) { tree.select(&:readme?).first }
+ let(:file) { entries.select(&:readme?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('README.md') }
end
describe '#contributing?' do
- let(:file) { tree.select(&:contributing?).first }
+ let(:file) { entries.select(&:contributing?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('CONTRIBUTING.md') }
end
describe '#submodule?' do
- let(:submodule) { tree.select(&:submodule?).first }
+ let(:submodule) { entries.select(&:submodule?).first }
it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') }
@@ -149,7 +167,16 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
end
describe '.where with Gitaly enabled' do
- it_behaves_like :repo
+ it_behaves_like :repo do
+ context 'with pagination parameters' do
+ let(:pagination_params) { { limit: 3, page_token: nil } }
+
+ it 'returns paginated list of tree objects' do
+ expect(entries.count).to eq(3)
+ expect(cursor.next_cursor).to be_present
+ end
+ end
+ end
end
describe '.where with Rugged enabled', :enable_rugged do
@@ -161,6 +188,15 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do
described_class.where(repository, SeedRepo::Commit::ID, 'files', false)
end
- it_behaves_like :repo
+ it_behaves_like :repo do
+ context 'with pagination parameters' do
+ let(:pagination_params) { { limit: 3, page_token: nil } }
+
+ it 'does not support pagination' do
+ expect(entries.count).to be >= 10
+ expect(cursor).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index 3b85e3ddd1d..d690a4b2db4 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -382,12 +382,11 @@ RSpec.describe Gitlab::GitAccessSnippet do
it_behaves_like 'a push to repository to make it over the limit'
end
- shared_examples_for 'a change with GIT_OBJECT_DIRECTORY_RELATIVE env var unset' do
+ context 'a change with GIT_OBJECT_DIRECTORY_RELATIVE env var unset' do
let(:change_size) { 200 }
before do
- stub_feature_flags(git_access_batched_changes_size: batched)
- allow(snippet.repository).to receive(expected_call).and_return(
+ allow(snippet.repository).to receive(:blobs).and_return(
[double(:blob, size: change_size)]
)
end
@@ -396,20 +395,6 @@ RSpec.describe Gitlab::GitAccessSnippet do
it_behaves_like 'a push to repository below the limit'
it_behaves_like 'a push to repository to make it over the limit'
end
-
- context 'when batched computation is enabled' do
- let(:batched) { true }
- let(:expected_call) { :blobs }
-
- it_behaves_like 'a change with GIT_OBJECT_DIRECTORY_RELATIVE env var unset'
- end
-
- context 'when batched computation is disabled' do
- let(:batched) { false }
- let(:expected_call) { :new_blobs }
-
- it_behaves_like 'a change with GIT_OBJECT_DIRECTORY_RELATIVE env var unset'
- end
end
describe 'HEAD realignment' do
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 22c29403255..a0e2d43cf45 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -169,7 +169,11 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
describe '#tree_entries' do
+ subject { client.tree_entries(repository, revision, path, recursive, pagination_params) }
+
let(:path) { '/' }
+ let(:recursive) { false }
+ let(:pagination_params) { nil }
it 'sends a get_tree_entries message' do
expect_any_instance_of(Gitaly::CommitService::Stub)
@@ -177,7 +181,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([])
- client.tree_entries(repository, revision, path, false)
+ is_expected.to eq([[], nil])
end
context 'with UTF-8 params strings' do
@@ -190,7 +194,26 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([])
- client.tree_entries(repository, revision, path, false)
+ is_expected.to eq([[], nil])
+ end
+ end
+
+ context 'with pagination parameters' do
+ let(:pagination_params) { { limit: 3, page_token: nil } }
+
+ it 'responds with a pagination cursor' do
+ pagination_cursor = Gitaly::PaginationCursor.new(next_cursor: 'aabbccdd')
+ response = Gitaly::GetTreeEntriesResponse.new(
+ entries: [],
+ pagination_cursor: pagination_cursor
+ )
+
+ expect_any_instance_of(Gitaly::CommitService::Stub)
+ .to receive(:get_tree_entries)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([response])
+
+ is_expected.to eq([[], pagination_cursor])
end
end
end
@@ -320,6 +343,92 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
end
+ describe '#list_new_commits' do
+ let(:revisions) { [revision] }
+ let(:gitaly_commits) { create_list(:gitaly_commit, 3) }
+ let(:commits) { gitaly_commits.map { |c| Gitlab::Git::Commit.new(repository, c) }}
+
+ subject { client.list_new_commits(revisions, allow_quarantine: allow_quarantine) }
+
+ shared_examples 'a #list_all_commits message' do
+ it 'sends a list_all_commits message' do
+ expected_repository = repository.gitaly_repository.dup
+ expected_repository.git_alternate_object_directories = Google::Protobuf::RepeatedField.new(:string)
+
+ expect_next_instance_of(Gitaly::CommitService::Stub) do |service|
+ expect(service).to receive(:list_all_commits)
+ .with(gitaly_request_with_params(repository: expected_repository), kind_of(Hash))
+ .and_return([Gitaly::ListAllCommitsResponse.new(commits: gitaly_commits)])
+ end
+
+ expect(subject).to eq(commits)
+ end
+ end
+
+ shared_examples 'a #list_commits message' do
+ it 'sends a list_commits message' do
+ expect_next_instance_of(Gitaly::CommitService::Stub) do |service|
+ expect(service).to receive(:list_commits)
+ .with(gitaly_request_with_params(revisions: revisions + %w[--not --all]), kind_of(Hash))
+ .and_return([Gitaly::ListCommitsResponse.new(commits: gitaly_commits)])
+ end
+
+ expect(subject).to eq(commits)
+ end
+ end
+
+ before do
+ ::Gitlab::GitalyClient.clear_stubs!
+
+ allow(Gitlab::Git::HookEnv)
+ .to receive(:all)
+ .with(repository.gl_repository)
+ .and_return(git_env)
+ end
+
+ context 'with hook environment' do
+ let(:git_env) do
+ {
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => '.git/objects',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two']
+ }
+ end
+
+ context 'with allowed quarantine' do
+ let(:allow_quarantine) { true }
+
+ it_behaves_like 'a #list_all_commits message'
+ end
+
+ context 'with disallowed quarantine' do
+ let(:allow_quarantine) { false }
+
+ it_behaves_like 'a #list_commits message'
+ end
+ end
+
+ context 'without hook environment' do
+ let(:git_env) do
+ {
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => '',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => []
+ }
+ end
+
+ context 'with allowed quarantine' do
+ let(:allow_quarantine) { true }
+
+ it_behaves_like 'a #list_commits message'
+ end
+
+ context 'with disallowed quarantine' do
+ let(:allow_quarantine) { false }
+
+ it_behaves_like 'a #list_commits message'
+ end
+ end
+ end
+
describe '#commit_stats' do
let(:request) do
Gitaly::CommitStatsRequest.new(
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index 0bb8628af6c..0eecdfcb630 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -9,22 +9,37 @@ RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
target_repository = target_project.repository.raw
target_gitaly_repository = target_repository.gitaly_repository
+ ancestor_path_1 = 'ancestor/path/1'
our_path_1 = 'our/path/1'
their_path_1 = 'their/path/1'
our_mode_1 = 0744
commit_oid_1 = 'f00'
content_1 = 'content of the first file'
+ ancestor_path_2 = 'ancestor/path/2'
our_path_2 = 'our/path/2'
their_path_2 = 'their/path/2'
our_mode_2 = 0600
commit_oid_2 = 'ba7'
content_2 = 'content of the second file'
- header_1 = double(repository: target_gitaly_repository, commit_oid: commit_oid_1,
- our_path: our_path_1, their_path: their_path_1, our_mode: our_mode_1)
- header_2 = double(repository: target_gitaly_repository, commit_oid: commit_oid_2,
- our_path: our_path_2, their_path: their_path_2, our_mode: our_mode_2)
+ header_1 = double(
+ repository: target_gitaly_repository,
+ commit_oid: commit_oid_1,
+ ancestor_path: ancestor_path_1,
+ our_path: our_path_1,
+ their_path: their_path_1,
+ our_mode: our_mode_1
+ )
+
+ header_2 = double(
+ repository: target_gitaly_repository,
+ commit_oid: commit_oid_2,
+ ancestor_path: ancestor_path_2,
+ our_path: our_path_2,
+ their_path: their_path_2,
+ our_mode: our_mode_2
+ )
messages = [
double(files: [double(header: header_1), double(header: nil, content: content_1[0..5])]),
@@ -39,6 +54,7 @@ RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
expect(conflict_files.size).to be(2)
expect(conflict_files[0].content).to eq(content_1)
+ expect(conflict_files[0].ancestor_path).to eq(ancestor_path_1)
expect(conflict_files[0].their_path).to eq(their_path_1)
expect(conflict_files[0].our_path).to eq(our_path_1)
expect(conflict_files[0].our_mode).to be(our_mode_1)
@@ -46,6 +62,7 @@ RSpec.describe Gitlab::GitalyClient::ConflictFilesStitcher do
expect(conflict_files[0].commit_oid).to eq(commit_oid_1)
expect(conflict_files[1].content).to eq(content_2)
+ expect(conflict_files[1].ancestor_path).to eq(ancestor_path_2)
expect(conflict_files[1].their_path).to eq(their_path_2)
expect(conflict_files[1].our_path).to eq(our_path_2)
expect(conflict_files[1].our_mode).to be(our_mode_2)
diff --git a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
index e90cb966917..89a41ae71f3 100644
--- a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
@@ -15,18 +15,31 @@ RSpec.describe Gitlab::GitalyClient::ConflictsService do
end
describe '#list_conflict_files' do
+ let(:allow_tree_conflicts) { false }
let(:request) do
Gitaly::ListConflictFilesRequest.new(
- repository: target_gitaly_repository, our_commit_oid: our_commit_oid,
- their_commit_oid: their_commit_oid
+ repository: target_gitaly_repository,
+ our_commit_oid: our_commit_oid,
+ their_commit_oid: their_commit_oid,
+ allow_tree_conflicts: allow_tree_conflicts
)
end
- it 'sends an RPC request' do
- expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:list_conflict_files)
- .with(request, kind_of(Hash)).and_return([].to_enum)
+ shared_examples_for 'listing conflicts' do
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::ConflictsService::Stub).to receive(:list_conflict_files)
+ .with(request, kind_of(Hash)).and_return([].to_enum)
+
+ client.list_conflict_files(allow_tree_conflicts: allow_tree_conflicts)
+ end
+ end
+
+ it_behaves_like 'listing conflicts'
+
+ context 'when allow_tree_conflicts is set to true' do
+ let(:allow_tree_conflicts) { true }
- client.list_conflict_files
+ it_behaves_like 'listing conflicts'
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index a4c6e30bba8..e19be965e68 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -178,6 +178,17 @@ RSpec.describe Gitlab::GitalyClient::RefService do
end
end
+ describe '#get_tag_signatures' do
+ it 'sends a get_tag_signatures message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:get_tag_signatures)
+ .with(gitaly_request_with_params(tag_revisions: ['some_tag_id']), kind_of(Hash))
+ .and_return([])
+
+ client.get_tag_signatures(['some_tag_id'])
+ end
+ end
+
describe '#find_ref_name', :seed_helper do
subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') }
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 2ec5f70be76..3d0f8358406 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -6,36 +6,9 @@ RSpec.describe Gitlab::GitalyClient::RemoteService do
let(:project) { create(:project) }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
- let(:remote_name) { 'my-remote' }
let(:client) { described_class.new(project.repository) }
- describe '#add_remote' do
- let(:url) { 'http://my-repo.git' }
- let(:mirror_refmap) { :all_refs }
-
- it 'sends an add_remote message' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:add_remote)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return(double(:add_remote_response))
-
- client.add_remote(remote_name, url, mirror_refmap)
- end
- end
-
- describe '#remove_remote' do
- it 'sends an remove_remote message and returns the result value' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:remove_remote)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return(double(result: true))
-
- expect(client.remove_remote(remote_name)).to be(true)
- end
- end
-
describe '#find_remote_root_ref' do
- let(:remote) { 'origin' }
let(:url) { 'http://git.example.com/my-repo.git' }
let(:auth) { 'Basic secret' }
let(:expected_params) { { remote_url: url, http_authorization_header: auth } }
@@ -47,7 +20,7 @@ RSpec.describe Gitlab::GitalyClient::RemoteService do
.with(gitaly_request_with_params(expected_params), kind_of(Hash))
.and_return(double(ref: 'master'))
- expect(client.find_remote_root_ref(remote, url, auth)).to eq 'master'
+ expect(client.find_remote_root_ref(url, auth)).to eq 'master'
end
it 'ensure ref is a valid UTF-8 string' do
@@ -57,39 +30,24 @@ RSpec.describe Gitlab::GitalyClient::RemoteService do
.with(gitaly_request_with_params(expected_params), kind_of(Hash))
.and_return(double(ref: "an_invalid_ref_\xE5"))
- expect(client.find_remote_root_ref(remote, url, auth)).to eq "an_invalid_ref_å"
+ expect(client.find_remote_root_ref(url, auth)).to eq "an_invalid_ref_å"
end
end
describe '#update_remote_mirror' do
- let(:ref_name) { 'remote_mirror_1' }
let(:only_branches_matching) { %w[my-branch master] }
let(:ssh_key) { 'KEY' }
let(:known_hosts) { 'KNOWN HOSTS' }
+ let(:url) { 'http:://git.example.com/my-repo.git' }
+ let(:expected_params) { { remote: Gitaly::UpdateRemoteMirrorRequest::Remote.new(url: url) } }
- shared_examples 'an update' do
- it 'sends an update_remote_mirror message' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:update_remote_mirror)
- .with(array_including(gitaly_request_with_params(expected_params)), kind_of(Hash))
- .and_return(double(:update_remote_mirror_response))
-
- client.update_remote_mirror(ref_name, url, only_branches_matching, ssh_key: ssh_key, known_hosts: known_hosts, keep_divergent_refs: true)
- end
- end
-
- context 'with remote name' do
- let(:url) { nil }
- let(:expected_params) { { ref_name: ref_name } }
-
- it_behaves_like 'an update'
- end
-
- context 'with remote URL' do
- let(:url) { 'http:://git.example.com/my-repo.git' }
- let(:expected_params) { { remote: Gitaly::UpdateRemoteMirrorRequest::Remote.new(url: url) } }
+ it 'sends an update_remote_mirror message' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:update_remote_mirror)
+ .with(array_including(gitaly_request_with_params(expected_params)), kind_of(Hash))
+ .and_return(double(:update_remote_mirror_response))
- it_behaves_like 'an update'
+ client.update_remote_mirror(url, only_branches_matching, ssh_key: ssh_key, known_hosts: known_hosts, keep_divergent_refs: true)
end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 53805d67f9f..4b037d3f836 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -122,89 +122,75 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
describe '#fetch_remote' do
- shared_examples 'a fetch' do
- it 'sends a fetch_remote_request message' do
- expected_remote_params = Gitaly::Remote.new(
- url: url, http_authorization_header: "", mirror_refmaps: [])
-
- expected_request = gitaly_request_with_params(
- remote: remote,
- remote_params: url ? expected_remote_params : nil,
- ssh_key: '',
- known_hosts: '',
- force: false,
- no_tags: false,
- no_prune: false,
- check_tags_changed: false
- )
-
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:fetch_remote)
- .with(expected_request, kind_of(Hash))
- .and_return(double(value: true))
-
- client.fetch_remote(remote, url: url, refmap: nil, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false)
- end
+ let(:url) { 'https://example.com/git/repo.git' }
+
+ it 'sends a fetch_remote_request message' do
+ expected_request = gitaly_request_with_params(
+ remote_params: Gitaly::Remote.new(
+ url: url,
+ http_authorization_header: "",
+ mirror_refmaps: []
+ ),
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false,
+ check_tags_changed: false
+ )
- context 'SSH auth' do
- where(:ssh_mirror_url, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do
- false | false | 'key' | 'known_hosts' | {}
- false | true | 'key' | 'known_hosts' | {}
- true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' }
- true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' }
- true | true | 'key' | nil | { ssh_key: 'key' }
- true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' }
- true | true | nil | nil | {}
- true | true | '' | '' | {}
- end
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:fetch_remote)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
- with_them do
- let(:ssh_auth) do
- double(
- :ssh_auth,
- ssh_mirror_url?: ssh_mirror_url,
- ssh_key_auth?: ssh_key_auth,
- ssh_private_key: ssh_private_key,
- ssh_known_hosts: ssh_known_hosts
- )
- end
-
- it do
- expected_remote_params = Gitaly::Remote.new(
- url: url, http_authorization_header: "", mirror_refmaps: [])
-
- expected_request = gitaly_request_with_params({
- remote: remote,
- remote_params: url ? expected_remote_params : nil,
- ssh_key: '',
- known_hosts: '',
- force: false,
- no_tags: false,
- no_prune: false
- }.update(expected_params))
-
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:fetch_remote)
- .with(expected_request, kind_of(Hash))
- .and_return(double(value: true))
-
- client.fetch_remote(remote, url: url, refmap: nil, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1)
- end
- end
- end
+ client.fetch_remote(url, refmap: nil, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false)
end
- context 'with remote' do
- it_behaves_like 'a fetch' do
- let(:remote) { 'remote-name' }
- let(:url) { nil }
+ context 'SSH auth' do
+ where(:ssh_mirror_url, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do
+ false | false | 'key' | 'known_hosts' | {}
+ false | true | 'key' | 'known_hosts' | {}
+ true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' }
+ true | true | 'key' | nil | { ssh_key: 'key' }
+ true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | nil | nil | {}
+ true | true | '' | '' | {}
end
- end
- context 'with URL' do
- it_behaves_like 'a fetch' do
- let(:remote) { "" }
- let(:url) { 'https://example.com/git/repo.git' }
+ with_them do
+ let(:ssh_auth) do
+ double(
+ :ssh_auth,
+ ssh_mirror_url?: ssh_mirror_url,
+ ssh_key_auth?: ssh_key_auth,
+ ssh_private_key: ssh_private_key,
+ ssh_known_hosts: ssh_known_hosts
+ )
+ end
+
+ it do
+ expected_request = gitaly_request_with_params({
+ remote_params: Gitaly::Remote.new(
+ url: url,
+ http_authorization_header: "",
+ mirror_refmaps: []
+ ),
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false
+ }.update(expected_params))
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:fetch_remote)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fetch_remote(url, refmap: nil, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1)
+ end
end
end
end
@@ -333,4 +319,17 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
client.replicate(source_repository)
end
end
+
+ describe '#set_full_path' do
+ let(:path) { 'repo/path' }
+
+ it 'sends a set_full_path message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:set_full_path)
+ .with(gitaly_request_with_params(path: path), kind_of(Hash))
+ .and_return(double)
+
+ client.set_full_path(path)
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 63dce51c5da..6c94973b5a8 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -3,8 +3,20 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::BulkImporting do
- let(:importer) do
- Class.new { include(Gitlab::GithubImport::BulkImporting) }.new
+ let(:project) { instance_double(Project, id: 1) }
+ let(:importer) { MyImporter.new(project, double) }
+ let(:importer_class) do
+ Class.new do
+ include Gitlab::GithubImport::BulkImporting
+
+ def object_type
+ :object_type
+ end
+ end
+ end
+
+ before do
+ stub_const 'MyImporter', importer_class
end
describe '#build_database_rows' do
@@ -21,6 +33,24 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
.with(object)
.and_return(false)
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_type: :github,
+ project_id: 1,
+ importer: 'MyImporter',
+ message: '1 object_types fetched'
+ )
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .with(
+ project,
+ :object_type,
+ :fetched,
+ value: 1
+ )
+
enum = [[object, 1]].to_enum
expect(importer.build_database_rows(enum)).to eq([{ title: 'Foo' }])
@@ -37,6 +67,24 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
.with(object)
.and_return(true)
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_type: :github,
+ project_id: 1,
+ importer: 'MyImporter',
+ message: '0 object_types fetched'
+ )
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .with(
+ project,
+ :object_type,
+ :fetched,
+ value: 0
+ )
+
enum = [[object, 1]].to_enum
expect(importer.build_database_rows(enum)).to be_empty
@@ -48,12 +96,32 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
rows = [{ title: 'Foo' }] * 10
model = double(:model, table_name: 'kittens')
- expect(Gitlab::Database)
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .twice
+ .with(
+ import_type: :github,
+ project_id: 1,
+ importer: 'MyImporter',
+ message: '5 object_types imported'
+ )
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .twice
+ .with(
+ project,
+ :object_type,
+ :imported,
+ value: 5
+ )
+
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.ordered
.with('kittens', rows.first(5))
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.ordered
.with('kittens', rows.last(5))
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index 9eea85526f5..0af840d2c10 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter do
describe '#execute' do
context 'when the merge request no longer exists' do
it 'does not import anything' do
- expect(Gitlab::Database).not_to receive(:bulk_insert)
+ expect(Gitlab::Database.main).not_to receive(:bulk_insert)
importer.execute
end
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter do
.to receive(:author_id_for)
.and_return([user.id, true])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
LegacyDiffNote.table_name,
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter do
.to receive(:author_id_for)
.and_return([project.creator_id, false])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
LegacyDiffNote.table_name,
@@ -133,7 +133,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter do
.to receive(:author_id_for)
.and_return([project.creator_id, false])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index fb826c987e1..0926000428c 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi
.with(issue.assignees[1])
.and_return(5)
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
IssueAssignee.table_name,
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index 6d143f78c66..241a0fef600 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
.and_return(1)
freeze_time do
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
LabelLink.table_name,
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
.with('bug')
.and_return(nil)
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(LabelLink.table_name, [])
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index 8ee534734f0..a2c7d51214a 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
- let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let_it_be(:project) { create(:project, :import_started) }
+
let(:client) { double(:client) }
let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" }
@@ -61,27 +62,12 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
.and_raise(exception)
end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- message: 'importer failed',
- import_source: :github,
- project_id: project.id,
- parallel: false,
- importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter',
- 'error.message': 'Invalid Project URL'
- )
- end
-
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
+ expect(Gitlab::Import::ImportFailureService)
+ .to receive(:track)
.with(
- exception,
- import_source: :github,
- parallel: false,
project_id: project.id,
- importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter'
+ exception: exception,
+ error_source: 'Gitlab::GithubImport::Importer::LfsObjectImporter'
).and_call_original
importer.execute
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index ef0bb90db4a..820f46c7286 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([user.id, true])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
Note.table_name,
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([project.creator_id, false])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(
Note.table_name,
@@ -115,7 +115,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
context 'when the noteable does not exist' do
it 'does not import the note' do
- expect(Gitlab::Database).not_to receive(:bulk_insert)
+ expect(Gitlab::Database.main).not_to receive(:bulk_insert)
importer.execute
end
@@ -134,7 +134,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([user.id, true])
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index 133d515246a..067b8b09516 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
end
end
- shared_examples '#update_repository' do
+ describe '#update_repository' do
it 'updates the repository' do
importer = described_class.new(project, client)
@@ -162,6 +162,10 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
.to receive(:increment)
.and_call_original
+ expect(project.repository)
+ .to receive(:fetch_remote)
+ .with(url, forced: false, refmap: Gitlab::GithubImport.refmap)
+
freeze_time do
importer.update_repository
@@ -170,28 +174,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
end
end
- describe '#update_repository with :fetch_remote_params enabled' do
- before do
- stub_feature_flags(fetch_remote_params: true)
- expect(project.repository)
- .to receive(:fetch_remote)
- .with('github', forced: false, url: url, refmap: Gitlab::GithubImport.refmap)
- end
-
- it_behaves_like '#update_repository'
- end
-
- describe '#update_repository with :fetch_remote_params disabled' do
- before do
- stub_feature_flags(fetch_remote_params: false)
- expect(project.repository)
- .to receive(:fetch_remote)
- .with('github', forced: false)
- end
-
- it_behaves_like '#update_repository'
- end
-
describe '#update_repository?' do
let(:importer) { described_class.new(project, client) }
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
index 08be350f0f9..c5fa67e50aa 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
@@ -27,100 +27,62 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do
end
describe '#each_object_to_import', :clean_gitlab_redis_cache do
- context 'when github_review_importer_query_only_unimported_merge_requests is enabled' do
- before do
- stub_feature_flags(github_review_importer_query_only_unimported_merge_requests: true)
- end
-
- let(:merge_request) do
- create(
- :merged_merge_request,
- iid: 999,
- source_project: project,
- target_project: project
- )
- end
-
- let(:review) { double(id: 1) }
-
- it 'fetches the pull requests reviews data' do
- page = double(objects: [review], number: 1)
-
- expect(review)
- .to receive(:merge_request_id=)
- .with(merge_request.id)
-
- expect(client)
- .to receive(:each_page)
- .exactly(:once) # ensure to be cached on the second call
- .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 1)
- .and_yield(page)
+ let(:merge_request) do
+ create(
+ :merged_merge_request,
+ iid: 999,
+ source_project: project,
+ target_project: project
+ )
+ end
- expect { |b| subject.each_object_to_import(&b) }
- .to yield_with_args(review)
+ let(:review) { double(id: 1) }
- subject.each_object_to_import {}
- end
+ it 'fetches the pull requests reviews data' do
+ page = double(objects: [review], number: 1)
- it 'skips cached pages' do
- Gitlab::GithubImport::PageCounter
- .new(project, "merge_request/#{merge_request.id}/pull_request_reviews")
- .set(2)
+ expect(review)
+ .to receive(:merge_request_id=)
+ .with(merge_request.id)
- expect(review).not_to receive(:merge_request_id=)
+ expect(client)
+ .to receive(:each_page)
+ .exactly(:once) # ensure to be cached on the second call
+ .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 1)
+ .and_yield(page)
- expect(client)
- .to receive(:each_page)
- .exactly(:once) # ensure to be cached on the second call
- .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 2)
+ expect { |b| subject.each_object_to_import(&b) }
+ .to yield_with_args(review)
- subject.each_object_to_import {}
- end
+ subject.each_object_to_import {}
+ end
- it 'skips cached merge requests' do
- Gitlab::Cache::Import::Caching.set_add(
- "github-importer/merge_request/already-imported/#{project.id}",
- merge_request.id
- )
+ it 'skips cached pages' do
+ Gitlab::GithubImport::PageCounter
+ .new(project, "merge_request/#{merge_request.id}/pull_request_reviews")
+ .set(2)
- expect(review).not_to receive(:merge_request_id=)
+ expect(review).not_to receive(:merge_request_id=)
- expect(client).not_to receive(:each_page)
+ expect(client)
+ .to receive(:each_page)
+ .exactly(:once) # ensure to be cached on the second call
+ .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 2)
- subject.each_object_to_import {}
- end
+ subject.each_object_to_import {}
end
- context 'when github_review_importer_query_only_unimported_merge_requests is disabled' do
- before do
- stub_feature_flags(github_review_importer_query_only_unimported_merge_requests: false)
- end
-
- it 'fetchs the merged pull requests data' do
- merge_request = create(
- :merged_merge_request,
- iid: 999,
- source_project: project,
- target_project: project
- )
-
- review = double
-
- expect(review)
- .to receive(:merge_request_id=)
- .with(merge_request.id)
+ it 'skips cached merge requests' do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/merge_request/already-imported/#{project.id}",
+ merge_request.id
+ )
- allow(client)
- .to receive(:pull_request_reviews)
- .exactly(:once) # ensure to be cached on the second call
- .with('github/repo', merge_request.iid)
- .and_return([review])
+ expect(review).not_to receive(:merge_request_id=)
- expect { |b| subject.each_object_to_import(&b) }
- .to yield_with_args(review)
+ expect(client).not_to receive(:each_page)
- subject.each_object_to_import {}
- end
+ subject.each_object_to_import {}
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 3839303b881..58a8fb1b7e4 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -202,7 +202,7 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(repository)
.to receive(:fetch_as_mirror)
- .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github')
+ .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true)
service = double
expect(Repositories::HousekeepingService)
@@ -211,17 +211,6 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(importer.import_repository).to eq(true)
end
-
- it 'marks the import as failed when an error was raised' do
- expect(project).to receive(:ensure_repository)
- .and_raise(Gitlab::Git::Repository::NoRepository)
-
- expect(importer)
- .to receive(:fail_import)
- .and_return(false)
-
- expect(importer.import_repository).to eq(false)
- end
end
describe '#import_wiki_repository' do
@@ -234,28 +223,40 @@ RSpec.describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(importer.import_wiki_repository).to eq(true)
end
- it 'marks the import as failed and creates an empty repo if an error was raised' do
- expect(wiki_repository)
- .to receive(:import_repository)
- .with(importer.wiki_url)
- .and_raise(Gitlab::Git::CommandError)
+ context 'when it raises a Gitlab::Git::CommandError' do
+ context 'when the error is not a "repository not exported"' do
+ it 'creates the wiki and re-raise the exception' do
+ exception = Gitlab::Git::CommandError.new
- expect(importer)
- .to receive(:fail_import)
- .and_return(false)
+ expect(wiki_repository)
+ .to receive(:import_repository)
+ .with(importer.wiki_url)
+ .and_raise(exception)
- expect(project)
- .to receive(:create_wiki)
+ expect(project)
+ .to receive(:create_wiki)
- expect(importer.import_wiki_repository).to eq(false)
- end
- end
+ expect { importer.import_wiki_repository }
+ .to raise_error(exception)
+ end
+ end
+
+ context 'when the error is a "repository not exported"' do
+ it 'returns true' do
+ exception = Gitlab::Git::CommandError.new('repository not exported')
- describe '#fail_import' do
- it 'marks the import as failed' do
- expect(project.import_state).to receive(:mark_as_failed).with('foo')
+ expect(wiki_repository)
+ .to receive(:import_repository)
+ .with(importer.wiki_url)
+ .and_raise(exception)
- expect(importer.fail_import('foo')).to eq(false)
+ expect(project)
+ .not_to receive(:create_wiki)
+
+ expect(importer.import_wiki_repository)
+ .to eq(true)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/logger_spec.rb b/spec/lib/gitlab/github_import/logger_spec.rb
new file mode 100644
index 00000000000..6fd0f5db93e
--- /dev/null
+++ b/spec/lib/gitlab/github_import/logger_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Logger do
+ subject(:logger) { described_class.new('/dev/null') }
+
+ let(:now) { Time.zone.now }
+
+ describe '#format_message' do
+ before do
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ end
+
+ it 'formats strings' do
+ output = subject.format_message('INFO', now, 'test', 'Hello world')
+
+ expect(Gitlab::Json.parse(output)).to eq({
+ 'severity' => 'INFO',
+ 'time' => now.utc.iso8601(3),
+ 'message' => 'Hello world',
+ 'correlation_id' => 'new-correlation-id',
+ 'feature_category' => 'importers',
+ 'import_type' => 'github'
+ })
+ end
+
+ it 'formats hashes' do
+ output = subject.format_message('INFO', now, 'test', { hello: 1 })
+
+ expect(Gitlab::Json.parse(output)).to eq({
+ 'severity' => 'INFO',
+ 'time' => now.utc.iso8601(3),
+ 'hello' => 1,
+ 'correlation_id' => 'new-correlation-id',
+ 'feature_category' => 'importers',
+ 'import_type' => 'github'
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/object_counter_spec.rb b/spec/lib/gitlab/github_import/object_counter_spec.rb
index 668c11667b5..c9e4ac67061 100644
--- a/spec/lib/gitlab/github_import/object_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/object_counter_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do
it 'validates the operation being incremented' do
expect { described_class.increment(project, :issue, :unknown) }
- .to raise_error(ArgumentError, 'Operation must be fetched or imported')
+ .to raise_error(ArgumentError, 'operation must be fetched or imported')
end
it 'increments the counter and saves the key to be listed in the summary later' do
@@ -33,4 +33,20 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do
'imported' => { 'issue' => 2 }
})
end
+
+ it 'does not increment the counter if the given value is <= 0' do
+ expect(Gitlab::Metrics)
+ .not_to receive(:counter)
+
+ expect(Gitlab::Metrics)
+ .not_to receive(:counter)
+
+ described_class.increment(project, :issue, :fetched, value: 0)
+ described_class.increment(project, :issue, :imported, value: nil)
+
+ expect(described_class.summary(project)).to eq({
+ 'fetched' => {},
+ 'imported' => {}
+ })
+ end
end
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index d56d4708385..1fc7d3c887f 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Gitlab::GithubImport::ParallelScheduling do
let(:importer_class) do
Class.new do
+ def self.name
+ 'MyImporter'
+ end
+
include(Gitlab::GithubImport::ParallelScheduling)
def importer_class
@@ -21,7 +25,8 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
end
end
- let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let_it_be(:project) { create(:project, :import_started, import_source: 'foo/bar') }
+
let(:client) { double(:client) }
describe '#parallel?' do
@@ -79,73 +84,130 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
.to receive(:sequential_import)
.and_return([])
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ message: 'starting importer',
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ message: 'importer finished',
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+
+ importer.execute
+ end
+
+ context 'when abort_on_failure is false' do
+ it 'logs the error when it fails' do
+ exception = StandardError.new('some error')
+
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_raise(exception)
+
+ expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
message: 'starting importer',
- import_source: :github,
parallel: false,
project_id: project.id,
importer: 'Class'
)
- expect(logger)
- .to receive(:info)
+
+ expect(Gitlab::Import::ImportFailureService)
+ .to receive(:track)
.with(
- message: 'importer finished',
- import_source: :github,
- parallel: false,
project_id: project.id,
- importer: 'Class'
- )
- end
+ exception: exception,
+ error_source: 'MyImporter',
+ fail_import: false
+ ).and_call_original
- importer.execute
+ expect { importer.execute }
+ .to raise_error(exception)
+
+ expect(project.import_state.reload.status).to eq('started')
+
+ expect(project.import_failures).not_to be_empty
+ expect(project.import_failures.last.exception_class).to eq('StandardError')
+ expect(project.import_failures.last.exception_message).to eq('some error')
+ end
end
- it 'logs the error when it fails' do
- exception = StandardError.new('some error')
+ context 'when abort_on_failure is true' do
+ let(:importer_class) do
+ Class.new do
+ def self.name
+ 'MyImporter'
+ end
- importer = importer_class.new(project, client, parallel: false)
+ include(Gitlab::GithubImport::ParallelScheduling)
- expect(importer)
- .to receive(:sequential_import)
- .and_raise(exception)
+ def importer_class
+ Class
+ end
+
+ def object_type
+ :dummy
+ end
+
+ def collection_method
+ :issues
+ end
+
+ def abort_on_failure
+ true
+ end
+ end
+ end
+
+ it 'logs the error when it fails and marks import as failed' do
+ exception = StandardError.new('some error')
+
+ importer = importer_class.new(project, client, parallel: false)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_raise(exception)
+
+ expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
message: 'starting importer',
- import_source: :github,
parallel: false,
project_id: project.id,
importer: 'Class'
)
- expect(logger)
- .to receive(:error)
+
+ expect(Gitlab::Import::ImportFailureService)
+ .to receive(:track)
.with(
- message: 'importer failed',
- import_source: :github,
project_id: project.id,
- parallel: false,
- importer: 'Class',
- 'error.message': 'some error'
- )
- end
+ exception: exception,
+ error_source: 'MyImporter',
+ fail_import: true
+ ).and_call_original
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(
- exception,
- import_source: :github,
- parallel: false,
- project_id: project.id,
- importer: 'Class'
- )
- .and_call_original
+ expect { importer.execute }
+ .to raise_error(exception)
+
+ expect(project.import_state.reload.status).to eq('failed')
+ expect(project.import_state.last_error).to eq('some error')
- expect { importer.execute }.to raise_error(exception)
+ expect(project.import_failures).not_to be_empty
+ expect(project.import_failures.last.exception_class).to eq('StandardError')
+ expect(project.import_failures.last.exception_message).to eq('some error')
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index 20e67a784e1..f81fa3b1e2e 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -3,7 +3,14 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
- let(:project) { create(:project) }
+ let(:project) do
+ create(
+ :project,
+ import_type: 'github',
+ import_url: 'https://api.github.com/user/repo'
+ )
+ end
+
let(:client) { double(:client) }
let(:finder) { described_class.new(project, client) }
@@ -263,6 +270,26 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
finder.id_for_github_id(id)
end
+
+ context 'when importing from github enterprise' do
+ let(:project) do
+ create(
+ :project,
+ import_type: 'github',
+ import_url: 'https://othergithub.net/user/repo'
+ )
+ end
+
+ it 'does not look up the user by external id' do
+ expect(finder).not_to receive(:query_id_for_github_id)
+
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with(described_class::ID_CACHE_KEY % id, nil)
+
+ finder.id_for_github_id(id)
+ end
+ end
end
describe '#id_for_github_email' do
diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
index 95e3af34174..641fb27a071 100644
--- a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
@@ -3,26 +3,23 @@
require 'spec_helper'
RSpec.describe Gitlab::GrapeLogging::Loggers::PerfLogger do
- subject { described_class.new }
+ let(:mock_request) { OpenStruct.new(env: {}) }
describe ".parameters" do
- let(:mock_request) { OpenStruct.new(env: {}) }
+ subject { described_class.new.parameters(mock_request, nil) }
- describe 'when no performance datais are present' do
- it 'returns an empty Hash' do
- expect(subject.parameters(mock_request, nil)).to eq({})
- end
+ let(:perf_data) { { redis_calls: 1 } }
+
+ describe 'when no performance data present' do
+ it { is_expected.not_to include(perf_data) }
end
- describe 'when Redis calls are present', :request_store do
- it 'returns a Hash with Redis information' do
+ describe 'when performance data present', :request_store do
+ before do
Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') }
-
- payload = subject.parameters(mock_request, nil)
-
- expect(payload[:redis_calls]).to eq(1)
- expect(payload[:redis_duration_s]).to be >= 0
end
+
+ it { is_expected.to include(perf_data) }
end
end
end
diff --git a/spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb b/spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb
index 1d8849f7e38..33f49dbc8d4 100644
--- a/spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb
+++ b/spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Graphql::CallsGitaly::FieldExtension, :request_store do
let(:field_args) { {} }
let(:owner) { fresh_object_type }
let(:field) do
- ::Types::BaseField.new(name: 'value', type: GraphQL::STRING_TYPE, null: true, owner: owner, **field_args)
+ ::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner, **field_args)
end
def resolve_value
diff --git a/spec/lib/gitlab/graphql/copy_field_description_spec.rb b/spec/lib/gitlab/graphql/copy_field_description_spec.rb
index 310b4046b56..84aa548f2cf 100644
--- a/spec/lib/gitlab/graphql/copy_field_description_spec.rb
+++ b/spec/lib/gitlab/graphql/copy_field_description_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Graphql::CopyFieldDescription do
Class.new(Types::BaseObject) do
graphql_name "TestType"
- field :field_name, GraphQL::STRING_TYPE, null: true, description: 'Foo'
+ field :field_name, GraphQL::Types::String, null: true, description: 'Foo'
end
end
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index 44ca23f547c..a3fb0bbbed8 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
expect(field.name).to eq('testHtml')
expect(field.description).to eq('The GitLab Flavored Markdown rendering of `hello`')
- expect(field.type).to eq(GraphQL::STRING_TYPE)
+ expect(field.type).to eq(GraphQL::Types::String)
expect(field.to_graphql.complexity).to eq(5)
end
diff --git a/spec/lib/gitlab/graphql/mount_mutation_spec.rb b/spec/lib/gitlab/graphql/mount_mutation_spec.rb
index d6b932e08d2..fe25e923506 100644
--- a/spec/lib/gitlab/graphql/mount_mutation_spec.rb
+++ b/spec/lib/gitlab/graphql/mount_mutation_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Gitlab::Graphql::MountMutation do
Class.new(Mutations::BaseMutation) do
graphql_name 'TestMutation'
- argument :foo, GraphQL::STRING_TYPE, required: false
- field :bar, GraphQL::STRING_TYPE, null: true
+ argument :foo, GraphQL::Types::String, required: false
+ field :bar, GraphQL::Types::String, null: true
end
end
diff --git a/spec/lib/gitlab/graphql/negatable_arguments_spec.rb b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb
index bc6e25eb018..71ef75836c0 100644
--- a/spec/lib/gitlab/graphql/negatable_arguments_spec.rb
+++ b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Graphql::NegatableArguments do
it 'defines any arguments passed as block' do
test_resolver.negated do
- argument :foo, GraphQL::STRING_TYPE, required: false
+ argument :foo, GraphQL::Types::String, required: false
end
expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(['foo'])
@@ -27,10 +27,10 @@ RSpec.describe Gitlab::Graphql::NegatableArguments do
it 'defines all arguments passed as block even if called multiple times' do
test_resolver.negated do
- argument :foo, GraphQL::STRING_TYPE, required: false
+ argument :foo, GraphQL::Types::String, required: false
end
test_resolver.negated do
- argument :bar, GraphQL::STRING_TYPE, required: false
+ argument :bar, GraphQL::Types::String, required: false
end
expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(%w[foo bar])
diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
index e89e5c17644..f3f59113c81 100644
--- a/spec/lib/gitlab/graphql/pagination/connections_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
let(:node_type) do
Class.new(::GraphQL::Schema::Object) do
graphql_name 'Node'
- field :value, GraphQL::INT_TYPE, null: false
+ field :value, GraphQL::Types::Int, null: false
end
end
diff --git a/spec/lib/gitlab/graphql/present/field_extension_spec.rb b/spec/lib/gitlab/graphql/present/field_extension_spec.rb
index 6ea313d30b3..5f0f444e0bb 100644
--- a/spec/lib/gitlab/graphql/present/field_extension_spec.rb
+++ b/spec/lib/gitlab/graphql/present/field_extension_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
let(:owner) { fresh_object_type }
let(:field_name) { 'value' }
let(:field) do
- ::Types::BaseField.new(name: field_name, type: GraphQL::STRING_TYPE, null: true, owner: owner)
+ ::Types::BaseField.new(name: field_name, type: GraphQL::Types::String, null: true, owner: owner)
end
let(:base_presenter) do
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
Module.new do
include ::Types::BaseInterface
- field :interface_field, GraphQL::STRING_TYPE, null: true
+ field :interface_field, GraphQL::Types::String, null: true
end
end
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
end
it 'resolves the interface field using the implementation from the presenter' do
- field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::STRING_TYPE, null: true, owner: interface)
+ field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::Types::String, null: true, owner: interface)
value = resolve_field(field, object, object_type: implementation)
expect(value).to eq 'made of concrete'
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
context 'when the implementation is inherited' do
it 'resolves the interface field using the implementation from the presenter' do
subclass = Class.new(implementation) { graphql_name 'Subclass' }
- field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::STRING_TYPE, null: true, owner: interface)
+ field = ::Types::BaseField.new(name: :interface_field, type: GraphQL::Types::String, null: true, owner: interface)
value = resolve_field(field, object, object_type: subclass)
expect(value).to eq 'made of concrete'
@@ -79,8 +79,8 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
def parent
type = fresh_object_type('Parent')
type.present_using(provide_foo)
- type.field :foo, ::GraphQL::INT_TYPE, null: true
- type.field :value, ::GraphQL::STRING_TYPE, null: true
+ type.field :foo, ::GraphQL::Types::Int, null: true
+ type.field :value, ::GraphQL::Types::String, null: true
type
end
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
type = Class.new(parent)
type.graphql_name 'Child'
type.present_using(provide_bar)
- type.field :bar, ::GraphQL::INT_TYPE, null: true
+ type.field :bar, ::GraphQL::Types::Int, null: true
type
end
@@ -150,7 +150,7 @@ RSpec.describe Gitlab::Graphql::Present::FieldExtension do
let(:field) do
::Types::BaseField.new(
name: field_name,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
owner: owner,
resolve: ->(obj, args, ctx) { 'Hello from a proc' }
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
index a1cd2cdb2de..8b7f4ca7933 100644
--- a/spec/lib/gitlab/graphql/queries_spec.rb
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -21,30 +21,30 @@ RSpec.describe Gitlab::Graphql::Queries do
let_it_be(:schema) do
author = Class.new(GraphQL::Schema::Object) do
graphql_name 'Author'
- field :name, GraphQL::STRING_TYPE, null: true
- field :handle, GraphQL::STRING_TYPE, null: false
- field :verified, GraphQL::BOOLEAN_TYPE, null: false
+ field :name, GraphQL::Types::String, null: true
+ field :handle, GraphQL::Types::String, null: false
+ field :verified, GraphQL::Types::Boolean, null: false
end
post = Class.new(GraphQL::Schema::Object) do
graphql_name 'Post'
- field :name, GraphQL::STRING_TYPE, null: false
- field :title, GraphQL::STRING_TYPE, null: false
- field :content, GraphQL::STRING_TYPE, null: true
+ field :name, GraphQL::Types::String, null: false
+ field :title, GraphQL::Types::String, null: false
+ field :content, GraphQL::Types::String, null: true
field :author, author, null: false
end
author.field :posts, [post], null: false do
- argument :blog_title, GraphQL::STRING_TYPE, required: false
+ argument :blog_title, GraphQL::Types::String, required: false
end
blog = Class.new(GraphQL::Schema::Object) do
graphql_name 'Blog'
- field :title, GraphQL::STRING_TYPE, null: false
- field :description, GraphQL::STRING_TYPE, null: false
+ field :title, GraphQL::Types::String, null: false
+ field :description, GraphQL::Types::String, null: false
field :main_author, author, null: false
field :posts, [post], null: false
field :post, post, null: true do
- argument :slug, GraphQL::STRING_TYPE, required: true
+ argument :slug, GraphQL::Types::String, required: true
end
end
@@ -52,10 +52,10 @@ RSpec.describe Gitlab::Graphql::Queries do
query(Class.new(GraphQL::Schema::Object) do
graphql_name 'Query'
field :blog, blog, null: true do
- argument :title, GraphQL::STRING_TYPE, required: true
+ argument :title, GraphQL::Types::String, required: true
end
field :post, post, null: true do
- argument :slug, GraphQL::STRING_TYPE, required: true
+ argument :slug, GraphQL::Types::String, required: true
end
end)
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index ccb3ae1018a..1f06019c929 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -50,16 +50,9 @@ RSpec.describe Gitlab::Highlight do
let(:result) { described_class.highlight(file_name, content) } # content is 44 bytes
before do
- stub_feature_flags(one_megabyte_file_size_limit: false)
stub_config(extra: { 'maximum_text_highlight_size_kilobytes' => 0.0001 } ) # 1.024 bytes
end
- it 'confirm file size is 1MB when `one_megabyte_file_size_limit` is enabled' do
- stub_feature_flags(one_megabyte_file_size_limit: true)
- expect(described_class.too_large?(1024.kilobytes)).to eq(false)
- expect(described_class.too_large?(1025.kilobytes)).to eq(true)
- end
-
it 'increments the metric for oversized files' do
expect { result }.to change { over_highlight_size_limit('file size: 0.0001') }.by(1)
end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 71e80de9f89..d0aae2ac475 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::HTTP do
WebMock.stub_request(:post, /.*/).to_return do |request|
sleep 0.002.seconds
- { body: 'I\m slow', status: 200 }
+ { body: 'I\'m slow', status: 200 }
end
end
@@ -41,25 +41,67 @@ RSpec.describe Gitlab::HTTP do
subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
- specify do
- expect { request_slow_responder }.not_to raise_error
+ shared_examples 'tracks the timeout but does not raise an error' do
+ specify :aggregate_failures do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ an_instance_of(Gitlab::HTTP::ReadTotalTimeout)
+ ).once
+
+ expect { request_slow_responder }.not_to raise_error
+ end
+
+ it 'still calls the block' do
+ expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_with_args
+ end
end
- context 'with use_read_total_timeout option' do
+ shared_examples 'does not track or raise timeout error' do
+ specify :aggregate_failures do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ expect { request_slow_responder }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'tracks the timeout but does not raise an error'
+
+ context 'and use_read_total_timeout option is truthy' do
let(:options) { { use_read_total_timeout: true } }
- it 'raises a timeout error' do
+ it 'raises an error' do
expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
end
+ end
- context 'and timeout option' do
- let(:options) { { use_read_total_timeout: true, timeout: 10.seconds } }
+ context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do
+ let(:options) { { timeout: 10.seconds } }
- it 'overrides the default timeout when timeout option is present' do
- expect { request_slow_responder }.not_to raise_error
- end
+ it_behaves_like 'does not track or raise timeout error'
+ end
+
+ context 'and stream_body option is truthy' do
+ let(:options) { { stream_body: true } }
+
+ it_behaves_like 'does not track or raise timeout error'
+
+ context 'but skip_read_total_timeout option is falsey' do
+ let(:options) { { stream_body: true, skip_read_total_timeout: false } }
+
+ it_behaves_like 'tracks the timeout but does not raise an error'
end
end
+
+ context 'and skip_read_total_timeout option is truthy' do
+ let(:options) { { skip_read_total_timeout: true } }
+
+ it_behaves_like 'does not track or raise timeout error'
+ end
+
+ context 'and skip_read_total_timeout option is falsely' do
+ let(:options) { { skip_read_total_timeout: false } }
+
+ it_behaves_like 'tracks the timeout but does not raise an error'
+ end
end
it 'calls a block' do
diff --git a/spec/lib/gitlab/import/database_helpers_spec.rb b/spec/lib/gitlab/import/database_helpers_spec.rb
index d56e05df5d7..079faed2518 100644
--- a/spec/lib/gitlab/import/database_helpers_spec.rb
+++ b/spec/lib/gitlab/import/database_helpers_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Import::DatabaseHelpers do
let(:project) { create(:project) }
it 'returns the ID returned by the query' do
- expect(Gitlab::Database)
+ expect(Gitlab::Database.main)
.to receive(:bulk_insert)
.with(Issue.table_name, [attributes], return_ids: true)
.and_return([10])
diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb
new file mode 100644
index 00000000000..50b32d634ad
--- /dev/null
+++ b/spec/lib/gitlab/import/import_failure_service_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Import::ImportFailureService do
+ let_it_be(:import_type) { 'import_type' }
+
+ let_it_be(:project) do
+ create(
+ :project,
+ :import_started,
+ import_type: import_type
+ )
+ end
+
+ let(:import_state) { project.import_state }
+ let(:exception) { StandardError.new('some error') }
+
+ shared_examples 'logs the exception and fails the import' do
+ it 'when the failure does not abort the import' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ exception,
+ project_id: project.id,
+ import_type: import_type,
+ source: 'SomeImporter'
+ )
+
+ expect(Gitlab::Import::Logger)
+ .to receive(:error)
+ .with(
+ message: 'importer failed',
+ 'error.message': 'some error',
+ project_id: project.id,
+ import_type: import_type,
+ source: 'SomeImporter'
+ )
+
+ described_class.track(**arguments)
+
+ expect(project.import_state.reload.status).to eq('failed')
+
+ expect(project.import_failures).not_to be_empty
+ expect(project.import_failures.last.exception_class).to eq('StandardError')
+ expect(project.import_failures.last.exception_message).to eq('some error')
+ end
+ end
+
+ shared_examples 'logs the exception and does not fail the import' do
+ it 'when the failure does not abort the import' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ exception,
+ project_id: project.id,
+ import_type: import_type,
+ source: 'SomeImporter'
+ )
+
+ expect(Gitlab::Import::Logger)
+ .to receive(:error)
+ .with(
+ message: 'importer failed',
+ 'error.message': 'some error',
+ project_id: project.id,
+ import_type: import_type,
+ source: 'SomeImporter'
+ )
+
+ described_class.track(**arguments)
+
+ expect(project.import_state.reload.status).to eq('started')
+
+ expect(project.import_failures).not_to be_empty
+ expect(project.import_failures.last.exception_class).to eq('StandardError')
+ expect(project.import_failures.last.exception_message).to eq('some error')
+ end
+ end
+
+ context 'when using the project as reference' do
+ context 'when it fails the import' do
+ let(:arguments) do
+ {
+ project_id: project.id,
+ exception: exception,
+ error_source: 'SomeImporter',
+ fail_import: true
+ }
+ end
+
+ it_behaves_like 'logs the exception and fails the import'
+ end
+
+ context 'when it does not fail the import' do
+ let(:arguments) do
+ {
+ project_id: project.id,
+ exception: exception,
+ error_source: 'SomeImporter',
+ fail_import: false
+ }
+ end
+
+ it_behaves_like 'logs the exception and does not fail the import'
+ end
+ end
+
+ context 'when using the import_state as reference' do
+ context 'when it fails the import' do
+ let(:arguments) do
+ {
+ import_state: import_state,
+ exception: exception,
+ error_source: 'SomeImporter',
+ fail_import: true
+ }
+ end
+
+ it_behaves_like 'logs the exception and fails the import'
+ end
+
+ context 'when it does not fail the import' do
+ let(:arguments) do
+ {
+ import_state: import_state,
+ exception: exception,
+ error_source: 'SomeImporter',
+ fail_import: false
+ }
+ end
+
+ it_behaves_like 'logs the exception and does not fail the import'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import/logger_spec.rb b/spec/lib/gitlab/import/logger_spec.rb
new file mode 100644
index 00000000000..60978aaa25c
--- /dev/null
+++ b/spec/lib/gitlab/import/logger_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Import::Logger do
+ subject { described_class.new('/dev/null') }
+
+ let(:now) { Time.zone.now }
+
+ describe '#format_message' do
+ before do
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ end
+
+ it 'formats strings' do
+ output = subject.format_message('INFO', now, 'test', 'Hello world')
+
+ expect(Gitlab::Json.parse(output)).to eq({
+ 'severity' => 'INFO',
+ 'time' => now.utc.iso8601(3),
+ 'message' => 'Hello world',
+ 'correlation_id' => 'new-correlation-id',
+ 'feature_category' => 'importers'
+ })
+ end
+
+ it 'formats hashes' do
+ output = subject.format_message('INFO', now, 'test', { hello: 1 })
+
+ expect(Gitlab::Json.parse(output)).to eq({
+ 'severity' => 'INFO',
+ 'time' => now.utc.iso8601(3),
+ 'hello' => 1,
+ 'correlation_id' => 'new-correlation-id',
+ 'feature_category' => 'importers'
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 78805cea66a..2b7138a7a10 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -7,6 +7,7 @@ issues:
- updated_by
- milestone
- iteration
+- work_item_type
- notes
- resource_label_events
- resource_weight_events
@@ -56,6 +57,9 @@ issues:
- issue_email_participants
- test_reports
- requirement
+- incident_management_issuable_escalation_status
+work_item_type:
+- issues
events:
- author
- project
@@ -461,7 +465,6 @@ project:
- file_uploads
- import_state
- members_and_requesters
-- build_trace_section_names
- build_trace_chunks
- job_artifacts
- root_of_fork_network
@@ -579,6 +582,7 @@ project:
- security_orchestration_policy_configuration
- timelogs
- error_tracking_errors
+- error_tracking_client_keys
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index 29b192de809..fc08a13a8bd 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
end
it 'does not complain about non UTF-8 characters in MR diff files' do
- ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+ MergeRequest.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
index bfcd4994995..dbd6cb243f6 100644
--- a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do
let(:group) { create(:group) }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group, group_hash: nil) }
- let(:group_json) { ActiveSupport::JSON.decode(IO.read(File.join(shared.export_path, 'group.json'))) }
+ let(:group_json) { Gitlab::Json.parse(IO.read(File.join(shared.export_path, 'group.json'))) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
index d2153221e8f..b67d42d1b71 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group) }
let(:exported_file) { File.join(shared.export_path, 'tree/groups/4352.json') }
- let(:group_json) { ActiveSupport::JSON.decode(IO.read(exported_file)) }
+ let(:group_json) { Gitlab::Json.parse(IO.read(exported_file)) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[
diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
index 9c6d2708607..90966cb4915 100644
--- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Test coverage of the Project Import' do
end
def relations_from_json(json_file)
- json = ActiveSupport::JSON.decode(IO.read(json_file))
+ json = Gitlab::Json.parse(IO.read(json_file))
[].tap {|res| gather_relations({ project: json }, res, [])}
.map {|relation_names| relation_names.join('.')}
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index deb22de9160..9e30564b437 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -156,6 +156,41 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
subject.execute
end
end
+
+ describe 'load balancing' do
+ context 'when feature flag load_balancing_for_export_workers is enabled' do
+ before do
+ stub_feature_flags(load_balancing_for_export_workers: true)
+ end
+
+ context 'when enabled', :db_load_balancing do
+ it 'reads from replica' do
+ expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
+
+ subject.execute
+ end
+ end
+
+ context 'when disabled' do
+ it 'reads from primary' do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ expect(Gitlab::Database::LoadBalancing::Session.current).not_to receive(:use_replicas_for_read_queries)
+
+ subject.execute
+ end
+ end
+ end
+
+ context 'when feature flag load_balancing_for_export_workers is disabled' do
+ it 'reads from primary' do
+ stub_feature_flags(load_balancing_for_export_workers: false)
+
+ expect(Gitlab::Database::LoadBalancing::Session.current).not_to receive(:use_replicas_for_read_queries)
+
+ subject.execute
+ end
+ end
+ end
end
describe '.batch_size' do
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 9755e322221..04c27b6f8ad 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -165,11 +165,10 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do
let(:member_class) { ProjectMember }
let(:importable) { create(:project, :public, name: 'searchable_project') }
- it 'authorizes the users to the project' do
+ it 'adds users to project members' do
members_mapper.map
- expect(user.authorized_project?(importable)).to be true
- expect(user2.authorized_project?(importable)).to be true
+ expect(importable.reload.members.map(&:user)).to include(user, user2)
end
it 'maps an owner as a maintainer' do
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index fd6c66a10a7..bee7c59cab0 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -386,7 +386,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
end
it 'does not complain about non UTF-8 characters in MR diff files' do
- ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+ MergeRequestDiffFile.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
expect(project_tree_saver.save).to be true
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 09280402e2b..cd1828791c3 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -111,4 +111,35 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end
end
+
+ context 'when a command takes longer than DURATION_ERROR_THRESHOLD' do
+ let(:threshold) { 0.5 }
+
+ before do
+ stub_const("#{described_class}::DURATION_ERROR_THRESHOLD", threshold)
+ end
+
+ context 'when report_on_long_redis_durations is disabled' do
+ it 'does nothing' do
+ stub_feature_flags(report_on_long_redis_durations: false)
+
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+ end
+ end
+
+ context 'when report_on_long_redis_durations is enabled' do
+ it 'tracks an exception and continues' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(an_instance_of(described_class::MysteryRedisDurationError),
+ command: 'mget',
+ duration: be > threshold,
+ timestamp: a_string_matching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{5}/))
+
+ Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 48fcc9f93db..85daf50717c 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -83,6 +83,12 @@ RSpec.describe Gitlab::InstrumentationHelper do
expect(payload).to include(:cpu_s)
end
+ it 'logs the process ID' do
+ subject
+
+ expect(payload).to include(:pid)
+ end
+
context 'when logging memory allocations' do
include MemoryInstrumentationHelper
@@ -102,8 +108,6 @@ RSpec.describe Gitlab::InstrumentationHelper do
end
context 'when load balancing is enabled' do
- include_context 'clear DB Load Balancing configuration'
-
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
end
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index e57a8457e7c..198d2db234c 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -192,6 +192,19 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
expect(subject[:assignee_ids]).to be_nil
end
end
+
+ context 'with jira server response' do
+ let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'key' => '1234' }) }
+
+ context 'when assignee maps to a valid GitLab user' do
+ it 'sets the issue assignees to the mapped user' do
+ expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, '1234')
+ .and_return(user.id)
+
+ expect(subject[:assignee_ids]).to eq([user.id])
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb
index 8265c3449bb..7899d01b475 100644
--- a/spec/lib/gitlab/json_cache_spec.rb
+++ b/spec/lib/gitlab/json_cache_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe Gitlab::JsonCache do
.with(expanded_key)
.and_return(nil)
- expect(ActiveSupport::JSON).not_to receive(:decode)
+ expect(Gitlab::Json).not_to receive(:parse)
expect(cache.read(key)).to be_nil
end
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::JsonCache do
.with(expanded_key)
.and_return(true)
- expect(ActiveSupport::JSON).to receive(:decode).with("true").and_call_original
+ expect(Gitlab::Json).to receive(:parse).with("true").and_call_original
expect(cache.read(key, BroadcastMessage)).to eq(true)
end
end
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::JsonCache do
.with(expanded_key)
.and_return(false)
- expect(ActiveSupport::JSON).to receive(:decode).with("false").and_call_original
+ expect(Gitlab::Json).to receive(:parse).with("false").and_call_original
expect(cache.read(key, BroadcastMessage)).to eq(false)
end
end
diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb
index 24d2b03fe2a..17d038ed16c 100644
--- a/spec/lib/gitlab/kas_spec.rb
+++ b/spec/lib/gitlab/kas_spec.rb
@@ -65,6 +65,38 @@ RSpec.describe Gitlab::Kas do
end
end
+ describe '.tunnel_url' do
+ before do
+ stub_config(gitlab_kas: { external_url: external_url })
+ end
+
+ subject { described_class.tunnel_url }
+
+ context 'external_url uses wss://' do
+ let(:external_url) { 'wss://kas.gitlab.example.com' }
+
+ it { is_expected.to eq('https://kas.gitlab.example.com/k8s-proxy') }
+ end
+
+ context 'external_url uses ws://' do
+ let(:external_url) { 'ws://kas.gitlab.example.com' }
+
+ it { is_expected.to eq('http://kas.gitlab.example.com/k8s-proxy') }
+ end
+
+ context 'external_url uses grpcs://' do
+ let(:external_url) { 'grpcs://kas.gitlab.example.com' }
+
+ it { is_expected.to eq('https://kas.gitlab.example.com/k8s-proxy') }
+ end
+
+ context 'external_url uses grpc://' do
+ let(:external_url) { 'grpc://kas.gitlab.example.com' }
+
+ it { is_expected.to eq('http://kas.gitlab.example.com/k8s-proxy') }
+ end
+ end
+
describe '.internal_url' do
it 'returns gitlab_kas internal_url config' do
expect(described_class.internal_url).to eq(Gitlab.config.gitlab_kas.internal_url)
diff --git a/spec/lib/gitlab/kubernetes/default_namespace_spec.rb b/spec/lib/gitlab/kubernetes/default_namespace_spec.rb
index 976fe4a0a87..b6816a18baa 100644
--- a/spec/lib/gitlab/kubernetes/default_namespace_spec.rb
+++ b/spec/lib/gitlab/kubernetes/default_namespace_spec.rb
@@ -32,6 +32,14 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do
subject { generator.from_environment_slug(environment.slug) }
+ shared_examples_for 'handles very long project paths' do
+ before do
+ allow(project).to receive(:path).and_return 'x' * 100
+ end
+
+ it { is_expected.to satisfy { |s| s.length <= 63 } }
+ end
+
context 'namespace per environment is enabled' do
context 'platform namespace is specified' do
let(:platform_namespace) { 'platform-namespace' }
@@ -47,15 +55,12 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do
context 'platform namespace is blank' do
let(:platform_namespace) { nil }
- let(:mock_namespace) { 'mock-namespace' }
- it 'constructs a namespace from the project and environment' do
- expect(Gitlab::NamespaceSanitizer).to receive(:sanitize)
- .with("#{project.path}-#{project.id}-#{environment.slug}".downcase)
- .and_return(mock_namespace)
-
- expect(subject).to eq mock_namespace
+ it 'constructs a namespace from the project and environment slug' do
+ expect(subject).to eq "path-with-capitals-#{project.id}-#{environment.slug}"
end
+
+ it_behaves_like 'handles very long project paths'
end
end
@@ -70,15 +75,12 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do
context 'platform namespace is blank' do
let(:platform_namespace) { nil }
- let(:mock_namespace) { 'mock-namespace' }
- it 'constructs a namespace from the project and environment' do
- expect(Gitlab::NamespaceSanitizer).to receive(:sanitize)
- .with("#{project.path}-#{project.id}".downcase)
- .and_return(mock_namespace)
-
- expect(subject).to eq mock_namespace
+ it 'constructs a namespace from just the project' do
+ expect(subject).to eq "path-with-capitals-#{project.id}"
end
+
+ it_behaves_like 'handles very long project paths'
end
end
end
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb
new file mode 100644
index 00000000000..508808be1be
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/cluster_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Cluster do
+ describe '#to_h' do
+ let(:name) { 'name' }
+ let(:url) { 'url' }
+
+ subject { described_class.new(name: name, url: url).to_h }
+
+ it { is_expected.to eq({ name: name, cluster: { server: url } }) }
+
+ context 'with a certificate' do
+ let(:cert) { 'certificate' }
+ let(:cert_encoded) { Base64.strict_encode64(cert) }
+
+ subject { described_class.new(name: name, url: url, ca_pem: cert).to_h }
+
+ it { is_expected.to eq({ name: name, cluster: { server: url, 'certificate-authority-data': cert_encoded } }) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb
new file mode 100644
index 00000000000..43d4c46fda1
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/context_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::Context do
+ describe '#to_h' do
+ let(:name) { 'name' }
+ let(:user) { 'user' }
+ let(:cluster) { 'cluster' }
+
+ subject { described_class.new(name: name, user: user, cluster: cluster).to_h }
+
+ it { is_expected.to eq({ name: name, context: { cluster: cluster, user: user } }) }
+
+ context 'with a namespace' do
+ let(:namespace) { 'namespace' }
+
+ subject { described_class.new(name: name, user: user, cluster: cluster, namespace: namespace).to_h }
+
+ it { is_expected.to eq({ name: name, context: { cluster: cluster, user: user, namespace: namespace } }) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb
new file mode 100644
index 00000000000..3d6acc80823
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/entry/user_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Kubeconfig::Entry::User do
+ describe '#to_h' do
+ let(:name) { 'name' }
+ let(:token) { 'token' }
+
+ subject { described_class.new(name: name, token: token).to_h }
+
+ it { is_expected.to eq({ name: name, user: { token: token } }) }
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb
new file mode 100644
index 00000000000..057c4373329
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/kubeconfig/template_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Kubeconfig::Template do
+ let(:template) { described_class.new }
+
+ describe '#valid?' do
+ subject { template.valid? }
+
+ it { is_expected.to be_falsey }
+
+ context 'with configuration added' do
+ before do
+ template.add_context(name: 'name', cluster: 'cluster', user: 'user')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#to_h' do
+ subject { described_class.new.to_h }
+
+ it do
+ is_expected.to eq(
+ apiVersion: 'v1',
+ kind: 'Config',
+ clusters: [],
+ users: [],
+ contexts: []
+ )
+ end
+ end
+
+ describe '#to_yaml' do
+ subject { template.to_yaml }
+
+ it { is_expected.to eq(YAML.dump(template.to_h.deep_stringify_keys)) }
+ end
+
+ describe 'adding entries' do
+ let(:entry) { instance_double(entry_class, to_h: attributes) }
+ let(:attributes) do
+ { name: 'name', other: 'other' }
+ end
+
+ subject { template.to_h }
+
+ before do
+ expect(entry_class).to receive(:new).with(attributes).and_return(entry)
+ end
+
+ describe '#add_cluster' do
+ let(:entry_class) { Gitlab::Kubernetes::Kubeconfig::Entry::Cluster }
+
+ before do
+ template.add_cluster(**attributes)
+ end
+
+ it { is_expected.to include(clusters: [attributes]) }
+ end
+
+ describe '#add_user' do
+ let(:entry_class) { Gitlab::Kubernetes::Kubeconfig::Entry::User }
+
+ before do
+ template.add_user(**attributes)
+ end
+
+ it { is_expected.to include(users: [attributes]) }
+ end
+
+ describe '#add_context' do
+ let(:entry_class) { Gitlab::Kubernetes::Kubeconfig::Entry::Context }
+
+ before do
+ template.add_context(**attributes)
+ end
+
+ it { is_expected.to include(contexts: [attributes]) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
index 23dbd4a5bb3..98385cd80cc 100644
--- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -178,4 +178,66 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
thing.refresh_markdown_cache!
end
end
+
+ context 'with note' do
+ let(:klass) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'notes'
+ include CacheMarkdownField
+ include Importable
+ include Mentionable
+
+ attr_mentionable :note, pipeline: :note
+ cache_markdown_field :note, pipeline: :note
+ end
+ end
+
+ let(:thing) { klass.new(note: markdown) }
+
+ before do
+ thing.note = "hello world"
+ end
+
+ it 'calls store_mentions!' do
+ expect(thing).to receive(:store_mentions!).and_call_original
+
+ thing.save!
+ end
+
+ context 'during import' do
+ before do
+ thing.importing = true
+ end
+
+ it 'does not call store_mentions!' do
+ expect(thing).not_to receive(:store_mentions!)
+
+ thing.save!
+ end
+ end
+ end
+
+ context 'when persisted cache is newer than current version' do
+ before do
+ thing.update_column(:cached_markdown_version, thing.cached_markdown_version + 1)
+ end
+
+ it 'does not save the generated HTML' do
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+ end
+
+ context 'when persisted cache is nil' do
+ before do
+ thing.update_column(:cached_markdown_version, nil)
+ end
+
+ it 'does not save the generated HTML' do
+ expect(thing).to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
index 9572e9f50be..7dda10ab41d 100644
--- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
let(:labels) do
{
class: 'ActiveRecord::Base',
- host: Gitlab::Database.config['host'],
- port: Gitlab::Database.config['port']
+ host: Gitlab::Database.main.config['host'],
+ port: Gitlab::Database.main.config['port']
}
end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
index 0516091a8ec..08437920e0c 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_cable_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActionCable, :request_store do
allow(::Gitlab::Metrics).to receive(:histogram).with(
:action_cable_transmitted_bytes, /transmit/
).and_return(counter)
- message_size = ::ActiveSupport::JSON.encode(data).bytesize
+ message_size = ::Gitlab::Json.generate(data).bytesize
expect(counter).to receive(:observe).with({ channel: channel_class, operation: 'event' }, message_size)
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 6fc8f090431..3ffbcbea03c 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:env) { {} }
let(:subscriber) { described_class.new }
let(:connection) { ActiveRecord::Base.connection }
+ let(:db_config_name) { ::Gitlab::Database.db_config_name(connection) }
describe '#transaction' do
let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') }
@@ -36,7 +37,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23)
+ expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, db_config_name: db_config_name)
expect(background_transaction).not_to receive(:observe)
expect(background_transaction).not_to receive(:increment)
@@ -56,7 +57,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23)
+ expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name })
expect(background_transaction).not_to receive(:observe)
expect(background_transaction).not_to receive(:increment)
@@ -76,7 +77,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23)
+ expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, db_config_name: db_config_name)
expect(web_transaction).not_to receive(:observe)
expect(web_transaction).not_to receive(:increment)
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index 65ec3535271..294a5ee82ed 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -77,7 +77,8 @@ RSpec.describe Gitlab::Middleware::Multipart do
result = subject
expect(result[0]).to eq(400)
- expect(result[2]).to include('insecure path used')
+ expect(result[2]).to be_a(Array)
+ expect(result[2].first).to include('insecure path used')
end
end
end
diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb
index 64161fbafdd..86d09f4601c 100644
--- a/spec/lib/gitlab/object_hierarchy_spec.rb
+++ b/spec/lib/gitlab/object_hierarchy_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
end
it 'can find ancestors upto a certain level' do
- relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1)
+ relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1.id)
expect(relation).to contain_exactly(child2)
end
@@ -143,7 +143,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
end
it 'can find ancestors upto a certain level' do
- relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1)
+ relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1.id)
expect(relation).to be_empty
end
diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb
index 562a9bf4460..b867dd533e0 100644
--- a/spec/lib/gitlab/pagination/keyset/order_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb
@@ -6,32 +6,67 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
describe 'paginate over items correctly' do
let(:table) { Arel::Table.new(:my_table) }
let(:order) { nil }
+ let(:default_limit) { 999 }
+ let(:query_building_method) { :build_query }
def run_query(query)
- ActiveRecord::Base.connection.execute(query).to_a
+ ApplicationRecord.connection.execute(query).to_a
end
- def build_query(order:, where_conditions: nil, limit: nil)
+ def where_conditions_as_sql(where_conditions)
+ "WHERE #{Array(where_conditions).map(&:to_sql).join(' OR ')}"
+ end
+
+ def build_query(order:, where_conditions: [], limit: nil)
+ where_string = where_conditions_as_sql(where_conditions)
+
+ <<-SQL
+ SELECT id, year, month
+ FROM (#{table_data}) my_table (id, year, month)
+ #{where_string if where_conditions.present?}
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit};
+ SQL
+ end
+
+ def build_union_query(order:, where_conditions: [], limit: nil)
+ return build_query(order: order, where_conditions: where_conditions, limit: limit) if where_conditions.blank?
+
+ union_queries = Array(where_conditions).map do |where_condition|
+ <<-SQL
+ (SELECT id, year, month
+ FROM (#{table_data}) my_table (id, year, month)
+ WHERE #{where_condition.to_sql}
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit})
+ SQL
+ end
+
+ union_query = union_queries.join(" UNION ALL ")
+
<<-SQL
- SELECT id, year, month
- FROM (#{table_data}) my_table (id, year, month)
- WHERE #{where_conditions || '1=1'}
- ORDER BY #{order}
- LIMIT #{limit || 999};
+ SELECT id, year, month
+ FROM (#{union_query}) as my_table
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit};
SQL
end
+ def cursor_attributes_for_node(node)
+ order.cursor_attributes_for_node(node)
+ end
+
def iterate_and_collect(order:, page_size:, where_conditions: nil)
all_items = []
loop do
- paginated_items = run_query(build_query(order: order, where_conditions: where_conditions, limit: page_size))
+ paginated_items = run_query(send(query_building_method, order: order, where_conditions: where_conditions, limit: page_size))
break if paginated_items.empty?
all_items.concat(paginated_items)
last_item = paginated_items.last
- cursor_attributes = order.cursor_attributes_for_node(last_item)
- where_conditions = order.where_values_with_or_query(cursor_attributes).to_sql
+ cursor_attributes = cursor_attributes_for_node(last_item)
+ where_conditions = order.build_where_values(cursor_attributes)
end
all_items
@@ -54,15 +89,41 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
it { expect(subject).to eq(expected) }
end
+
+ context 'when using the conditions in an UNION query' do
+ let(:query_building_method) { :build_union_query }
+
+ it { expect(subject).to eq(expected) }
+ end
+
+ context 'when the cursor attributes are SQL literals' do
+ def cursor_attributes_for_node(node)
+ # Simulate the scenario where the cursor attributes are SQL literals
+ order.cursor_attributes_for_node(node).transform_values.each_with_index do |value, i|
+ index = i + 1
+ value_sql = value.nil? ? 'NULL::integer' : value
+ values = [value_sql] * index
+ Arel.sql("(ARRAY[#{values.join(',')}])[#{index}]") # example: ARRAY[cursor_value][1] will return cursor_value
+ end
+ end
+
+ it { expect(subject).to eq(expected) }
+
+ context 'when using the conditions in an UNION query' do
+ let(:query_building_method) { :build_union_query }
+
+ it { expect(subject).to eq(expected) }
+ end
+ end
end
context 'when paginating backwards' do
subject do
last_item = expected.last
cursor_attributes = order.cursor_attributes_for_node(last_item)
- where_conditions = order.reversed_order.where_values_with_or_query(cursor_attributes)
+ where_conditions = order.reversed_order.build_where_values(cursor_attributes)
- iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions.to_sql)
+ iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions)
end
it do
@@ -371,7 +432,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
reversed = order.reversed_order
before_conditions = reversed.where_values_with_or_query(before_cursor)
- query = build_query(order: order, where_conditions: "(#{after_conditions.to_sql}) AND (#{before_conditions.to_sql})", limit: 100)
+ query = build_query(order: order, where_conditions: [Arel::Nodes::And.new([after_conditions, before_conditions])], limit: 100)
expect(run_query(query)).to eq([
{ "id" => 2, "year" => 2011, "month" => 0 },
diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
index 1ab8e22d6d1..5ccde789a2e 100644
--- a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
+++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::QueryLimiting::ActiveSupportSubscriber do
context 'when the query is actually a rails cache hit' do
it 'does not increment the number of executed SQL queries' do
- ActiveRecord::Base.connection.cache do
+ User.connection.cache do
User.count
User.count
end
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 4dcf9dc2c05..c93fd884347 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -94,12 +94,6 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
expect(cache.read(:foo)).to be_empty
end
-
- it 'expires the old key format' do
- expect_any_instance_of(Redis).to receive(:unlink).with(cache.cache_key(:foo), cache.old_cache_key(:foo)) # rubocop:disable RSpec/AnyInstanceOf
-
- subject
- end
end
context 'multiple keys' do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 2974893ec4a..b8972f28889 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -33,6 +33,10 @@ RSpec.describe Gitlab::SearchResults do
expect(results.objects('projects', page: 1, per_page: 1, without_count: false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount)
end
+ it 'returns without counts collection when requested' do
+ expect(results.objects('projects', page: 1, per_page: 1, without_count: true)).to be_kind_of(Kaminari::PaginatableWithoutCount)
+ end
+
it 'uses page and per_page to paginate results' do
project2 = create(:project, name: 'foo')
diff --git a/spec/lib/gitlab/setup_helper/workhorse_spec.rb b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
index aa9b4595799..18cb266bf4e 100644
--- a/spec/lib/gitlab/setup_helper/workhorse_spec.rb
+++ b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
@@ -22,4 +22,28 @@ RSpec.describe Gitlab::SetupHelper::Workhorse do
end
end
end
+
+ describe '.redis_url' do
+ it 'matches the SharedState URL' do
+ expect(Gitlab::Redis::SharedState).to receive(:url).and_return('foo')
+
+ expect(described_class.redis_url).to eq('foo')
+ end
+ end
+
+ describe '.redis_db' do
+ subject { described_class.redis_db }
+
+ it 'matches the SharedState DB' do
+ expect(Gitlab::Redis::SharedState).to receive(:params).and_return(db: 1)
+
+ is_expected.to eq(1)
+ end
+
+ it 'defaults to 0 if unspecified' do
+ expect(Gitlab::Redis::SharedState).to receive(:params).and_return({})
+
+ is_expected.to eq(0)
+ end
+ end
end
diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
index 5347680b253..3dd5ac8ee6c 100644
--- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
end
end
- context '-timeout flag' do
+ context 'with --timeout flag' do
it 'when given', 'starts Sidekiq workers with given timeout' do
expect(Gitlab::SidekiqCluster).to receive(:start)
.with([['foo']], default_options.merge(timeout: 10))
@@ -97,6 +97,27 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
end
end
+ context 'with --list-queues flag' do
+ it 'errors when given --list-queues and --dryrun' do
+ expect { cli.run(%w(foo --list-queues --dryrun)) }.to raise_error(described_class::CommandError)
+ end
+
+ it 'prints out a list of queues in alphabetical order' do
+ expected_queues = [
+ 'epics:epics_update_epics_dates',
+ 'epics_new_epic_issue',
+ 'new_epic',
+ 'todos_destroyer:todos_destroyer_confidential_epic'
+ ]
+
+ allow(Gitlab::SidekiqConfig::CliMethods).to receive(:query_queues).and_return(expected_queues.shuffle)
+
+ expect(cli).to receive(:puts).with([expected_queues])
+
+ cli.run(%w(--queue-selector feature_category=epics --list-queues))
+ end
+ end
+
context 'queue namespace expansion' do
it 'starts Sidekiq workers for all queues in all_queues.yml with a namespace in argv' do
expect(Gitlab::SidekiqConfig::CliMethods).to receive(:worker_queues).and_return(['cronjob:foo', 'cronjob:bar'])
diff --git a/spec/lib/gitlab/sidekiq_config/worker_router_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_router_spec.rb
index 687e35813b1..4a8dbe69d36 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_router_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_router_spec.rb
@@ -114,6 +114,13 @@ RSpec.describe Gitlab::SidekiqConfig::WorkerRouter do
['resource_boundary=cpu', 'queue_b'],
['tags=expensive', 'queue_c']
] | 'queue_foo'
+ # Match by generated queue name
+ [
+ ['name=foo_bar', 'queue_foo'],
+ ['feature_category=feature_a|urgency=low', 'queue_a'],
+ ['resource_boundary=cpu', 'queue_b'],
+ ['tags=expensive', 'queue_c']
+ ] | 'queue_foo'
end
end
diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
index 0c43c33ff8c..f4d7a4b3359 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
namespace = queue.include?(':') && queue.split(':').first
inner_worker = double(
name: attributes[:worker_name] || 'Foo::BarWorker',
- queue: queue,
+ generated_queue_name: queue,
queue_namespace: namespace,
get_feature_category: attributes[:feature_category],
get_weight: attributes[:weight],
@@ -48,9 +48,9 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
describe 'delegations' do
[
- :feature_category_not_owned?, :get_feature_category, :get_weight,
- :get_worker_resource_boundary, :get_urgency, :queue,
- :queue_namespace, :worker_has_external_dependencies?
+ :feature_category_not_owned?, :generated_queue_name,
+ :get_feature_category, :get_weight, :get_worker_resource_boundary,
+ :get_urgency, :queue_namespace, :worker_has_external_dependencies?
].each do |meth|
it "delegates #{meth} to the worker class" do
worker = double
diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb
index d2a53185acd..da135f202f6 100644
--- a/spec/lib/gitlab/sidekiq_config_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::SidekiqConfig do
describe '.workers_for_all_queues_yml' do
it 'returns a tuple with FOSS workers first' do
expect(described_class.workers_for_all_queues_yml.first)
- .to include(an_object_having_attributes(queue: 'post_receive'))
+ .to include(an_object_having_attributes(generated_queue_name: 'post_receive'))
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 4406b34e638..a98038cd3f8 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -228,8 +228,6 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
context 'when the job performs database queries' do
- include_context 'clear DB Load Balancing configuration'
-
before do
allow(Time).to receive(:now).and_return(timestamp)
allow(Process).to receive(:clock_gettime).and_call_original
@@ -256,7 +254,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
call_subject(job, 'test_queue') do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ ApplicationRecord.connection.execute('SELECT pg_sleep(0.1);')
end
end
@@ -267,7 +265,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_end_payload).ordered
call_subject(job.dup, 'test_queue') do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ ApplicationRecord.connection.execute('SELECT pg_sleep(0.1);')
end
Gitlab::SafeRequestStore.clear!
@@ -293,54 +291,41 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
include_examples 'performs database queries'
end
- context 'when load balancing is enabled' do
- before do
- allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
- end
+ context 'when load balancing is enabled', :db_load_balancing do
+ let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) }
- let(:dbname) { ::Gitlab::Database.dbname(ActiveRecord::Base.connection) }
+ let(:expected_db_payload_defaults) do
+ metrics =
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys +
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys +
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys +
+ [:db_duration_s]
+
+ metrics.each_with_object({}) do |key, result|
+ result[key.to_s] = 0
+ end
+ end
let(:expected_end_payload_with_db) do
- expected_end_payload.merge(
+ expected_end_payload.merge(expected_db_payload_defaults).merge(
'db_duration_s' => a_value >= 0.1,
'db_count' => a_value >= 1,
- 'db_cached_count' => 0,
- 'db_write_count' => 0,
- 'db_replica_count' => 0,
- 'db_replica_cached_count' => 0,
- 'db_replica_wal_count' => 0,
+ "db_replica_#{db_config_name}_count" => 0,
'db_replica_duration_s' => a_value >= 0,
'db_primary_count' => a_value >= 1,
- 'db_primary_cached_count' => 0,
- 'db_primary_wal_count' => 0,
+ "db_primary_#{db_config_name}_count" => a_value >= 1,
'db_primary_duration_s' => a_value > 0,
- "db_primary_#{dbname}_duration_s" => a_value > 0,
- 'db_primary_wal_cached_count' => 0,
- 'db_replica_wal_cached_count' => 0
+ "db_primary_#{db_config_name}_duration_s" => a_value > 0
)
end
let(:end_payload) do
- start_payload.merge(
+ start_payload.merge(expected_db_payload_defaults).merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
'job_status' => 'done',
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
- 'cpu_s' => 1.111112,
- 'db_duration_s' => 0.0,
- 'db_cached_count' => 0,
- 'db_count' => 0,
- 'db_write_count' => 0,
- 'db_replica_count' => 0,
- 'db_replica_cached_count' => 0,
- 'db_replica_wal_count' => 0,
- 'db_replica_duration_s' => 0,
- 'db_primary_count' => 0,
- 'db_primary_cached_count' => 0,
- 'db_primary_wal_count' => 0,
- 'db_primary_wal_cached_count' => 0,
- 'db_replica_wal_cached_count' => 0,
- 'db_primary_duration_s' => 0
+ 'cpu_s' => 1.111112
)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 3ec8d404bf0..cae0bb6b167 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -236,7 +236,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
include_context 'server metrics with mocked prometheus'
include_context 'server metrics call'
- include_context 'clear DB Load Balancing configuration'
shared_context 'worker declaring data consistency' do
let(:worker_class) { LBTestWorker }
diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb
index 4cf94f4dcab..18ae2cb065f 100644
--- a/spec/lib/gitlab/sql/cte_spec.rb
+++ b/spec/lib/gitlab/sql/cte_spec.rb
@@ -8,9 +8,9 @@ RSpec.describe Gitlab::SQL::CTE do
relation = User.where(id: 1)
cte = described_class.new(:cte_name, relation)
sql = cte.to_arel.to_sql
- name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+ name = ApplicationRecord.connection.quote_table_name(:cte_name)
- sql1 = ActiveRecord::Base.connection.unprepared_statement do
+ sql1 = ApplicationRecord.connection.unprepared_statement do
relation.except(:order).to_sql
end
@@ -30,8 +30,8 @@ RSpec.describe Gitlab::SQL::CTE do
cte = described_class.new(:cte_name, nil)
table = Arel::Table.new(:kittens)
- source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
- alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens)
+ source_name = ApplicationRecord.connection.quote_table_name(:cte_name)
+ alias_name = ApplicationRecord.connection.quote_table_name(:kittens)
expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
end
diff --git a/spec/lib/gitlab/sql/glob_spec.rb b/spec/lib/gitlab/sql/glob_spec.rb
index 8e2b842add6..bb3ca0d3f5b 100644
--- a/spec/lib/gitlab/sql/glob_spec.rb
+++ b/spec/lib/gitlab/sql/glob_spec.rb
@@ -46,10 +46,10 @@ RSpec.describe Gitlab::SQL::Glob do
end
def query(sql)
- ActiveRecord::Base.connection.select_all(sql)
+ ApplicationRecord.connection.select_all(sql)
end
def quote(string)
- ActiveRecord::Base.connection.quote(string)
+ ApplicationRecord.connection.quote(string)
end
end
diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb
index edcacd404c2..f78c4a0cc02 100644
--- a/spec/lib/gitlab/sql/recursive_cte_spec.rb
+++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb
@@ -14,9 +14,9 @@ RSpec.describe Gitlab::SQL::RecursiveCTE do
cte << rel2
sql = cte.to_arel.to_sql
- name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+ name = ApplicationRecord.connection.quote_table_name(:cte_name)
- sql1, sql2 = ActiveRecord::Base.connection.unprepared_statement do
+ sql1, sql2 = ApplicationRecord.connection.unprepared_statement do
[rel1.except(:order).to_sql, rel2.except(:order).to_sql]
end
@@ -28,8 +28,8 @@ RSpec.describe Gitlab::SQL::RecursiveCTE do
it 'returns an alias for the CTE' do
table = Arel::Table.new(:kittens)
- source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
- alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens)
+ source_name = ApplicationRecord.connection.quote_table_name(:cte_name)
+ alias_name = ApplicationRecord.connection.quote_table_name(:kittens)
expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
end
@@ -37,8 +37,8 @@ RSpec.describe Gitlab::SQL::RecursiveCTE do
it 'replaces dots with an underscore' do
table = Arel::Table.new('gitlab.kittens')
- source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
- alias_name = ActiveRecord::Base.connection.quote_table_name(:gitlab_kittens)
+ source_name = ApplicationRecord.connection.quote_table_name(:cte_name)
+ alias_name = ApplicationRecord.connection.quote_table_name(:gitlab_kittens)
expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
end
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index ed551521b1d..628eb380396 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Gitlab::SubscriptionPortal, skip: Gitlab.jh? do
+RSpec.describe ::Gitlab::SubscriptionPortal do
using RSpec::Parameterized::TableSyntax
where(:method_name, :test, :development, :result) do
diff --git a/spec/lib/gitlab/usage/docs/helper_spec.rb b/spec/lib/gitlab/usage/docs/helper_spec.rb
deleted file mode 100644
index e2bb1d8d818..00000000000
--- a/spec/lib/gitlab/usage/docs/helper_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Docs::Helper do
- subject(:helper) { klass.new }
-
- let_it_be(:klass) do
- Class.new do
- include Gitlab::Usage::Docs::Helper
- end
- end
-
- let(:metric_definition) do
- {
- data_category: 'Standard',
- name: 'test_metric',
- description: description,
- product_group: 'group::product intelligence',
- status: 'data_available',
- tier: %w(free premium)
- }
- end
-
- let(:description) { 'Metric description' }
-
- describe '#render_name' do
- it { expect(helper.render_name(metric_definition[:name])).to eq('### `test_metric`') }
- end
-
- describe '#render_description' do
- context 'without description' do
- let(:description) { nil }
-
- it { expect(helper.render_description(metric_definition)).to eq('Missing description') }
- end
-
- context 'without description' do
- it { expect(helper.render_description(metric_definition)).to eq('Metric description') }
- end
- end
-
- describe '#render_yaml_link' do
- let(:yaml_link) { 'config/metrics/license/test_metric.yml' }
- let(:expected) { "[YAML definition](#{yaml_link})" }
-
- it { expect(helper.render_yaml_link(yaml_link)).to eq(expected) }
- end
-
- describe '#render_status' do
- let(:expected) { "Status: `data_available`" }
-
- it { expect(helper.render_status(metric_definition)).to eq(expected) }
- end
-
- describe '#render_owner' do
- let(:expected) { "Group: `group::product intelligence`" }
-
- it { expect(helper.render_owner(metric_definition)).to eq(expected) }
- end
-
- describe '#render_tiers' do
- let(:expected) { "Tiers: `free`, `premium`" }
-
- it { expect(helper.render_tiers(metric_definition)).to eq(expected) }
- end
-
- describe '#render_data_category' do
- let(:expected) { 'Data Category: `Standard`' }
-
- it { expect(helper.render_data_category(metric_definition)).to eq(expected) }
- end
-
- describe '#render_owner' do
- let(:expected) { "Group: `group::product intelligence`" }
-
- it { expect(helper.render_owner(metric_definition)).to eq(expected) }
- end
-end
diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb
deleted file mode 100644
index f3b83a4a4b3..00000000000
--- a/spec/lib/gitlab/usage/docs/renderer_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-CODE_REGEX = %r{<code>(.*)</code>}.freeze
-
-RSpec.describe Gitlab::Usage::Docs::Renderer do
- describe 'contents' do
- let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH }
- let(:items) { Gitlab::Usage::MetricDefinition.definitions.first(10).to_h }
-
- it 'generates dictionary for given items' do
- generated_dictionary = described_class.new(items).contents
-
- generated_dictionary_keys = RDoc::Markdown
- .parse(generated_dictionary)
- .table_of_contents
- .select { |metric_doc| metric_doc.level == 3 }
- .map { |item| item.text.match(CODE_REGEX)&.captures&.first }
-
- expect(generated_dictionary_keys).to match_array(items.keys)
- end
- end
-end
diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
deleted file mode 100644
index f21656df894..00000000000
--- a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Docs::ValueFormatter do
- describe '.format' do
- using RSpec::Parameterized::TableSyntax
- where(:key, :value, :expected_value) do
- :product_group | 'growth::product intelligence' | '`growth::product intelligence`'
- :data_source | 'redis' | 'Redis'
- :data_source | 'ruby' | 'Ruby'
- :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)'
- :tier | %w(gold premium) | ' `gold`, `premium`'
- :distribution | %w(ce ee) | ' `ce`, `ee`'
- :key_path | 'key.path' | '**`key.path`**'
- :milestone | '13.4' | '13.4'
- :status | 'data_available' | '`data_available`'
- end
-
- with_them do
- subject { described_class.format(key, value) }
-
- it { is_expected.to eq(expected_value) }
- end
- end
-end
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index f3c3e5fc550..1ae8a0881ef 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
distribution: %w(ee ce),
tier: %w(free starter premium ultimate bronze silver gold),
name: 'uuid',
- data_category: 'Standard'
+ data_category: 'standard'
}
end
@@ -87,14 +87,14 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
it 'raise exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
described_class.new(path, attributes).validate!
end
context 'with skip_validation' do
it 'raise exception if skip_validation: false' do
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
described_class.new(path, attributes.merge( { skip_validation: false } )).validate!
end
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
attributes[:status] = 'broken'
attributes.delete(:repair_issue_url)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
described_class.new(path, attributes).validate!
end
@@ -173,7 +173,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
write_metric(metric1, path, yaml_content)
write_metric(metric2, path, yaml_content)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
subject
end
@@ -199,7 +199,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
data_source: 'database',
distribution: %w(ee ce),
tier: %w(free starter premium ultimate bronze silver gold),
- data_category: 'Optional'
+ data_category: 'optional'
}
end
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index d4a789419a4..d83f59e4a7d 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -3,27 +3,46 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metric do
- describe '#definition' do
- it 'returns key_path metric definiton' do
- expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
- end
+ let!(:issue) { create(:issue) }
+
+ let(:attributes) do
+ {
+ data_category: "Operational",
+ key_path: "counts.issues",
+ description: "Count of Issues created",
+ product_section: "dev",
+ product_stage: "plan",
+ product_group: "group::plan",
+ product_category: "issue_tracking",
+ value_type: "number",
+ status: "data_available",
+ time_frame: "all",
+ data_source: "database",
+ instrumentation_class: "CountIssuesMetric",
+ distribution: %w(ce ee),
+ tier: %w(free premium ultimate)
+ }
end
- describe '#unflatten_default_path' do
- using RSpec::Parameterized::TableSyntax
+ let(:issue_count_metric_definiton) do
+ double(:issue_count_metric_definiton,
+ attributes.merge({ attributes: attributes })
+ )
+ end
- where(:key_path, :value, :expected_hash) do
- 'uuid' | nil | { uuid: nil }
- 'uuid' | '1111' | { uuid: '1111' }
- 'counts.issues' | nil | { counts: { issues: nil } }
- 'counts.issues' | 100 | { counts: { issues: 100 } }
- 'usage_activity_by_stage.verify.ci_builds' | 100 | { usage_activity_by_stage: { verify: { ci_builds: 100 } } }
- end
+ before do
+ allow(ApplicationRecord.connection).to receive(:transaction_open?).and_return(false)
+ end
- with_them do
- subject { described_class.new(key_path: key_path, value: value).unflatten_key_path }
+ describe '#with_value' do
+ it 'returns key_path metric with the corresponding value' do
+ expect(described_class.new(issue_count_metric_definiton).with_value).to eq({ counts: { issues: 1 } })
+ end
+ end
- it { is_expected.to eq(expected_hash) }
+ describe '#with_instrumentation' do
+ it 'returns key_path metric with the corresponding generated query' do
+ expect(described_class.new(issue_count_metric_definiton).with_instrumentation).to eq({ counts: { issues: "SELECT COUNT(\"issues\".\"id\") FROM \"issues\"" } })
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb
index 8f52d550e5c..1b2170baf17 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CollectedDataCategoriesMetric do
it_behaves_like 'a correct instrumented metric value', {} do
- let(:expected_value) { %w[Standard Subscription Operational Optional] }
+ let(:expected_value) { %w[standard subscription operational optional] }
before do
allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance|
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 5e36820df5e..0a32bdb95d3 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -4,11 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
subject do
- described_class.tap do |m|
- m.relation { Issue }
- m.operation :count
- m.start { m.relation.minimum(:id) }
- m.finish { m.relation.maximum(:id) }
+ described_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation :count
+ metric_class.start { metric_class.relation.minimum(:id) }
+ metric_class.finish { metric_class.relation.maximum(:id) }
end.new(time_frame: 'all')
end
@@ -38,9 +38,9 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with start and finish not called' do
subject do
- described_class.tap do |m|
- m.relation { Issue }
- m.operation :count
+ described_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation :count
end.new(time_frame: 'all')
end
@@ -51,12 +51,12 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with cache_start_and_finish_as called' do
subject do
- described_class.tap do |m|
- m.relation { Issue }
- m.operation :count
- m.start { m.relation.minimum(:id) }
- m.finish { m.relation.maximum(:id) }
- m.cache_start_and_finish_as :special_issue_count
+ described_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation :count
+ metric_class.start { metric_class.relation.minimum(:id) }
+ metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.cache_start_and_finish_as :special_issue_count
end.new(time_frame: 'all')
end
@@ -71,5 +71,45 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
expect(Rails.cache.read('metric_instrumentation/special_issue_count_maximum_id')).to eq(issues.max_by(&:id).id)
end
end
+
+ context 'with estimate_batch_distinct_count' do
+ subject do
+ described_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation(:estimate_batch_distinct_count)
+ metric_class.start { metric_class.relation.minimum(:id) }
+ metric_class.finish { metric_class.relation.maximum(:id) }
+ end.new(time_frame: 'all')
+ end
+
+ it 'calculates a correct result' do
+ expect(subject.value).to be_within(Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE).percent_of(3)
+ end
+
+ context 'with block passed to operation' do
+ let(:buckets) { double('Buckets').as_null_object }
+
+ subject do
+ described_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation(:estimate_batch_distinct_count) do |result|
+ result.foo
+ end
+ metric_class.start { metric_class.relation.minimum(:id) }
+ metric_class.finish { metric_class.relation.maximum(:id) }
+ end.new(time_frame: 'all')
+ end
+
+ before do
+ allow(Gitlab::Database::PostgresHll::Buckets).to receive(:new).and_return(buckets)
+ end
+
+ it 'calls the block passing HLL buckets as an argument' do
+ expect(buckets).to receive(:foo)
+
+ subject.value
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
new file mode 100644
index 00000000000..158be34d39c
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
+ shared_examples 'custom fallback' do |custom_fallback|
+ subject do
+ Class.new(described_class) do
+ fallback(custom_fallback)
+ value { Gitlab::Database.main.version }
+ end.new(time_frame: 'none')
+ end
+
+ describe '#value' do
+ it 'gives the correct value' do
+ expect(subject.value).to eq(Gitlab::Database.main.version)
+ end
+
+ context 'when raising an exception' do
+ it 'return the custom fallback' do
+ expect(Gitlab::Database.main).to receive(:version).and_raise('Error')
+ expect(subject.value).to eq(custom_fallback)
+ end
+ end
+ end
+ end
+
+ context 'with default fallback' do
+ subject do
+ Class.new(described_class) do
+ value { Gitlab::Database.main.version }
+ end.new(time_frame: 'none')
+ end
+
+ describe '#value' do
+ it 'gives the correct value' do
+ expect(subject.value).to eq(Gitlab::Database.main.version )
+ end
+
+ context 'when raising an exception' do
+ it 'return the default fallback' do
+ expect(Gitlab::Database.main).to receive(:version).and_raise('Error')
+ expect(subject.value).to eq(described_class::FALLBACK)
+ end
+ end
+ end
+ end
+
+ context 'with custom fallback -2' do
+ it_behaves_like 'custom fallback', -2
+ end
+
+ context 'with custom fallback nil' do
+ it_behaves_like 'custom fallback', nil
+ end
+
+ context 'with custom fallback false' do
+ it_behaves_like 'custom fallback', false
+ end
+
+ context 'with custom fallback true' do
+ it_behaves_like 'custom fallback', true
+ end
+
+ context 'with custom fallback []' do
+ it_behaves_like 'custom fallback', []
+ end
+
+ context 'with custom fallback { major: -1 }' do
+ it_behaves_like 'custom fallback', { major: -1 }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
new file mode 100644
index 00000000000..fb3bd1ba834
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_gitlab_redis_shared_state do
+ before do
+ 4.times do
+ Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
+ end
+ end
+
+ let(:expected_value) { 4 }
+
+ it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', counter_class: 'SourceCodeCounter' } }
+
+ it 'raises an exception if event option is not present' do
+ expect { described_class.new(counter_class: 'SourceCodeCounter') }.to raise_error(ArgumentError)
+ end
+
+ it 'raises an exception if counter_class option is not present' do
+ expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError)
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
index b4ab9d4861b..0f95da74ff9 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -16,6 +16,14 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
end
end
+ describe '#add_metric' do
+ let(:metric) {'CountIssuesMetric' }
+
+ it 'computes the suggested name for given metric' do
+ expect(described_class.add_metric(metric)).to eq('count_issues')
+ end
+ end
+
context 'for count with default column metrics' do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with count(Board)
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index b1d5d106082..d4148b57348 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
project: project,
user: double,
- sha: double
+ sha: 'd310cc759caaa20cd05a9e0983d6017896d9c34c'
).execute
config_source = :auto_devops_source
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index d89202ae7fe..887759014f5 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -143,7 +143,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
context 'when usage_ping is disabled' do
it 'does not track the event' do
- stub_application_setting(usage_ping_enabled: false)
+ allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
described_class.track_event(weekly_event, values: entity1, time: Date.current)
@@ -153,7 +153,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
context 'when usage_ping is enabled' do
before do
- stub_application_setting(usage_ping_enabled: true)
+ allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(true)
end
it 'tracks event when using symbol' do
diff --git a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
index d4f6110b3df..753e09731bf 100644
--- a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb
@@ -8,12 +8,12 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
subject { Class.new.extend(described_class) }
before do
- stub_application_setting(usage_ping_enabled: setting_value)
+ allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(service_ping_enabled)
end
describe '.increment' do
context 'when usage_ping is disabled' do
- let(:setting_value) { false }
+ let(:service_ping_enabled) { false }
it 'counter is not increased' do
expect do
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
end
context 'when usage_ping is enabled' do
- let(:setting_value) { true }
+ let(:service_ping_enabled) { true }
it 'counter is increased' do
expect do
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
describe '.increment_by' do
context 'when usage_ping is disabled' do
- let(:setting_value) { false }
+ let(:service_ping_enabled) { false }
it 'counter is not increased' do
expect do
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar
end
context 'when usage_ping is enabled' do
- let(:setting_value) { true }
+ let(:service_ping_enabled) { true }
it 'counter is increased' do
expect do
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 18acd767c6d..e0063194f9b 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::UsageDataMetrics do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
- context 'whith instrumentation_class' do
+ context 'with instrumentation_class' do
it 'includes top level keys' do
expect(subject).to include(:uuid)
expect(subject).to include(:hostname)
@@ -26,11 +26,6 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:counts]).to include(:boards)
end
- it 'includes i_quickactions_approve monthly and weekly key' do
- expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_monthly)
- expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_weekly)
- end
-
it 'includes counts keys' do
expect(subject[:counts]).to include(:issues)
end
@@ -42,6 +37,43 @@ RSpec.describe Gitlab::UsageDataMetrics do
it 'includes usage_activity_by_stage_monthly keys' do
expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
end
+
+ it 'includes settings keys' do
+ expect(subject[:settings]).to include(:collected_data_categories)
+ end
+
+ describe 'Redis_HLL_counters' do
+ let(:metric_files_key_paths) do
+ Gitlab::Usage::MetricDefinition
+ .definitions
+ .select { |k, v| v.attributes[:data_source] == 'redis_hll' && v.key_path.starts_with?('redis_hll_counters') }
+ .keys
+ .sort
+ end
+
+ # Recursively traverse nested Hash of a generated Service Ping to return an Array of key paths
+ # in the dotted format used in metric definition YAML files, e.g.: 'count.category.metric_name'
+ def parse_service_ping_keys(object, key_path = [])
+ if object.is_a?(Hash)
+ object.each_with_object([]) do |(key, value), result|
+ result.append parse_service_ping_keys(value, key_path + [key])
+ end
+ else
+ key_path.join('.')
+ end
+ end
+
+ let(:service_ping_key_paths) do
+ parse_service_ping_keys(subject)
+ .flatten
+ .select { |k| k.starts_with?('redis_hll_counters') }
+ .sort
+ end
+
+ it 'is included in the Service Ping hash structure' do
+ expect(metric_files_key_paths).to match_array(service_ping_key_paths)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
index 32d1288c59c..49682acbc66 100644
--- a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
@@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::UsageDataNonSqlMetrics do
let(:default_count) { Gitlab::UsageDataNonSqlMetrics::SQL_METRIC_DEFAULT }
+ describe '#add_metric' do
+ let(:metric) { 'UuidMetric' }
+
+ it 'computes the metric value for given metric' do
+ expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
+ end
+ end
+
describe '.count' do
it 'returns default value for count' do
expect(described_class.count(User)).to eq(default_count)
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 438ae3efd11..64eff76a9f2 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -7,6 +7,14 @@ RSpec.describe Gitlab::UsageDataQueries do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
+ describe '#add_metric' do
+ let(:metric) { 'CountBoardsMetric' }
+
+ it 'builds the query for given metric' do
+ expect(described_class.add_metric(metric)).to eq('SELECT COUNT("boards"."id") FROM "boards"')
+ end
+ end
+
describe '.count' do
it 'returns the raw SQL' do
expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index d84974e562a..5d85ad5ad01 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -568,7 +568,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_custom_issue_tracker_active]).to eq(1)
expect(count_data[:projects_mattermost_active]).to eq(1)
expect(count_data[:groups_mattermost_active]).to eq(1)
- expect(count_data[:templates_mattermost_active]).to eq(1)
expect(count_data[:instances_mattermost_active]).to eq(1)
expect(count_data[:projects_inheriting_mattermost_active]).to eq(1)
expect(count_data[:groups_inheriting_slack_active]).to eq(1)
@@ -623,6 +622,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:deployments]).to eq(4)
expect(count_data[:successful_deployments]).to eq(2)
expect(count_data[:failed_deployments]).to eq(2)
+ expect(count_data[:feature_flags]).to eq(1)
expect(count_data[:snippets]).to eq(6)
expect(count_data[:personal_snippets]).to eq(2)
expect(count_data[:project_snippets]).to eq(4)
@@ -892,9 +892,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
expect(subject[:git][:version]).to eq(Gitlab::Git.version)
- expect(subject[:database][:adapter]).to eq(Gitlab::Database.adapter_name)
- expect(subject[:database][:version]).to eq(Gitlab::Database.version)
- expect(subject[:database][:pg_system_id]).to eq(Gitlab::Database.system_id)
+ expect(subject[:database][:adapter]).to eq(Gitlab::Database.main.adapter_name)
+ expect(subject[:database][:version]).to eq(Gitlab::Database.main.version)
+ expect(subject[:database][:pg_system_id]).to eq(Gitlab::Database.main.system_id)
expect(subject[:mail][:smtp_server]).to eq(ActionMailer::Base.smtp_settings[:address])
expect(subject[:gitaly][:version]).to be_present
expect(subject[:gitaly][:servers]).to be >= 1
@@ -1067,8 +1067,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.system_usage_data_settings }
- it 'gathers settings usage data', :aggregate_failures do
+ it 'gathers encrypted secrets usage data', :aggregate_failures do
expect(subject[:settings][:ldap_encrypted_secrets_enabled]).to eq(Gitlab::Auth::Ldap::Config.encrypted_secrets.active?)
+ expect(subject[:settings][:smtp_encrypted_secrets_enabled]).to eq(Gitlab::Email::SmtpConfig.encrypted_secrets.active?)
end
it 'populates operating system information' do
@@ -1080,7 +1081,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
it 'reports collected data categories' do
- expected_value = %w[Standard Subscription Operational Optional]
+ expected_value = %w[standard subscription operational optional]
allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance|
expect(instance).to receive(:execute).and_return(expected_value)
@@ -1360,6 +1361,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_create_1_cta_clicked" => -1,
"in_product_marketing_email_create_2_sent" => -1,
"in_product_marketing_email_create_2_cta_clicked" => -1,
+ "in_product_marketing_email_team_short_0_sent" => -1,
+ "in_product_marketing_email_team_short_0_cta_clicked" => -1,
+ "in_product_marketing_email_trial_short_0_sent" => -1,
+ "in_product_marketing_email_trial_short_0_cta_clicked" => -1,
+ "in_product_marketing_email_admin_verify_0_sent" => -1,
+ "in_product_marketing_email_admin_verify_0_cta_clicked" => -1,
"in_product_marketing_email_verify_0_sent" => -1,
"in_product_marketing_email_verify_0_cta_clicked" => -1,
"in_product_marketing_email_verify_1_sent" => -1,
@@ -1399,6 +1406,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_create_1_cta_clicked" => 0,
"in_product_marketing_email_create_2_sent" => 0,
"in_product_marketing_email_create_2_cta_clicked" => 0,
+ "in_product_marketing_email_team_short_0_sent" => 0,
+ "in_product_marketing_email_team_short_0_cta_clicked" => 0,
+ "in_product_marketing_email_trial_short_0_sent" => 0,
+ "in_product_marketing_email_trial_short_0_cta_clicked" => 0,
+ "in_product_marketing_email_admin_verify_0_sent" => 0,
+ "in_product_marketing_email_admin_verify_0_cta_clicked" => 0,
"in_product_marketing_email_verify_0_sent" => 1,
"in_product_marketing_email_verify_0_cta_clicked" => 0,
"in_product_marketing_email_verify_1_sent" => 0,
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 8f705d6a487..1d01d5c7e6a 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Utils::UsageData do
include Database::DatabaseHelpers
+ describe '#add_metric' do
+ let(:metric) { 'UuidMetric'}
+
+ it 'computes the metric value for given metric' do
+ expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
+ end
+ end
+
describe '#count' do
let(:relation) { double(:relation) }
@@ -41,10 +49,10 @@ RSpec.describe Gitlab::Utils::UsageData do
describe '#estimate_batch_distinct_count' do
let(:error_rate) { Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE } # HyperLogLog is a probabilistic algorithm, which provides estimated data, with given error margin
- let(:relation) { double(:relation) }
+ let(:relation) { double(:relation, connection: double(:connection)) }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop: disable Database/MultipleDatabases
end
it 'delegates counting to counter class instance' do
@@ -95,6 +103,10 @@ RSpec.describe Gitlab::Utils::UsageData do
let(:build_needs_estimated_cardinality) { 5.217656147118495 }
let(:ci_builds_estimated_cardinality) { 2.0809220082170614 }
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop: disable Database/MultipleDatabases
+ end
+
context 'different counting parameters' do
before_all do
1.upto(3) { |i| create(:ci_build_need, name: i, build: build) }
diff --git a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
index 8dbe64af1c7..9af21685c9e 100644
--- a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
@@ -82,7 +82,6 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do
it 'returns correct script' do
expect(global.terminal_value).to eq({
tag_list: [],
- yaml_variables: [],
job_variables: [],
options: {
before_script: ['ls'],
diff --git a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
index d6d0fc4224d..f8c4a28ed45 100644
--- a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
+++ b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
@@ -141,7 +141,6 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do
expect(entry.value)
.to eq(
tag_list: ['webide'],
- yaml_variables: [{ key: 'KEY', value: 'value', public: true }],
job_variables: [{ key: 'KEY', value: 'value', public: true }],
options: {
image: { name: "ruby:3.0" },
diff --git a/spec/lib/gitlab/x509/tag_spec.rb b/spec/lib/gitlab/x509/tag_spec.rb
index b011ea515de..be120aaf16a 100644
--- a/spec/lib/gitlab/x509/tag_spec.rb
+++ b/spec/lib/gitlab/x509/tag_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
RSpec.describe Gitlab::X509::Tag do
- subject(:signature) { described_class.new(tag).signature }
+ subject(:signature) { described_class.new(project.repository, tag).signature }
describe '#signature' do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:project) { create(:project, :repository) }
- describe 'signed tag' do
+ shared_examples 'signed tag' do
let(:tag) { project.repository.find_tag('v1.1.1') }
let(:certificate_attributes) do
{
@@ -33,10 +33,24 @@ RSpec.describe Gitlab::X509::Tag do
it { expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) }
end
- context 'unsigned tag' do
+ shared_examples 'unsigned tag' do
let(:tag) { project.repository.find_tag('v1.0.0') }
it { expect(signature).to be_nil }
end
+
+ context 'with :get_tag_signatures enabled' do
+ it_behaves_like 'signed tag'
+ it_behaves_like 'unsigned tag'
+ end
+
+ context 'with :get_tag_signatures disabled' do
+ before do
+ stub_feature_flags(get_tag_signatures: false)
+ end
+
+ it_behaves_like 'signed tag'
+ it_behaves_like 'unsigned tag'
+ end
end
end
diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb
index e5aae2822ed..6d50922904e 100644
--- a/spec/lib/peek/views/active_record_spec.rb
+++ b/spec/lib/peek/views/active_record_spec.rb
@@ -52,6 +52,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
allow(connection_primary_1).to receive(:transaction_open?).and_return(false)
allow(connection_primary_2).to receive(:transaction_open?).and_return(true)
allow(connection_unknown).to receive(:transaction_open?).and_return(false)
+ allow(::Gitlab::Database).to receive(:db_config_name).and_return('the_db_config_name')
end
context 'when database load balancing is not enabled' do
@@ -77,32 +78,48 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
cached: '',
transaction: '',
duration: 1000.0,
- sql: 'SELECT * FROM users WHERE id = 10'
+ sql: 'SELECT * FROM users WHERE id = 10',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0,
- sql: 'SELECT * FROM users WHERE id = 10'
+ sql: 'SELECT * FROM users WHERE id = 10',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: 'In a transaction',
duration: 3000.0,
- sql: 'UPDATE users SET admin = true WHERE id = 10'
+ sql: 'UPDATE users SET admin = true WHERE id = 10',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 4000.0,
- sql: 'SELECT VERSION()'
+ sql: 'SELECT VERSION()',
+ db_config_name: "Config name: the_db_config_name"
)
)
)
end
+
+ context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
+ before do
+ stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
+ end
+
+ it 'does not include db_config_name field' do
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
+
+ expect(subject.results[:details][0][:db_config_name]).to be_nil
+ end
+ end
end
context 'when database load balancing is enabled' do
@@ -114,7 +131,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_unknown).and_return(nil)
end
- it 'includes db role data' do
+ it 'includes db role data and db_config_name name' do
Timecop.freeze(2021, 2, 23, 10, 0) do
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
@@ -127,9 +144,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
summary: {
"Cached" => 1,
"In a transaction" => 1,
- "Primary" => 2,
- "Replica" => 1,
- "Unknown" => 1
+ "Role: Primary" => 2,
+ "Role: Replica" => 1,
+ "Role: Unknown" => 1
},
duration: '10000.00ms',
warnings: ["active-record duration: 10000.0 over 3000"],
@@ -140,7 +157,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
transaction: '',
duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10',
- db_role: 'Primary'
+ db_role: 'Role: Primary',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
@@ -148,7 +166,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
transaction: '',
duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10',
- db_role: 'Replica'
+ db_role: 'Role: Replica',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
@@ -156,7 +175,8 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
transaction: 'In a transaction',
duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10',
- db_role: 'Primary'
+ db_role: 'Role: Primary',
+ db_config_name: "Config name: the_db_config_name"
),
a_hash_including(
start: be_a(Time),
@@ -164,10 +184,23 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
transaction: '',
duration: 4000.0,
sql: 'SELECT VERSION()',
- db_role: 'Unknown'
+ db_role: 'Role: Unknown',
+ db_config_name: "Config name: the_db_config_name"
)
)
)
end
+
+ context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
+ before do
+ stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
+ end
+
+ it 'does not include db_config_name field' do
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
+
+ expect(subject.results[:details][0][:db_config_name]).to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb
new file mode 100644
index 00000000000..1ba89af1b02
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::CiCdMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:root_group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:group) { root_group }
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+
+ describe 'Menu Items' do
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
+
+ describe 'Runners' do
+ let(:item_id) { :runners }
+
+ specify { is_expected.not_to be_nil }
+
+ describe 'when feature flag :runner_list_group_view_vue_ui is disabled' do
+ before do
+ stub_feature_flags(runner_list_group_view_vue_ui: false)
+ end
+
+ specify { is_expected.to be_nil }
+ end
+
+ describe 'when the user does not have access' do
+ let(:user) { nil }
+
+ specify { is_expected.to be_nil }
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb b/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb
new file mode 100644
index 00000000000..b68af6fb8ab
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::GroupInformationMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:root_group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:group) { root_group }
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+
+ describe '#title' do
+ subject { described_class.new(context).title }
+
+ context 'when group is a root group' do
+ specify { is_expected.to eq 'Group information'}
+ end
+
+ context 'when group is a child group' do
+ let(:group) { build(:group, parent: root_group) }
+
+ specify { is_expected.to eq 'Subgroup information'}
+ end
+ end
+
+ describe 'Menu Items' do
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
+
+ shared_examples 'menu access rights' do
+ specify { is_expected.not_to be_nil }
+
+ describe 'when the user does not have access' do
+ let(:user) { nil }
+
+ specify { is_expected.to be_nil }
+ end
+ end
+
+ describe 'Activity' do
+ let(:item_id) { :activity }
+
+ specify { is_expected.not_to be_nil }
+
+ it_behaves_like 'menu access rights'
+ end
+
+ describe 'Labels' do
+ let(:item_id) { :labels }
+
+ it_behaves_like 'menu access rights'
+ end
+
+ describe 'Members' do
+ let(:item_id) { :members }
+
+ it_behaves_like 'menu access rights'
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/issues_menu_spec.rb b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb
new file mode 100644
index 00000000000..3d55eb3af40
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::IssuesMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe 'Menu Items' do
+ subject { menu.renderable_items.index { |e| e.item_id == item_id } }
+
+ shared_examples 'menu access rights' do
+ specify { is_expected.not_to be_nil }
+
+ describe 'when the user does not have access' do
+ let(:user) { nil }
+
+ specify { is_expected.to be_nil }
+ end
+ end
+
+ describe 'List' do
+ let(:item_id) { :issue_list }
+
+ specify { is_expected.not_to be_nil }
+
+ it_behaves_like 'menu access rights'
+ end
+
+ describe 'Boards' do
+ let(:item_id) { :boards }
+
+ it_behaves_like 'menu access rights'
+ end
+
+ describe 'Milestones' do
+ let(:item_id) { :milestones }
+
+ it_behaves_like 'menu access rights'
+ end
+ end
+
+ it_behaves_like 'pill_count formatted results' do
+ let(:count_service) { ::Groups::OpenIssuesCountService }
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb
new file mode 100644
index 00000000000..76e58367c9d
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when user can read clusters' do
+ it 'returns true' do
+ expect(menu.render?).to eq true
+ end
+ end
+
+ context 'when user cannot read clusters rules' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(menu.render?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
new file mode 100644
index 00000000000..3aceff29d6d
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::MergeRequestsMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when user can read merge requests' do
+ it 'returns true' do
+ expect(menu.render?).to eq true
+ end
+ end
+
+ context 'when user cannot read merge requests' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(menu.render?).to eq false
+ end
+ end
+ end
+
+ it_behaves_like 'pill_count formatted results' do
+ let(:count_service) { ::Groups::MergeRequestsCountService }
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
new file mode 100644
index 00000000000..5ebd67462f8
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when menu has menu items to show' do
+ it 'returns true' do
+ expect(menu.render?).to eq true
+ end
+ end
+
+ context 'when menu does not have any menu item to show' do
+ it 'returns false' do
+ stub_container_registry_config(enabled: false)
+ stub_config(packages: { enabled: false })
+ stub_config(dependency_proxy: { enabled: false })
+
+ expect(menu.render?).to eq false
+ end
+ end
+ end
+
+ describe '#link' do
+ let(:registry_enabled) { true }
+ let(:packages_enabled) { true }
+
+ before do
+ stub_container_registry_config(enabled: registry_enabled)
+ stub_config(packages: { enabled: packages_enabled })
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ subject { menu.link }
+
+ context 'when Packages Registry is visible' do
+ it 'menu link points to Packages Registry page' do
+ expect(subject).to eq find_menu(menu, :packages_registry).link
+ end
+ end
+
+ context 'when Packages Registry is not visible' do
+ let(:packages_enabled) { false }
+
+ it 'menu link points to Container Registry page' do
+ expect(subject).to eq find_menu(menu, :container_registry).link
+ end
+
+ context 'when Container Registry is not visible' do
+ let(:registry_enabled) { false }
+
+ it 'menu link points to Dependency Proxy page' do
+ expect(subject).to eq find_menu(menu, :dependency_proxy).link
+ end
+ end
+ end
+ end
+
+ describe 'Menu items' do
+ subject { find_menu(menu, item_id) }
+
+ describe 'Packages Registry' do
+ let(:item_id) { :packages_registry }
+
+ context 'when user can read packages' do
+ before do
+ stub_config(packages: { enabled: packages_enabled })
+ end
+
+ context 'when config package setting is disabled' do
+ let(:packages_enabled) { false }
+
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when config package setting is enabled' do
+ let(:packages_enabled) { true }
+
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'Container Registry' do
+ let(:item_id) { :container_registry }
+
+ context 'when user can read container images' do
+ before do
+ stub_container_registry_config(enabled: container_enabled)
+ end
+
+ context 'when config registry setting is disabled' do
+ let(:container_enabled) { false }
+
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when config registry setting is enabled' do
+ let(:container_enabled) { true }
+
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+
+ context 'when user cannot read container images' do
+ let(:user) { nil }
+
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Dependency Proxy' do
+ let(:item_id) { :dependency_proxy }
+
+ before do
+ stub_config(dependency_proxy: { enabled: dependency_enabled })
+ end
+
+ context 'when config dependency_proxy is enabled' do
+ let(:dependency_enabled) { true }
+
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+ end
+
+ context 'when config dependency_proxy is not enabled' do
+ let(:dependency_enabled) { false }
+
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+
+ private
+
+ def find_menu(menu, item)
+ menu.renderable_items.find { |i| i.item_id == item }
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
new file mode 100644
index 00000000000..314c4cdc602
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::SettingsMenu do
+ let_it_be(:owner) { create(:user) }
+
+ let_it_be_with_refind(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when user cannot admin group' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(menu.render?).to be false
+ end
+ end
+ end
+
+ describe 'Menu items' do
+ subject { menu.renderable_items.find { |e| e.item_id == item_id } }
+
+ shared_examples 'access rights checks' do
+ specify { is_expected.not_to be_nil }
+
+ context 'when the user does not have access' do
+ let(:user) { nil }
+
+ specify { is_expected.to be_nil }
+ end
+ end
+
+ describe 'General menu' do
+ let(:item_id) { :general }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Integrations menu' do
+ let(:item_id) { :integrations }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Projects menu' do
+ let(:item_id) { :group_projects }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Repository menu' do
+ let(:item_id) { :repository }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'CI/CD menu' do
+ let(:item_id) { :ci_cd }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Applications menu' do
+ let(:item_id) { :applications }
+
+ it_behaves_like 'access rights checks'
+ end
+
+ describe 'Packages & Registries' do
+ let(:item_id) { :packages_and_registries }
+
+ before do
+ allow(group).to receive(:packages_feature_enabled?).and_return(packages_enabled)
+ end
+
+ describe 'when packages feature is disabled' do
+ let(:packages_enabled) { false }
+
+ specify { is_expected.to be_nil }
+ end
+
+ describe 'when packages feature is enabled' do
+ let(:packages_enabled) { true }
+
+ it_behaves_like 'access rights checks'
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index 95009aa063f..1db80351e45 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -26,6 +26,14 @@ RSpec.describe Sidebars::Menu do
it 'returns false' do
expect(menu.render?).to be false
end
+
+ context 'when menu has a partial' do
+ it 'returns true' do
+ allow(menu).to receive(:menu_partial).and_return('foo')
+
+ expect(menu.render?).to be true
+ end
+ end
end
context 'when the menu has items' do
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index cc4760e69e5..d6807451a25 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -51,8 +51,8 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
context 'when Container Registry is not visible' do
let(:registry_enabled) { false }
- it 'menu link points to Infrastructure Registry page' do
- expect(subject.link).to eq described_class.new(context).renderable_items.find { |i| i.item_id == :infrastructure_registry }.link
+ it 'does not display menu link' do
+ expect(subject.render?).to eq false
end
end
end
@@ -124,18 +124,22 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
describe 'Infrastructure Registry' do
let(:item_id) { :infrastructure_registry }
- context 'when feature flag :infrastructure_registry_page is enabled' do
- it 'the menu item is added to list of menu items' do
- stub_feature_flags(infrastructure_registry_page: true)
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+
+ context 'when config package setting is disabled' do
+ it 'does not add the menu item to the list' do
+ stub_config(packages: { enabled: false })
- is_expected.not_to be_nil
+ is_expected.to be_nil
end
end
- context 'when feature flag :infrastructure_registry_page is disabled' do
- it 'the menu item is not added to list of menu items' do
- stub_feature_flags(infrastructure_registry_page: false)
+ context 'when user cannot read packages' do
+ let(:user) { nil }
+ it 'does not add the menu item to the list' do
is_expected.to be_nil
end
end