Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /spec/lib/gitlab
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/lib/gitlab')
-rw-r--r--spec/lib/gitlab/alert_management/alert_params_spec.rb101
-rw-r--r--spec/lib/gitlab/alert_management/alert_status_counts_spec.rb4
-rw-r--r--spec/lib/gitlab/alert_management/payload/base_spec.rb103
-rw-r--r--spec/lib/gitlab/alert_management/payload/generic_spec.rb32
-rw-r--r--spec/lib/gitlab/alerting/alert_spec.rb299
-rw-r--r--spec/lib/gitlab/alerting/notification_payload_parser_spec.rb204
-rw-r--r--spec/lib/gitlab/analytics/unique_visits_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb26
-rw-r--r--spec/lib/gitlab/auth/current_user_mode_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/devise_spec.rb16
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb55
-rw-r--r--spec/lib/gitlab/auth/unique_ips_limiter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb8
-rw-r--r--spec/lib/gitlab/auth_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb61
-rw-r--r--spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb65
-rw-r--r--spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb36
-rw-r--r--spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb8
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bulk_import/client_spec.rb95
-rw-r--r--spec/lib/gitlab/checks/matching_merge_request_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/artifact_file_reader_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb108
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb147
-rw-r--r--spec/lib/gitlab/ci/config/entry/product/variables_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb127
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb110
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/reports/test_case_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/runner/backoff_spec.rb126
-rw-r--r--spec/lib/gitlab/ci/status/bridge/common_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb61
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/preparing_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/scheduled_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/trace/checksum_spec.rb121
-rw-r--r--spec/lib/gitlab/ci/trace/metrics_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb80
-rw-r--r--spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb12
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb19
-rw-r--r--spec/lib/gitlab/code_navigation_path_spec.rb14
-rw-r--r--spec/lib/gitlab/config/entry/composable_array_spec.rb69
-rw-r--r--spec/lib/gitlab/config/entry/composable_hash_spec.rb108
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb273
-rw-r--r--spec/lib/gitlab/danger/commit_linter_spec.rb14
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb26
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb101
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb5
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration_job_spec.rb2
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb131
-rw-r--r--spec/lib/gitlab/database/bulk_update_spec.rb139
-rw-r--r--spec/lib/gitlab/database/concurrent_reindex_spec.rb207
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb343
-rw-r--r--spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb2
-rw-r--r--spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb17
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb116
-rw-r--r--spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb255
-rw-r--r--spec/lib/gitlab/database/reindexing/coordinator_spec.rb68
-rw-r--r--spec/lib/gitlab/database/reindexing/reindex_action_spec.rb86
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb32
-rw-r--r--spec/lib/gitlab/database/similarity_score_spec.rb11
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb64
-rw-r--r--spec/lib/gitlab/database_spec.rb101
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb34
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb23
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb15
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb177
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb6
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb46
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb12
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb4
-rw-r--r--spec/lib/gitlab/git/remote_mirror_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb37
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb4
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb25
-rw-r--r--spec/lib/gitlab/git_access_spec.rb23
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb18
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb3
-rw-r--r--spec/lib/gitlab/gitpod_spec.rb31
-rw-r--r--spec/lib/gitlab/gl_repository/identifier_spec.rb12
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb8
-rw-r--r--spec/lib/gitlab/gl_repository_spec.rb2
-rw-r--r--spec/lib/gitlab/gon_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb49
-rw-r--r--spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb33
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb59
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb26
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb23
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb7
-rw-r--r--spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb2
-rw-r--r--spec/lib/gitlab/group_search_results_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml13
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/group/relation_factory_spec.rb95
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/project/relation_factory_spec.rb73
-rw-r--r--spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb87
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb35
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb21
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml3
-rw-r--r--spec/lib/gitlab/issuables_count_for_state_spec.rb21
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb17
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb32
-rw-r--r--spec/lib/gitlab/lfs/client_spec.rb132
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb2
-rw-r--r--spec/lib/gitlab/manifest_import/manifest_spec.rb14
-rw-r--r--spec/lib/gitlab/manifest_import/metadata_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb82
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb121
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb16
-rw-r--r--spec/lib/gitlab/middleware/handle_null_bytes_spec.rb88
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/same_site_cookies_spec.rb4
-rw-r--r--spec/lib/gitlab/pagination/offset_pagination_spec.rb30
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/project_template_spec.rb6
-rw-r--r--spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb2
-rw-r--r--spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb2
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb4
-rw-r--r--spec/lib/gitlab/redis/hll_spec.rb30
-rw-r--r--spec/lib/gitlab/regex_spec.rb221
-rw-r--r--spec/lib/gitlab/relative_positioning/mover_spec.rb17
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb6
-rw-r--r--spec/lib/gitlab/repository_size_checker_spec.rb59
-rw-r--r--spec/lib/gitlab/repository_size_error_message_spec.rb6
-rw-r--r--spec/lib/gitlab/sample_data_template_spec.rb66
-rw-r--r--spec/lib/gitlab/search/recent_issues_spec.rb6
-rw-r--r--spec/lib/gitlab/search/recent_merge_requests_spec.rb6
-rw-r--r--spec/lib/gitlab/search_results_spec.rb58
-rw-r--r--spec/lib/gitlab/sidekiq_cluster_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb7
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb37
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb245
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb38
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb101
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb53
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb50
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config_spec.rb82
-rw-r--r--spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb22
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb42
-rw-r--r--spec/lib/gitlab/themes_spec.rb14
-rw-r--r--spec/lib/gitlab/tracking_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb12
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb28
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb210
-rw-r--r--spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb119
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb17
-rw-r--r--spec/lib/gitlab/visibility_level_checker_spec.rb37
-rw-r--r--spec/lib/gitlab/webpack/manifest_spec.rb113
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb34
188 files changed, 6297 insertions, 2108 deletions
diff --git a/spec/lib/gitlab/alert_management/alert_params_spec.rb b/spec/lib/gitlab/alert_management/alert_params_spec.rb
deleted file mode 100644
index c3171be5e29..00000000000
--- a/spec/lib/gitlab/alert_management/alert_params_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::AlertManagement::AlertParams do
- let_it_be(:project) { create(:project, :repository, :private) }
-
- describe '.from_generic_alert' do
- let(:started_at) { Time.current.change(usec: 0).rfc3339 }
- let(:default_payload) do
- {
- 'title' => 'Alert title',
- 'description' => 'Description',
- 'monitoring_tool' => 'Monitoring tool name',
- 'service' => 'Service',
- 'hosts' => ['gitlab.com'],
- 'start_time' => started_at,
- 'some' => { 'extra' => { 'payload' => 'here' } }
- }
- end
-
- let(:payload) { default_payload }
-
- subject { described_class.from_generic_alert(project: project, payload: payload) }
-
- it 'returns Alert compatible parameters' do
- is_expected.to eq(
- project_id: project.id,
- title: 'Alert title',
- description: 'Description',
- monitoring_tool: 'Monitoring tool name',
- service: 'Service',
- severity: 'critical',
- hosts: ['gitlab.com'],
- payload: payload,
- started_at: started_at,
- ended_at: nil,
- fingerprint: nil,
- environment: nil
- )
- end
-
- context 'when severity given' do
- let(:payload) { default_payload.merge(severity: 'low') }
-
- it 'returns Alert compatible parameters' do
- expect(subject[:severity]).to eq('low')
- end
- end
-
- context 'when there are no hosts in the payload' do
- let(:payload) { {} }
-
- it 'hosts param is an empty array' do
- expect(subject[:hosts]).to be_empty
- end
- end
- end
-
- describe '.from_prometheus_alert' do
- let(:payload) do
- {
- 'status' => 'firing',
- 'labels' => {
- 'alertname' => 'GitalyFileServerDown',
- 'channel' => 'gitaly',
- 'pager' => 'pagerduty',
- 'severity' => 's1'
- },
- 'annotations' => {
- 'description' => 'Alert description',
- 'runbook' => 'troubleshooting/gitaly-down.md',
- 'title' => 'Alert title'
- },
- 'startsAt' => '2020-04-27T10:10:22.265949279Z',
- 'endsAt' => '0001-01-01T00:00:00Z',
- 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1',
- 'fingerprint' => 'b6ac4d42057c43c1'
- }
- end
-
- let(:parsed_alert) { Gitlab::Alerting::Alert.new(project: project, payload: payload) }
-
- subject { described_class.from_prometheus_alert(project: project, parsed_alert: parsed_alert) }
-
- it 'returns Alert-compatible params' do
- is_expected.to eq(
- project_id: project.id,
- title: 'Alert title',
- description: 'Alert description',
- monitoring_tool: 'Prometheus',
- payload: payload,
- started_at: parsed_alert.starts_at,
- ended_at: parsed_alert.ends_at,
- fingerprint: parsed_alert.gitlab_fingerprint,
- environment: parsed_alert.environment,
- prometheus_alert: parsed_alert.gitlab_alert
- )
- end
- end
-end
diff --git a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
index a2b8f0aa8d4..fceda763717 100644
--- a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
+++ b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::AlertManagement::AlertStatusCounts do
expect(counts.open).to eq(0)
expect(counts.all).to eq(0)
- AlertManagement::Alert::STATUSES.each_key do |status|
+ ::AlertManagement::Alert.status_names.each do |status|
expect(counts.send(status)).to eq(0)
end
end
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::AlertManagement::AlertStatusCounts do
end
context 'when filtering params are included' do
- let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+ let(:params) { { status: :resolved } }
it 'returns the correct counts for each status' do
expect(counts.open).to eq(0)
diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb
index e0f63bad05d..0c26e94e596 100644
--- a/spec/lib/gitlab/alert_management/payload/base_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb
@@ -120,14 +120,107 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
end
describe '#alert_params' do
- before do
- allow(parsed_payload).to receive(:title).and_return('title')
- allow(parsed_payload).to receive(:description).and_return('description')
+ subject { parsed_payload.alert_params }
+
+ context 'with every key' do
+ let_it_be(:raw_payload) { { 'key' => 'value' } }
+ let_it_be(:stubs) do
+ {
+ description: 'description',
+ ends_at: Time.current,
+ environment: create(:environment, project: project),
+ gitlab_fingerprint: 'gitlab_fingerprint',
+ hosts: 'hosts',
+ monitoring_tool: 'monitoring_tool',
+ gitlab_alert: create(:prometheus_alert, project: project),
+ service: 'service',
+ severity: 'critical',
+ starts_at: Time.current,
+ title: 'title'
+ }
+ end
+
+ let(:expected_result) do
+ {
+ description: stubs[:description],
+ ended_at: stubs[:ends_at],
+ environment: stubs[:environment],
+ fingerprint: stubs[:gitlab_fingerprint],
+ hosts: [stubs[:hosts]],
+ monitoring_tool: stubs[:monitoring_tool],
+ payload: raw_payload,
+ project_id: project.id,
+ prometheus_alert: stubs[:gitlab_alert],
+ service: stubs[:service],
+ severity: stubs[:severity],
+ started_at: stubs[:starts_at],
+ title: stubs[:title]
+ }
+ end
+
+ before do
+ allow(parsed_payload).to receive_messages(stubs)
+ end
+
+ it { is_expected.to eq(expected_result) }
+
+ it 'can generate a valid new alert' do
+ expect(::AlertManagement::Alert.new(subject.except(:ended_at))).to be_valid
+ end
end
- subject { parsed_payload.alert_params }
+ context 'with too-long strings' do
+ let_it_be(:stubs) do
+ {
+ description: 'a' * (::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH + 1),
+ hosts: 'b' * (::AlertManagement::Alert::HOSTS_MAX_LENGTH + 1),
+ monitoring_tool: 'c' * (::AlertManagement::Alert::TOOL_MAX_LENGTH + 1),
+ service: 'd' * (::AlertManagement::Alert::SERVICE_MAX_LENGTH + 1),
+ title: 'e' * (::AlertManagement::Alert::TITLE_MAX_LENGTH + 1)
+ }
+ end
- it { is_expected.to eq({ description: 'description', project_id: project.id, title: 'title' }) }
+ before do
+ allow(parsed_payload).to receive_messages(stubs)
+ end
+
+ it do
+ is_expected.to eq({
+ description: stubs[:description].truncate(AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
+ hosts: ['b' * ::AlertManagement::Alert::HOSTS_MAX_LENGTH],
+ monitoring_tool: stubs[:monitoring_tool].truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH),
+ service: stubs[:service].truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH),
+ project_id: project.id,
+ title: stubs[:title].truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH)
+ })
+ end
+ end
+
+ context 'with too-long hosts array' do
+ let(:hosts) { %w(abc def ghij) }
+ let(:shortened_hosts) { %w(abc def ghi) }
+
+ before do
+ stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9)
+ allow(parsed_payload).to receive(:hosts).and_return(hosts)
+ end
+
+ it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) }
+
+ context 'with host cut off between elements' do
+ let(:hosts) { %w(abcde fghij) }
+ let(:shortened_hosts) { %w(abcde fghi) }
+
+ it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
+ end
+
+ context 'with nested hosts' do
+ let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray
+ let(:shortened_hosts) { %w(abc de f g hi) }
+
+ it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
+ end
+ end
end
describe '#gitlab_fingerprint' do
diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
index 538a822503e..b7660462b0d 100644
--- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
subject { parsed_payload.starts_at }
around do |example|
- Timecop.freeze(current_time) { example.run }
+ travel_to(current_time) { example.run }
end
context 'without start_time' do
@@ -86,4 +86,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
it_behaves_like 'parsable alert payload field', 'gitlab_environment_name'
end
+
+ describe '#description' do
+ subject { parsed_payload.description }
+
+ it_behaves_like 'parsable alert payload field', 'description'
+ end
+
+ describe '#ends_at' do
+ let(:current_time) { Time.current.change(usec: 0).utc }
+
+ subject { parsed_payload.ends_at }
+
+ around do |example|
+ travel_to(current_time) { example.run }
+ end
+
+ context 'without end_time' do
+ it { is_expected.to be_nil }
+ end
+
+ context "with end_time" do
+ let(:value) { 10.minutes.ago.change(usec: 0).utc }
+
+ before do
+ raw_payload['end_time'] = value.to_s
+ end
+
+ it { is_expected.to eq(value) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/alerting/alert_spec.rb b/spec/lib/gitlab/alerting/alert_spec.rb
deleted file mode 100644
index b53b71e3f3e..00000000000
--- a/spec/lib/gitlab/alerting/alert_spec.rb
+++ /dev/null
@@ -1,299 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Alerting::Alert do
- let_it_be(:project) { create(:project) }
-
- let(:alert) { build(:alerting_alert, project: project, payload: payload) }
- let(:payload) { {} }
-
- shared_context 'gitlab alert' do
- let!(:gitlab_alert) { create(:prometheus_alert, project: project) }
- let(:gitlab_alert_id) { gitlab_alert.id }
-
- before do
- payload['labels'] = {
- 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s,
- 'gitlab_prometheus_alert_id' => gitlab_alert_id
- }
- end
- end
-
- shared_context 'full query' do
- before do
- payload['generatorURL'] = 'http://localhost:9090/graph?g0.expr=vector%281%29'
- end
- end
-
- shared_examples 'invalid alert' do
- it 'is invalid' do
- expect(alert).not_to be_valid
- end
- end
-
- shared_examples 'parse payload' do |*pairs|
- context 'without payload' do
- it { is_expected.to be_nil }
- end
-
- pairs.each do |pair|
- context "with #{pair}" do
- let(:value) { 'some value' }
-
- before do
- section, name = pair.split('/')
- payload[section] = { name => value }
- end
-
- it { is_expected.to eq(value) }
- end
- end
- end
-
- describe '#gitlab_alert' do
- subject { alert.gitlab_alert }
-
- context 'without payload' do
- it { is_expected.to be_nil }
- end
-
- context 'with gitlab alert' do
- include_context 'gitlab alert'
-
- it { is_expected.to eq(gitlab_alert) }
- end
-
- context 'with unknown gitlab alert' do
- include_context 'gitlab alert' do
- let(:gitlab_alert_id) { 'unknown' }
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when two alerts with the same metric exist' do
- include_context 'gitlab alert'
-
- let!(:second_gitlab_alert) do
- create(:prometheus_alert,
- project: project,
- prometheus_metric_id: gitlab_alert.prometheus_metric_id
- )
- end
-
- context 'alert id given in params' do
- before do
- payload['labels'] = {
- 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s,
- 'gitlab_prometheus_alert_id' => second_gitlab_alert.id
- }
- end
-
- it { is_expected.to eq(second_gitlab_alert) }
- end
-
- context 'metric id given in params' do
- # This tests the case when two alerts are found, as metric id
- # is not unique.
-
- # Note the metric id was incorrectly named as 'gitlab_alert_id'
- # in PrometheusAlert#to_param.
- before do
- payload['labels'] = { 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id }
- end
-
- it { is_expected.to be_nil }
- end
- end
- end
-
- describe '#title' do
- subject { alert.title }
-
- it_behaves_like 'parse payload',
- 'annotations/title',
- 'annotations/summary',
- 'labels/alertname'
-
- context 'with gitlab alert' do
- include_context 'gitlab alert'
-
- context 'with annotations/title' do
- let(:value) { 'annotation title' }
-
- before do
- payload['annotations'] = { 'title' => value }
- end
-
- it { is_expected.to eq(gitlab_alert.title) }
- end
- end
- end
-
- describe '#description' do
- subject { alert.description }
-
- it_behaves_like 'parse payload', 'annotations/description'
- end
-
- describe '#annotations' do
- subject { alert.annotations }
-
- context 'without payload' do
- it { is_expected.to eq([]) }
- end
-
- context 'with payload' do
- before do
- payload['annotations'] = { 'foo' => 'value1', 'bar' => 'value2' }
- end
-
- it 'parses annotations' do
- expect(subject.size).to eq(2)
- expect(subject.map(&:label)).to eq(%w[foo bar])
- expect(subject.map(&:value)).to eq(%w[value1 value2])
- end
- end
- end
-
- describe '#environment' do
- subject { alert.environment }
-
- context 'without gitlab_alert' do
- it { is_expected.to be_nil }
- end
-
- context 'with gitlab alert' do
- include_context 'gitlab alert'
-
- it { is_expected.to eq(gitlab_alert.environment) }
- end
- end
-
- describe '#starts_at' do
- subject { alert.starts_at }
-
- context 'with empty startsAt' do
- before do
- payload['startsAt'] = nil
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'with invalid startsAt' do
- before do
- payload['startsAt'] = 'invalid'
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'with payload' do
- let(:time) { Time.current.change(usec: 0) }
-
- before do
- payload['startsAt'] = time.rfc3339
- end
-
- it { is_expected.to eq(time) }
- end
- end
-
- describe '#full_query' do
- using RSpec::Parameterized::TableSyntax
-
- subject { alert.full_query }
-
- where(:generator_url, :expected_query) do
- nil | nil
- 'http://localhost' | nil
- 'invalid url' | nil
- 'http://localhost:9090/graph?g1.expr=vector%281%29' | nil
- 'http://localhost:9090/graph?g0.expr=vector%281%29' | 'vector(1)'
- end
-
- with_them do
- before do
- payload['generatorURL'] = generator_url
- end
-
- it { is_expected.to eq(expected_query) }
- end
-
- context 'with gitlab alert' do
- include_context 'gitlab alert'
- include_context 'full query'
-
- it { is_expected.to eq(gitlab_alert.full_query) }
- end
- end
-
- describe '#y_label' do
- subject { alert.y_label }
-
- it_behaves_like 'parse payload', 'annotations/gitlab_y_label'
-
- context 'when y_label is not included in the payload' do
- it_behaves_like 'parse payload', 'annotations/title'
- end
- end
-
- describe '#alert_markdown' do
- subject { alert.alert_markdown }
-
- it_behaves_like 'parse payload', 'annotations/gitlab_incident_markdown'
- end
-
- describe '#gitlab_fingerprint' do
- subject { alert.gitlab_fingerprint }
-
- context 'when the alert is a GitLab managed alert' do
- include_context 'gitlab alert'
-
- it 'returns a fingerprint' do
- plain_fingerprint = [alert.metric_id, alert.starts_at_raw].join('/')
-
- is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
- end
- end
-
- context 'when the alert is from self managed Prometheus' do
- include_context 'full query'
-
- it 'returns a fingerprint' do
- plain_fingerprint = [alert.starts_at_raw, alert.title, alert.full_query].join('/')
-
- is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
- end
- end
- end
-
- describe '#valid?' do
- before do
- payload.update(
- 'annotations' => { 'title' => 'some title' },
- 'startsAt' => Time.current.rfc3339
- )
- end
-
- subject { alert }
-
- it { is_expected.to be_valid }
-
- context 'without project' do
- let(:project) { nil }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'without starts_at' do
- before do
- payload['startsAt'] = nil
- end
-
- it { is_expected.not_to be_valid }
- end
- end
-end
diff --git a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
deleted file mode 100644
index ff5ab1116fa..00000000000
--- a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Alerting::NotificationPayloadParser do
- let_it_be(:project) { build(:project) }
-
- describe '.call' do
- let(:starts_at) { Time.current.change(usec: 0) }
- let(:ends_at) { Time.current.change(usec: 0) }
- let(:payload) do
- {
- 'title' => 'alert title',
- 'start_time' => starts_at.rfc3339,
- 'end_time' => ends_at.rfc3339,
- 'description' => 'Description',
- 'monitoring_tool' => 'Monitoring tool name',
- 'service' => 'Service',
- 'hosts' => ['gitlab.com'],
- 'severity' => 'low'
- }
- end
-
- subject { described_class.call(payload, project) }
-
- it 'returns Prometheus-like payload' do
- is_expected.to eq(
- {
- 'annotations' => {
- 'title' => 'alert title',
- 'description' => 'Description',
- 'monitoring_tool' => 'Monitoring tool name',
- 'service' => 'Service',
- 'hosts' => ['gitlab.com'],
- 'severity' => 'low'
- },
- 'startsAt' => starts_at.rfc3339,
- 'endsAt' => ends_at.rfc3339
- }
- )
- end
-
- context 'when title is blank' do
- before do
- payload[:title] = ''
- end
-
- it 'sets a predefined title' do
- expect(subject.dig('annotations', 'title')).to eq('New: Incident')
- end
- end
-
- context 'when hosts attribute is a string' do
- before do
- payload[:hosts] = 'gitlab.com'
- end
-
- it 'returns hosts as an array of one element' do
- expect(subject.dig('annotations', 'hosts')).to eq(['gitlab.com'])
- end
- end
-
- context 'when the time is in unsupported format' do
- before do
- payload[:start_time] = 'invalid/date/format'
- end
-
- it 'sets startsAt to a current time in RFC3339 format' do
- expect(subject['startsAt']).to eq(starts_at.rfc3339)
- end
- end
-
- context 'when payload is blank' do
- let(:payload) { {} }
-
- it 'returns default parameters' do
- is_expected.to match(
- 'annotations' => {
- 'title' => described_class::DEFAULT_TITLE,
- 'severity' => described_class::DEFAULT_SEVERITY
- },
- 'startsAt' => starts_at.rfc3339
- )
- end
-
- context 'when severity is blank' do
- before do
- payload[:severity] = ''
- end
-
- it 'sets severity to the default ' do
- expect(subject.dig('annotations', 'severity')).to eq(described_class::DEFAULT_SEVERITY)
- end
- end
- end
-
- context 'with fingerprint' do
- before do
- payload[:fingerprint] = data
- end
-
- shared_examples 'fingerprint generation' do
- it 'generates the fingerprint correctly' do
- expect(result).to eq(Gitlab::AlertManagement::Fingerprint.generate(data))
- end
- end
-
- context 'with blank fingerprint' do
- it_behaves_like 'fingerprint generation' do
- let(:data) { ' ' }
- let(:result) { subject.dig('annotations', 'fingerprint') }
- end
- end
-
- context 'with fingerprint given' do
- it_behaves_like 'fingerprint generation' do
- let(:data) { 'fingerprint' }
- let(:result) { subject.dig('annotations', 'fingerprint') }
- end
- end
-
- context 'with array fingerprint given' do
- it_behaves_like 'fingerprint generation' do
- let(:data) { [1, 'fingerprint', 'given'] }
- let(:result) { subject.dig('annotations', 'fingerprint') }
- end
- end
- end
-
- context 'with environment' do
- let(:environment) { create(:environment, project: project) }
-
- before do
- payload[:gitlab_environment_name] = environment.name
- end
-
- it 'sets the environment ' do
- expect(subject.dig('annotations', 'environment')).to eq(environment)
- end
- end
-
- context 'when payload attributes have blank lines' do
- let(:payload) do
- {
- 'title' => '',
- 'start_time' => '',
- 'end_time' => '',
- 'description' => '',
- 'monitoring_tool' => '',
- 'service' => '',
- 'hosts' => ['']
- }
- end
-
- it 'returns default parameters' do
- is_expected.to eq(
- 'annotations' => {
- 'title' => 'New: Incident',
- 'severity' => described_class::DEFAULT_SEVERITY
- },
- 'startsAt' => starts_at.rfc3339
- )
- end
- end
-
- context 'when payload has secondary params' do
- let(:payload) do
- {
- 'description' => 'Description',
- 'additional' => {
- 'params' => {
- '1' => 'Some value 1',
- '2' => 'Some value 2',
- 'blank' => ''
- }
- }
- }
- end
-
- it 'adds secondary params to annotations' do
- is_expected.to eq(
- 'annotations' => {
- 'title' => 'New: Incident',
- 'severity' => described_class::DEFAULT_SEVERITY,
- 'description' => 'Description',
- 'additional.params.1' => 'Some value 1',
- 'additional.params.2' => 'Some value 2'
- },
- 'startsAt' => starts_at.rfc3339
- )
- end
- end
-
- context 'when secondary params hash is too big' do
- before do
- allow(Gitlab::Utils::SafeInlineHash).to receive(:merge_keys!).and_raise(ArgumentError)
- end
-
- it 'catches and re-raises an error' do
- expect { subject }.to raise_error Gitlab::Alerting::NotificationPayloadParser::BadPayloadError, 'The payload is too big'
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/analytics/unique_visits_spec.rb b/spec/lib/gitlab/analytics/unique_visits_spec.rb
index 1432c9ac58f..6ac58e13f4c 100644
--- a/spec/lib/gitlab/analytics/unique_visits_spec.rb
+++ b/spec/lib/gitlab/analytics/unique_visits_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state
# Without freezing the time, the test may behave inconsistently
# depending on which day of the week test is run.
reference_time = Time.utc(2020, 6, 1)
- Timecop.freeze(reference_time) { example.run }
+ travel_to(reference_time) { example.run }
end
describe '#track_visit' do
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 1ac8ebe1369..2ebde145bfd 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -419,10 +419,30 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_user_from_web_access_token(:ics)).to eq(user)
end
- it 'returns the user for API requests' do
- set_header('SCRIPT_NAME', '/api/endpoint')
+ context 'for API requests' do
+ it 'returns the user' do
+ set_header('SCRIPT_NAME', '/api/endpoint')
+
+ expect(find_user_from_web_access_token(:api)).to eq(user)
+ end
+
+ it 'returns nil if URL does not start with /api/' do
+ set_header('SCRIPT_NAME', '/relative_root/api/endpoint')
+
+ expect(find_user_from_web_access_token(:api)).to be_nil
+ end
- expect(find_user_from_web_access_token(:api)).to eq(user)
+ context 'when relative_url_root is set' do
+ before do
+ stub_config_setting(relative_url_root: '/relative_root')
+ end
+
+ it 'returns the user' do
+ set_header('SCRIPT_NAME', '/relative_root/api/endpoint')
+
+ expect(find_user_from_web_access_token(:api)).to eq(user)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb
index 60b403780c0..ffd7813190a 100644
--- a/spec/lib/gitlab/auth/current_user_mode_spec.rb
+++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
- Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
+ travel_to(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
# in the future this will be a new request, simulate by clearing the RequestStore
Gitlab::SafeRequestStore.clear!
diff --git a/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb
new file mode 100644
index 00000000000..0c88421d456
--- /dev/null
+++ b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Otp::Strategies::Devise do
+ let_it_be(:user) { create(:user) }
+ let(:otp_code) { 42 }
+
+ subject(:validate) { described_class.new(user).validate(otp_code) }
+
+ it 'calls Devise' do
+ expect(user).to receive(:validate_and_consume_otp!).with(otp_code)
+
+ validate
+ end
+end
diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb
new file mode 100644
index 00000000000..18fd6d08057
--- /dev/null
+++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do
+ let_it_be(:user) { create(:user) }
+ let(:otp_code) { 42 }
+
+ let(:host) { 'forti_authenticator.example.com' }
+ let(:port) { '444' }
+ let(:api_username) { 'janedoe' }
+ let(:api_token) { 's3cr3t' }
+
+ let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" }
+
+ subject(:validate) { described_class.new(user).validate(otp_code) }
+
+ before do
+ stub_feature_flags(forti_authenticator: true)
+
+ stub_forti_authenticator_config(
+ host: host,
+ port: port,
+ username: api_username,
+ token: api_token
+ )
+
+ request_body = { username: user.username,
+ token_code: otp_code }
+
+ stub_request(:post, forti_authenticator_auth_url)
+ .with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: response_status, body: '', headers: {})
+ end
+
+ context 'successful validation' do
+ let(:response_status) { 200 }
+
+ it 'returns success' do
+ expect(validate[:status]).to eq(:success)
+ end
+ end
+
+ context 'unsuccessful validation' do
+ let(:response_status) { 401 }
+
+ it 'returns error' do
+ expect(validate[:status]).to eq(:error)
+ end
+ end
+
+ def stub_forti_authenticator_config(forti_authenticator_settings)
+ allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings))
+ end
+end
diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
index a08055ab852..b239de841b6 100644
--- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Auth::UniqueIpsLimiter, :clean_gitlab_redis_shared_state
expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
- Timecop.travel(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
+ travel_to(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1)
expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2)
end
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
index 5cbd22827c9..d3c6cde5590 100644
--- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -49,5 +49,13 @@ RSpec.describe Gitlab::Auth::UserAccessDeniedReason do
it { is_expected.to match /Your primary email address is not confirmed/ }
end
+
+ context 'when the user is blocked pending approval' do
+ before do
+ user.block_pending_approval!
+ end
+
+ it { is_expected.to eq('Your account is pending approval from your administrator and hence blocked.') }
+ end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 74360637897..1768ab41a71 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -726,6 +726,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
+ it 'does not find user in blocked_pending_approval state' do
+ user.block_pending_approval
+
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ end
+
context 'with increment_failed_attempts' do
wrong_password = 'incorrect_password'
diff --git a/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb
new file mode 100644
index 00000000000..81b8b5dde08
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::AddModifiedToApprovalMergeRequestRule, schema: 20200817195628 do
+ let(:determine_if_rules_are_modified) { described_class.new }
+
+ let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
+ let(:projects) { table(:projects) }
+ let(:normal_project) { projects.create!(namespace_id: namespace.id) }
+ let(:overridden_project) { projects.create!(namespace_id: namespace.id) }
+ let(:rules) { table(:approval_merge_request_rules) }
+ let(:project_rules) { table(:approval_project_rules) }
+ let(:sources) { table(:approval_merge_request_rule_sources) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:groups) { table(:namespaces) }
+ let(:mr_groups) { table(:approval_merge_request_rules_groups) }
+ let(:project_groups) { table(:approval_project_rules_groups) }
+
+ before do
+ project_rule = project_rules.create!(project_id: normal_project.id, approvals_required: 3, name: 'test rule')
+ overridden_project_rule = project_rules.create!(project_id: overridden_project.id, approvals_required: 5, name: 'other test rule')
+ overridden_project_rule_two = project_rules.create!(project_id: overridden_project.id, approvals_required: 7, name: 'super cool rule')
+
+ merge_request = merge_requests.create!(target_branch: 'feature', source_branch: 'default', source_project_id: normal_project.id, target_project_id: normal_project.id)
+ overridden_merge_request = merge_requests.create!(target_branch: 'feature-2', source_branch: 'default', source_project_id: overridden_project.id, target_project_id: overridden_project.id)
+
+ merge_rule = rules.create!(merge_request_id: merge_request.id, approvals_required: 3, name: 'test rule')
+ overridden_merge_rule = rules.create!(merge_request_id: overridden_merge_request.id, approvals_required: 6, name: 'other test rule')
+ overridden_merge_rule_two = rules.create!(merge_request_id: overridden_merge_request.id, approvals_required: 7, name: 'super cool rule')
+
+ sources.create!(approval_project_rule_id: project_rule.id, approval_merge_request_rule_id: merge_rule.id)
+ sources.create!(approval_project_rule_id: overridden_project_rule.id, approval_merge_request_rule_id: overridden_merge_rule.id)
+ sources.create!(approval_project_rule_id: overridden_project_rule_two.id, approval_merge_request_rule_id: overridden_merge_rule_two.id)
+
+ group1 = groups.create!(name: "group1", path: "test_group1", type: 'Group')
+ group2 = groups.create!(name: "group2", path: "test_group2", type: 'Group')
+ group3 = groups.create!(name: "group3", path: "test_group3", type: 'Group')
+
+ project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group1.id)
+ project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group2.id)
+ project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group3.id)
+
+ mr_groups.create!(approval_merge_request_rule_id: overridden_merge_rule.id, group_id: group1.id)
+ mr_groups.create!(approval_merge_request_rule_id: overridden_merge_rule_two.id, group_id: group2.id)
+ end
+
+ describe '#perform' do
+ it 'changes the correct rules' do
+ original_count = rules.all.count
+
+ determine_if_rules_are_modified.perform(rules.minimum(:id), rules.maximum(:id))
+
+ results = rules.where(modified_from_project_rule: true)
+
+ expect(results.count).to eq 2
+ expect(results.collect(&:name)).to eq(['other test rule', 'super cool rule'])
+ expect(rules.count).to eq original_count
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb b/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb
index a3840e3a22e..85a9c88ebff 100644
--- a/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb
+++ b/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::BackgroundMigration::MergeRequestAssigneesMigrationProgre
described_class.new.perform
- expect(Feature.enabled?(:multiple_merge_request_assignees)).to eq(true)
+ expect(Feature.enabled?(:multiple_merge_request_assignees, type: :licensed)).to eq(true)
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
new file mode 100644
index 00000000000..33498ffa748
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20200925125321 do
+ let(:users) { table(:users) }
+
+ let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
+
+ let(:u2f_registrations) { table(:u2f_registrations) }
+ let(:webauthn_registrations) { table(:webauthn_registrations) }
+
+ let!(:u2f_registration_not_migrated) { create_u2f_registration(1, 'reg1') }
+ let!(:u2f_registration_not_migrated_no_name) { create_u2f_registration(2, nil, 2) }
+ let!(:u2f_registration_migrated) { create_u2f_registration(3, 'reg3') }
+
+ subject { described_class.new.perform(1, 3) }
+
+ before do
+ converted_credential = convert_credential_for(u2f_registration_migrated)
+ webauthn_registrations.create!(converted_credential)
+ end
+
+ it 'migrates all records' do
+ expect { subject }.to change { webauthn_registrations.count }.from(1).to(3)
+
+ all_webauthn_registrations = webauthn_registrations.all.map(&:attributes)
+
+ [u2f_registration_not_migrated, u2f_registration_not_migrated_no_name].each do |u2f_registration|
+ expected_credential = convert_credential_for(u2f_registration).except(:created_at).stringify_keys
+ expect(all_webauthn_registrations).to include(a_hash_including(expected_credential))
+ end
+ end
+
+ def create_u2f_registration(id, name, counter = 5)
+ device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
+ u2f_registrations.create!({ id: id,
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw),
+ counter: counter,
+ name: name,
+ user_id: user.id })
+ end
+
+ def convert_credential_for(u2f_registration)
+ converted_credential = WebAuthn::U2fMigrator.new(
+ app_id: Gitlab.config.gitlab.url,
+ certificate: u2f_registration.certificate,
+ key_handle: u2f_registration.key_handle,
+ public_key: u2f_registration.public_key,
+ counter: u2f_registration.counter
+ ).credential
+
+ {
+ credential_xid: Base64.strict_encode64(converted_credential.id),
+ public_key: Base64.strict_encode64(converted_credential.public_key),
+ counter: u2f_registration.counter,
+ name: u2f_registration.name || '',
+ user_id: u2f_registration.user_id,
+ u2f_registration_id: u2f_registration.id,
+ created_at: u2f_registration.created_at
+ }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb
index db3cbe7ccdc..3cec5cb4c35 100644
--- a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb
@@ -82,21 +82,4 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateUsersBioToUserDetails, :migra
expect(user_detail).to be_nil
end
-
- context 'when `migrate_bio_to_user_details` feature flag is off' do
- before do
- stub_feature_flags(migrate_bio_to_user_details: false)
- end
-
- it 'does nothing' do
- already_existing_user_details = user_details.where(user_id: [
- user_has_different_details.id,
- user_already_has_details.id
- ])
-
- subject
-
- expect(user_details.all).to match_array(already_existing_user_details)
- end
- end
end
diff --git a/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb
new file mode 100644
index 00000000000..fa4f2d1fd88
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20201015073808 do
+ let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') }
+ let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') }
+ let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') }
+ let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') }
+ let(:issue_links) { table(:issue_links) }
+ let!(:blocks_link) { issue_links.create!(source_id: issue1.id, target_id: issue2.id, link_type: 1) }
+ let!(:bidirectional_link) { issue_links.create!(source_id: issue2.id, target_id: issue1.id, link_type: 2) }
+ let!(:blocked_link) { issue_links.create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) }
+
+ subject { described_class.new.perform(issue_links.minimum(:id), issue_links.maximum(:id)) }
+
+ it 'deletes issue links where opposite relation already exists' do
+ expect { subject }.to change { issue_links.count }.by(-1)
+ end
+
+ it 'ignores issue links other than blocked_by' do
+ subject
+
+ expect(blocks_link.reload.link_type).to eq(1)
+ end
+
+ it 'updates blocked_by issue links' do
+ subject
+
+ link = blocked_link.reload
+ expect(link.link_type).to eq(1)
+ expect(link.source_id).to eq(issue3.id)
+ expect(link.target_id).to eq(issue1.id)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
index 392b44d1a1f..2dae4a65eeb 100644
--- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
+++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
@@ -74,14 +74,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent
let(:user_mentions) { merge_request_user_mentions }
let(:resource) { merge_request }
- it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, MergeRequest
+ it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, 'MergeRequest'
context 'when FF disabled' do
before do
stub_feature_flags(migrate_user_mentions: false)
end
- it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, MergeRequest
+ it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, 'MergeRequest'
end
end
@@ -103,14 +103,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent
let(:user_mentions) { commit_user_mentions }
let(:resource) { commit }
- it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, Commit
+ it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, 'Commit'
context 'when FF disabled' do
before do
stub_feature_flags(migrate_user_mentions: false)
end
- it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, Commit
+ it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, 'Commit'
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index d4483bf1754..b723c31c4aa 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
# attributes later.
existing_label.reload
- Timecop.freeze(Time.now + 1.minute) do
+ travel_to(Time.now + 1.minute) do
importer.execute
label_after_import = project.labels.find(existing_label.id)
diff --git a/spec/lib/gitlab/bulk_import/client_spec.rb b/spec/lib/gitlab/bulk_import/client_spec.rb
new file mode 100644
index 00000000000..a6f8dd6d194
--- /dev/null
+++ b/spec/lib/gitlab/bulk_import/client_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BulkImport::Client do
+ include ImportSpecHelper
+
+ let(:uri) { 'http://gitlab.example' }
+ let(:token) { 'token' }
+ let(:resource) { 'resource' }
+
+ subject { described_class.new(uri: uri, token: token) }
+
+ describe '#get' do
+ let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
+
+ shared_examples 'performs network request' do
+ it 'performs network request' do
+ expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double)
+
+ subject.get(resource)
+ end
+ end
+
+ describe 'parsed response' do
+ it 'returns parsed response' do
+ response_double = double(code: 200, success?: true, parsed_response: [{ id: 1 }, { id: 2 }])
+
+ allow(Gitlab::HTTP).to receive(:get).and_return(response_double)
+
+ expect(subject.get(resource)).to eq(response_double.parsed_response)
+ end
+ end
+
+ describe 'request query' do
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ [
+ anything,
+ hash_including(
+ query: {
+ page: described_class::DEFAULT_PAGE,
+ per_page: described_class::DEFAULT_PER_PAGE
+ }
+ )
+ ]
+ end
+ end
+ end
+
+ describe 'request headers' do
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ [
+ anything,
+ hash_including(
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{token}"
+ }
+ )
+ ]
+ end
+ end
+ end
+
+ describe 'request uri' do
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ ['http://gitlab.example:80/api/v4/resource', anything]
+ end
+ end
+ end
+
+ context 'error handling' do
+ context 'when error occurred' do
+ it 'raises ConnectionError' do
+ allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
+
+ expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError)
+ end
+ end
+
+ context 'when response is not success' do
+ it 'raises ConnectionError' do
+ response_double = double(code: 503, success?: false)
+
+ allow(Gitlab::HTTP).to receive(:get).and_return(response_double)
+
+ expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/matching_merge_request_spec.rb b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
new file mode 100644
index 00000000000..ca7ee784ee3
--- /dev/null
+++ b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Checks::MatchingMergeRequest do
+ describe '#match?' do
+ let_it_be(:newrev) { '012345678' }
+ let_it_be(:target_branch) { 'feature' }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:locked_merge_request) do
+ create(:merge_request,
+ :locked,
+ source_project: project,
+ target_project: project,
+ target_branch: target_branch,
+ in_progress_merge_commit_sha: newrev)
+ end
+
+ subject { described_class.new(newrev, target_branch, project) }
+
+ it 'matches a merge request' do
+ expect(subject.match?).to be true
+ end
+
+ it 'does not match any merge request' do
+ matcher = described_class.new(newrev, 'test', project)
+
+ expect(matcher.match?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index 8b1cd812a70..d681447a0e8 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -58,6 +58,15 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
end
+ describe '#set_section_options' do
+ it 'sets the current section\'s options' do
+ options = { collapsed: true }
+ subject.set_section_options(options)
+
+ expect(subject.to_h[:section_options]).to eq(options)
+ end
+ end
+
describe '#set_as_section_header' do
it 'change the section_header to true' do
expect { subject.set_as_section_header }
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
index cb6949fddc2..c9c0d1a744e 100644
--- a/spec/lib/gitlab/ci/ansi2json_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -229,7 +229,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do
expect(convert_json(trace)).to eq([
{
offset: 0,
- content: [{ text: "section_end:1:2<div>hello</div>" }],
+ content: [{ text: 'section_end:1:2<div>hello</div>' }],
section: 'prepare-script',
section_header: true
},
@@ -329,6 +329,32 @@ RSpec.describe Gitlab::Ci::Ansi2json do
])
end
end
+
+ context 'with section options' do
+ let(:option_section_start) { "section_start:#{section_start_time.to_i}:#{section_name}[collapsed=true,unused_option=123]\r\033[0K"}
+
+ it 'provides section options when set' do
+ trace = "#{option_section_start}hello#{section_end}"
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'hello' }],
+ section: 'prepare-script',
+ section_header: true,
+ section_options: {
+ 'collapsed' => 'true',
+ 'unused_option' => '123'
+ }
+ },
+ {
+ offset: 83,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ }
+ ])
+ end
+ end
end
describe 'incremental updates' do
@@ -339,7 +365,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do
context 'with split word' do
let(:pre_text) { "\e[1mHello " }
- let(:text) { "World" }
+ let(:text) { 'World' }
let(:lines) do
[
@@ -355,7 +381,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do
context 'with split word on second line' do
let(:pre_text) { "Good\nmorning " }
- let(:text) { "World" }
+ let(:text) { 'World' }
let(:lines) do
[
@@ -514,7 +540,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do
end
describe 'trucates' do
- let(:text) { "Hello World" }
+ let(:text) { 'Hello World' }
let(:stream) { StringIO.new(text) }
let(:subject) { described_class.convert(stream) }
@@ -522,11 +548,11 @@ RSpec.describe Gitlab::Ci::Ansi2json do
stream.seek(3, IO::SEEK_SET)
end
- it "returns truncated output" do
+ it 'returns truncated output' do
expect(subject.truncated).to be_truthy
end
- it "does not append output" do
+ it 'does not append output' do
expect(subject.append).to be_falsey
end
end
diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
index 83a37655ea9..e982f0eb015 100644
--- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
+++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
@@ -18,17 +18,6 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
end
- context 'when FF ci_new_artifact_file_reader is disabled' do
- before do
- stub_feature_flags(ci_new_artifact_file_reader: false)
- end
-
- it 'returns the content at the path' do
- is_expected.to be_present
- expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
- end
- end
-
context 'when path does not exist' do
let(:path) { 'file/does/not/exist.txt' }
let(:expected_error) do
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index f33176c3da3..8b2e0410474 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ describe '#manual_action?' do
+ context 'when job is a manual action' do
+ let(:config) { { script: 'deploy', when: 'manual' } }
+
+ it { is_expected.to be_manual_action }
+ end
+
+ context 'when job is not a manual action' do
+ let(:config) { { script: 'deploy' } }
+
+ it { is_expected.not_to be_manual_action }
+ end
+ end
+
+ describe '#ignored?' do
+ context 'when job is a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual' }
+ end
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: true }
+ end
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: false }
+ end
+
+ it { is_expected.not_to be_ignored }
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) { { script: 'deploy' } }
+
+ it { is_expected.not_to be_ignored }
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: true } }
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: false } }
+
+ it { is_expected.not_to be_ignored }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 3501812b76e..80427eaa6ee 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -13,18 +13,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
context 'when entry config value is correct' do
let(:policy) { nil }
let(:key) { 'some key' }
+ let(:when_config) { nil }
let(:config) do
- { key: key,
+ {
+ key: key,
untracked: true,
- paths: ['some/path/'],
- policy: policy }
+ paths: ['some/path/']
+ }.tap do |config|
+ config[:policy] = policy if policy
+ config[:when] = when_config if when_config
+ end
end
describe '#value' do
shared_examples 'hash key value' do
it 'returns hash value' do
- expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push')
+ expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success')
end
end
@@ -49,6 +54,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
expect(entry.value).to match(a_hash_including(key: nil))
end
end
+
+ context 'with `policy`' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:policy, :result) do
+ 'pull-push' | 'pull-push'
+ 'push' | 'push'
+ 'pull' | 'pull'
+ 'unknown' | 'unknown' # invalid
+ end
+
+ with_them do
+ it { expect(entry.value).to include(policy: result) }
+ end
+ end
+
+ context 'without `policy`' do
+ it 'assigns policy to default' do
+ expect(entry.value).to include(policy: 'pull-push')
+ end
+ end
+
+ context 'with `when`' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:when_config, :result) do
+ 'on_success' | 'on_success'
+ 'on_failure' | 'on_failure'
+ 'always' | 'always'
+ 'unknown' | 'unknown' # invalid
+ end
+
+ with_them do
+ it { expect(entry.value).to include(when: result) }
+ end
+ end
+
+ context 'without `when`' do
+ it 'assigns when to default' do
+ expect(entry.value).to include(when: 'on_success')
+ end
+ end
end
describe '#valid?' do
@@ -61,28 +108,41 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
end
- context 'policy is pull-push' do
- let(:policy) { 'pull-push' }
+ context 'with `policy`' do
+ using RSpec::Parameterized::TableSyntax
- it { is_expected.to be_valid }
- it { expect(entry.value).to include(policy: 'pull-push') }
- end
-
- context 'policy is push' do
- let(:policy) { 'push' }
+ where(:policy, :valid) do
+ 'pull-push' | true
+ 'push' | true
+ 'pull' | true
+ 'unknown' | false
+ end
- it { is_expected.to be_valid }
- it { expect(entry.value).to include(policy: 'push') }
+ with_them do
+ it 'returns expected validity' do
+ expect(entry.valid?).to eq(valid)
+ end
+ end
end
- context 'policy is pull' do
- let(:policy) { 'pull' }
+ context 'with `when`' do
+ using RSpec::Parameterized::TableSyntax
- it { is_expected.to be_valid }
- it { expect(entry.value).to include(policy: 'pull') }
+ where(:when_config, :valid) do
+ 'on_success' | true
+ 'on_failure' | true
+ 'always' | true
+ 'unknown' | false
+ end
+
+ with_them do
+ it 'returns expected validity' do
+ expect(entry.valid?).to eq(valid)
+ end
+ end
end
- context 'when key is missing' do
+ context 'with key missing' do
let(:config) do
{ untracked: true,
paths: ['some/path/'] }
@@ -110,13 +170,21 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'when policy is unknown' do
- let(:config) { { policy: "unknown" } }
+ let(:config) { { policy: 'unknown' } }
it 'reports error' do
is_expected.to include('cache policy should be pull-push, push, or pull')
end
end
+ context 'when `when` is unknown' do
+ let(:config) { { when: 'unknown' } }
+
+ it 'reports error' do
+ is_expected.to include('cache when should be on_success, on_failure or always')
+ end
+ end
+
context 'when descendants are invalid' do
context 'with invalid keys' do
let(:config) { { key: 1 } }
diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb
index 3e816f70c03..59f0b0e7a48 100644
--- a/spec/lib/gitlab/ci/config/entry/include_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb
@@ -61,6 +61,31 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
end
end
end
+
+ context 'when using "project"' do
+ context 'and specifying "ref" and "file"' do
+ let(:config) { { project: 'my-group/my-pipeline-library', ref: 'master', file: 'test.yml' } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'without "ref"' do
+ let(:config) { { project: 'my-group/my-pipeline-library', file: 'test.yml' } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'without "file"' do
+ let(:config) { { project: 'my-group/my-pipeline-library' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'has specific error' do
+ expect(include_entry.errors)
+ .to include('include config must specify the file where to fetch the config from')
+ end
+ end
+ end
end
context 'when value is something else' do
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index ab760b107f8..e0e8bc93770 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -537,7 +537,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'overrides default config' do
expect(entry[:image].value).to eq(name: 'some_image')
- expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
+ expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success')
end
end
@@ -552,7 +552,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'uses config from default entry' do
expect(entry[:image].value).to eq 'specified'
- expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push')
+ expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success')
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
index 39697884e3b..3388ae0af2f 100644
--- a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
@@ -46,33 +46,140 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
end
end
- context 'when entry config has only one variable' do
- let(:config) do
- [
- {
- 'VAR_1' => %w[test]
- }
- ]
+ context 'with one_dimensional_matrix feature flag enabled' do
+ before do
+ stub_feature_flags(one_dimensional_matrix: true)
+ matrix.compose!
end
- describe '#valid?' do
- it { is_expected.not_to be_valid }
- end
+ context 'when entry config has only one variable with multiple values' do
+ let(:config) do
+ [
+ {
+ 'VAR_1' => %w[build test]
+ }
+ ]
+ end
- describe '#errors' do
- it 'returns error about too many jobs' do
- expect(matrix.errors)
- .to include('variables config requires at least 2 items')
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns no errors' do
+ expect(matrix.errors)
+ .to be_empty
+ end
+ end
+
+ describe '#value' do
+ before do
+ matrix.compose!
+ end
+
+ it 'returns the value without raising an error' do
+ expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
+ end
end
+
+ context 'when entry config has only one variable with one value' do
+ let(:config) do
+ [
+ {
+ 'VAR_1' => %w[test]
+ }
+ ]
+ end
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns no errors' do
+ expect(matrix.errors)
+ .to be_empty
+ end
+ end
+
+ describe '#value' do
+ before do
+ matrix.compose!
+ end
+
+ it 'returns the value without raising an error' do
+ expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
+ end
+ end
+ end
+ end
+ end
+
+ context 'with one_dimensional_matrix feature flag disabled' do
+ before do
+ stub_feature_flags(one_dimensional_matrix: false)
+ matrix.compose!
end
- describe '#value' do
- before do
- matrix.compose!
+ context 'when entry config has only one variable with multiple values' do
+ let(:config) do
+ [
+ {
+ 'VAR_1' => %w[build test]
+ }
+ ]
end
- it 'returns the value without raising an error' do
- expect(matrix.value).to eq([{ 'VAR_1' => ['test'] }])
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about too many jobs' do
+ expect(matrix.errors)
+ .to include('variables config requires at least 2 items')
+ end
+ end
+
+ describe '#value' do
+ before do
+ matrix.compose!
+ end
+
+ it 'returns the value without raising an error' do
+ expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
+ end
+ end
+
+ context 'when entry config has only one variable with one value' do
+ let(:config) do
+ [
+ {
+ 'VAR_1' => %w[test]
+ }
+ ]
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns no errors' do
+ expect(matrix.errors)
+ .to include('variables config requires at least 2 items')
+ end
+ end
+
+ describe '#value' do
+ before do
+ matrix.compose!
+ end
+
+ it 'returns the value without raising an error' do
+ expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb
index 230b001d620..407efb438b5 100644
--- a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+# After Feature one_dimensional_matrix is removed, this can be changed back to fast_spec_helper
+require 'spec_helper'
require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
@@ -45,43 +46,71 @@ RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
end
end
- context 'when entry value is not correct' do
- shared_examples 'invalid variables' do |message|
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors).to include(message)
- end
+ context 'with one_dimensional_matrix feature flag enabled' do
+ context 'with only one variable' do
+ before do
+ stub_feature_flags(one_dimensional_matrix: true)
end
+ let(:config) { { VAR: 'test' } }
describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
end
end
end
+ end
- context 'with array' do
- let(:config) { [:VAR, 'test'] }
+ context 'with one_dimensional_matrix feature flag disabled' do
+ context 'when entry value is not correct' do
+ before do
+ stub_feature_flags(one_dimensional_matrix: false)
+ end
+ shared_examples 'invalid variables' do |message|
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors).to include(message)
+ end
+ end
- it_behaves_like 'invalid variables', /should be a hash of key value pairs/
- end
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
- context 'with empty array' do
- let(:config) { { VAR: 'test', VAR2: [] } }
+ context 'with array' do
+ let(:config) { [:VAR, 'test'] }
- it_behaves_like 'invalid variables', /should be a hash of key value pairs/
- end
+ it_behaves_like 'invalid variables', /should be a hash of key value pairs/
+ end
- context 'with nested array' do
- let(:config) { { VAR: 'test', VAR2: [1, [2]] } }
+ context 'with empty array' do
+ let(:config) { { VAR: 'test', VAR2: [] } }
- it_behaves_like 'invalid variables', /should be a hash of key value pairs/
- end
+ it_behaves_like 'invalid variables', /should be a hash of key value pairs/
+ end
- context 'with only one variable' do
- let(:config) { { VAR: 'test' } }
+ context 'with nested array' do
+ let(:config) { { VAR: 'test', VAR2: [1, [2]] } }
+
+ it_behaves_like 'invalid variables', /should be a hash of key value pairs/
+ end
- it_behaves_like 'invalid variables', /variables config requires at least 2 items/
+ context 'with one_dimensional_matrix feature flag disabled' do
+ context 'with only one variable' do
+ let(:config) { { VAR: 'test' } }
+
+ it_behaves_like 'invalid variables', /variables config requires at least 2 items/
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 252bda6461d..79716df6b60 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
+ cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
variables: { 'VAR' => 'root' },
ignore: false,
after_script: ['make clean'],
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
+ cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
variables: { 'VAR' => 'root' },
ignore: false,
after_script: ['make clean'],
@@ -156,7 +156,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
image: { name: "ruby:2.7" },
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
- cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" },
+ cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' },
only: { refs: %w(branches tags) },
variables: { 'VAR' => 'job' },
after_script: [],
@@ -203,7 +203,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
+ cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
variables: { 'VAR' => 'root' },
ignore: false,
after_script: ['make clean'],
@@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'ruby:2.7' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" },
+ cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
variables: { 'VAR' => 'job' },
ignore: false,
after_script: ['make clean'],
@@ -261,7 +261,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
describe '#cache_value' do
it 'returns correct cache definition' do
- expect(root.cache_value).to eq(key: 'a', policy: 'pull-push')
+ expect(root.cache_value).to eq(key: 'a', policy: 'pull-push', when: 'on_success')
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index d6391092f63..ac33f858f43 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -3,56 +3,109 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Variables do
- let(:entry) { described_class.new(config) }
+ subject { described_class.new(config) }
- describe 'validations' do
- context 'when entry config value is correct' do
- let(:config) do
- { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ shared_examples 'valid config' do
+ describe '#value' do
+ it 'returns hash with key value strings' do
+ expect(subject.value).to eq result
end
+ end
- describe '#value' do
- it 'returns hash with key value strings' do
- expect(entry.value).to eq config
- end
-
- context 'with numeric keys and values in the config' do
- let(:config) { { 10 => 20 } }
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(subject.errors).to be_empty
+ end
+ end
- it 'converts numeric key and numeric value into strings' do
- expect(entry.value).to eq('10' => '20')
- end
- end
+ describe '#valid?' do
+ it 'is valid' do
+ expect(subject).to be_valid
end
+ end
+ end
- describe '#errors' do
- it 'does not append errors' do
- expect(entry.errors).to be_empty
- end
+ shared_examples 'invalid config' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(subject).not_to be_valid
end
+ end
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
+ describe '#errors' do
+ it 'saves errors' do
+ expect(subject.errors)
+ .to include /should be a hash of key value pairs/
end
end
+ end
- context 'when entry value is not correct' do
- let(:config) { [:VAR, 'test'] }
+ context 'when entry config value has key-value pairs' do
+ let(:config) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include /should be a hash of key value pairs/
- end
- end
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
+ it_behaves_like 'valid config'
+ end
+
+ context 'with numeric keys and values in the config' do
+ let(:config) { { 10 => 20 } }
+ let(:result) do
+ { '10' => '20' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+
+ context 'when entry config value has key-value pair and hash' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
+ 'VARIABLE_2' => 'value 2' }
+ end
+
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
+ end
+
+ it_behaves_like 'valid config'
+ end
+
+ context 'when entry value is an array' do
+ let(:config) { [:VAR, 'test'] }
+
+ it_behaves_like 'invalid config'
+ end
+
+ context 'when entry value has hash with other key-pairs' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' },
+ 'VARIABLE_2' => 'value 2' }
end
+
+ it_behaves_like 'invalid config'
+ end
+
+ context 'when entry config value has hash with nil description' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1', description: nil } }
+ end
+
+ it_behaves_like 'invalid config'
+ end
+
+ context 'when entry config value has hash without description' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1' } }
+ end
+
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1' }
+ end
+
+ it_behaves_like 'valid config'
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index f724825a9cc..dd27b4045c9 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when PST (Pacific Standard Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
+ travel_to(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when PDT (Pacific Daylight Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
+ travel_to(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -117,7 +117,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when CET (Central European Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
+ travel_to(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -125,7 +125,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when CEST (Central European Summer Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
+ travel_to(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -152,7 +152,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when EST (Eastern Standard Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
+ travel_to(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::Ci::CronParser do
context 'when EDT (Eastern Daylight Time)' do
it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
+ travel_to(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
@@ -174,7 +174,7 @@ RSpec.describe Gitlab::Ci::CronParser do
# (e.g. America/Chicago) at the start of the test. Stubbing
# TZ doesn't appear to be enough.
it 'generates day without TZInfo::AmbiguousTime error' do
- Timecop.freeze(Time.utc(2020, 1, 1)) do
+ travel_to(Time.utc(2020, 1, 1)) do
expect(subject.year).to eq(year)
expect(subject.month).to eq(12)
expect(subject.day).to eq(1)
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index 077c0fd3162..c67f8464123 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_it_be(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:lint) { described_class.new(project: project, current_user: user) }
@@ -61,6 +61,43 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
+ shared_examples 'sets merged yaml' do
+ let(:content) do
+ <<~YAML
+ :include:
+ :local: another-gitlab-ci.yml
+ :test_job:
+ :stage: test
+ :script: echo
+ YAML
+ end
+
+ let(:included_content) do
+ <<~YAML
+ :another_job:
+ :script: echo
+ YAML
+ end
+
+ before do
+ project.repository.create_file(
+ project.creator,
+ 'another-gitlab-ci.yml',
+ included_content,
+ message: 'Automatically created 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])
+ expected_config = included_config.merge(root_config).except(:include)
+
+ expect(subject.merged_yaml).to eq(expected_config.to_yaml)
+ end
+ end
+
shared_examples 'content with errors and warnings' do
context 'when content has errors' do
let(:content) do
@@ -173,6 +210,8 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
+ it_behaves_like 'sets merged yaml'
+
include_context 'advanced validations' do
it 'does not catch advanced logical errors' do
expect(subject).to be_valid
@@ -203,6 +242,8 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
+ it_behaves_like 'sets merged yaml'
+
include_context 'advanced validations' do
it 'runs advanced logical validations' do
expect(subject).not_to be_valid
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 1f497dea2bf..7da602251a5 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -4,11 +4,12 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
describe '#parse!' do
- subject { described_class.new.parse!(junit, test_suite, args) }
+ subject { described_class.new.parse!(junit, test_suite, job: job) }
let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') }
let(:test_cases) { flattened_test_cases(test_suite) }
- let(:args) { { job: { id: 1, project: "project" } } }
+ let(:job) { double(max_test_cases_per_report: max_test_cases) }
+ let(:max_test_cases) { 0 }
context 'when data is JUnit style XML' do
context 'when there are no <testcases> in <testsuite>' do
@@ -43,7 +44,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites>
- <testsuite>
+ <testsuite name='Math'>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
</testsuite>
</testsuites>
@@ -53,6 +54,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
it 'parses XML and adds a test case to a suite' do
expect { subject }.not_to raise_error
+ expect(test_cases[0].suite_name).to eq('Math')
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
@@ -62,7 +64,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
context 'when there is <testcase>' do
let(:junit) do
<<-EOF.strip_heredoc
- <testsuite>
+ <testsuite name='Math'>
<testcase classname='Calculator' name='sumTest1' time='0.01'>
#{testcase_content}
</testcase>
@@ -79,6 +81,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
shared_examples_for '<testcase> XML parser' do |status, output|
it 'parses XML and adds a test case to the suite' do
aggregate_failures do
+ expect(test_case.suite_name).to eq('Math')
expect(test_case.classname).to eq('Calculator')
expect(test_case.name).to eq('sumTest1')
expect(test_case.execution_time).to eq(0.01)
@@ -152,13 +155,15 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
expect { subject }.not_to raise_error
expect(test_cases.count).to eq(1)
+ expect(test_cases.first.suite_name).to eq("XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest")
+ expect(test_cases.first.name).to eq("testIndexAction")
end
end
context 'when there are two test cases' do
let(:junit) do
<<-EOF.strip_heredoc
- <testsuite>
+ <testsuite name='Math'>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
<testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
</testsuite>
@@ -168,9 +173,11 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
it 'parses XML and adds test cases to a suite' do
expect { subject }.not_to raise_error
+ expect(test_cases[0].suite_name).to eq('Math')
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
+ expect(test_cases[1].suite_name).to eq('Math')
expect(test_cases[1].classname).to eq('Calculator')
expect(test_cases[1].name).to eq('sumTest2')
expect(test_cases[1].execution_time).to eq(0.02)
@@ -181,7 +188,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites>
- <testsuite>
+ <testsuite name='Math'>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
<testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
</testsuite>
@@ -196,18 +203,81 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
it 'parses XML and adds test cases to a suite' do
expect { subject }.not_to raise_error
- expect(test_cases[0].classname).to eq('Calculator')
- expect(test_cases[0].name).to eq('sumTest1')
- expect(test_cases[0].execution_time).to eq(0.01)
- expect(test_cases[1].classname).to eq('Calculator')
- expect(test_cases[1].name).to eq('sumTest2')
- expect(test_cases[1].execution_time).to eq(0.02)
- expect(test_cases[2].classname).to eq('Statemachine')
- expect(test_cases[2].name).to eq('happy path')
- expect(test_cases[2].execution_time).to eq(100)
- expect(test_cases[3].classname).to eq('Statemachine')
- expect(test_cases[3].name).to eq('unhappy path')
- expect(test_cases[3].execution_time).to eq(200)
+ expect(test_cases).to contain_exactly(
+ have_attributes(
+ suite_name: 'Math',
+ classname: 'Calculator',
+ name: 'sumTest1',
+ execution_time: 0.01
+ ),
+ have_attributes(
+ suite_name: 'Math',
+ classname: 'Calculator',
+ name: 'sumTest2',
+ execution_time: 0.02
+ ),
+ have_attributes(
+ suite_name: test_suite.name, # Defaults to test suite instance's name
+ classname: 'Statemachine',
+ name: 'happy path',
+ execution_time: 100
+ ),
+ have_attributes(
+ suite_name: test_suite.name, # Defaults to test suite instance's name
+ classname: 'Statemachine',
+ name: 'unhappy path',
+ execution_time: 200
+ )
+ )
+ end
+ end
+
+ context 'when number of test cases exceeds the max_test_cases limit' do
+ let(:max_test_cases) { 1 }
+
+ shared_examples_for 'rejecting too many test cases' do
+ it 'attaches an error to the TestSuite object' do
+ expect { subject }.not_to raise_error
+ expect(test_suite.suite_error).to eq("JUnit data parsing failed: number of test cases exceeded the limit of #{max_test_cases}")
+ end
+ end
+
+ context 'and test cases are unique' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuites>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
+ </testsuite>
+ <testsuite>
+ <testcase classname='Statemachine' name='happy path' time='100'></testcase>
+ <testcase classname='Statemachine' name='unhappy path' time='200'></testcase>
+ </testsuite>
+ </testsuites>
+ EOF
+ end
+
+ it_behaves_like 'rejecting too many test cases'
+ end
+
+ context 'and test cases are duplicates' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuites>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
+ </testsuite>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
+ </testsuite>
+ </testsuites>
+ EOF
+ end
+
+ it_behaves_like 'rejecting too many test cases'
end
end
end
@@ -296,9 +366,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
expect(test_cases[0].has_attachment?).to be_truthy
expect(test_cases[0].attachment).to eq("some/path.png")
- expect(test_cases[0].job).to be_present
- expect(test_cases[0].job[:id]).to eq(1)
- expect(test_cases[0].job[:project]).to eq("project")
+ expect(test_cases[0].job).to eq(job)
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
index 74c014b6408..570706bfaac 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -224,7 +224,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
key: 'a-key',
paths: ['vendor/ruby'],
untracked: true,
- policy: 'push'
+ policy: 'push',
+ when: 'on_success'
}
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 34df0e86a18..0b961336f3f 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
- let(:project) { create(:project, :repository) }
- let(:head_sha) { project.repository.head_commit.id }
- let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: head_sha) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:head_sha) { project.repository.head_commit.id }
+ let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) }
let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } }
let(:previous_stages) { [] }
@@ -503,7 +503,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
using RSpec::Parameterized
let(:pipeline) do
- build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project)
end
context 'matches' do
@@ -766,7 +766,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
context 'with a matching changes: rule' do
let(:pipeline) do
- create(:ci_pipeline, project: project).tap do |pipeline|
+ build(:ci_pipeline, project: project).tap do |pipeline|
stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml])
end
end
diff --git a/spec/lib/gitlab/ci/reports/test_case_spec.rb b/spec/lib/gitlab/ci/reports/test_case_spec.rb
index 7fb208213c1..a142846fc18 100644
--- a/spec/lib/gitlab/ci/reports/test_case_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_case_spec.rb
@@ -6,39 +6,26 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do
describe '#initialize' do
let(:test_case) { described_class.new(params) }
- context 'when both classname and name are given' do
- context 'when test case is passed' do
- let(:job) { build(:ci_build) }
- let(:params) { attributes_for(:test_case).merge!(job: job) }
-
- it 'initializes an instance' do
- expect { test_case }.not_to raise_error
-
- expect(test_case.name).to eq('test-1')
- expect(test_case.classname).to eq('trace')
- expect(test_case.file).to eq('spec/trace_spec.rb')
- expect(test_case.execution_time).to eq(1.23)
- expect(test_case.status).to eq(described_class::STATUS_SUCCESS)
- expect(test_case.system_output).to be_nil
- expect(test_case.job).to be_present
- end
- end
+ context 'when required params are given' do
+ let(:job) { build(:ci_build) }
+ let(:params) { attributes_for(:test_case).merge!(job: job) }
- context 'when test case is failed' do
- let(:job) { build(:ci_build) }
- let(:params) { attributes_for(:test_case, :failed).merge!(job: job) }
-
- it 'initializes an instance' do
- expect { test_case }.not_to raise_error
-
- expect(test_case.name).to eq('test-1')
- expect(test_case.classname).to eq('trace')
- expect(test_case.file).to eq('spec/trace_spec.rb')
- expect(test_case.execution_time).to eq(1.23)
- expect(test_case.status).to eq(described_class::STATUS_FAILED)
- expect(test_case.system_output)
- .to eq('Failure/Error: is_expected.to eq(300) expected: 300 got: -100')
- end
+ it 'initializes an instance', :aggregate_failures do
+ expect { test_case }.not_to raise_error
+
+ expect(test_case).to have_attributes(
+ suite_name: params[:suite_name],
+ name: params[:name],
+ classname: params[:classname],
+ file: params[:file],
+ execution_time: params[:execution_time],
+ status: params[:status],
+ system_output: params[:system_output],
+ job: params[:job]
+ )
+
+ key = "#{test_case.suite_name}_#{test_case.classname}_#{test_case.name}"
+ expect(test_case.key).to eq(Digest::SHA256.hexdigest(key))
end
end
@@ -53,6 +40,10 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do
end
end
+ context 'when suite_name is missing' do
+ it_behaves_like 'param is missing', :suite_name
+ end
+
context 'when classname is missing' do
it_behaves_like 'param is missing', :classname
end
diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
index 15fa78444e5..50d1595da73 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
@@ -229,6 +229,20 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
end
end
+ describe '#each_test_case' do
+ before do
+ test_suite.add_test_case(test_case_success)
+ test_suite.add_test_case(test_case_failed)
+ test_suite.add_test_case(test_case_skipped)
+ test_suite.add_test_case(test_case_error)
+ end
+
+ it 'yields each test case to given block' do
+ expect { |b| test_suite.each_test_case(&b) }
+ .to yield_successive_args(test_case_success, test_case_failed, test_case_skipped, test_case_error)
+ end
+ end
+
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}_count" do
subject { test_suite.public_send("#{status_type}_count") }
diff --git a/spec/lib/gitlab/ci/runner/backoff_spec.rb b/spec/lib/gitlab/ci/runner/backoff_spec.rb
new file mode 100644
index 00000000000..f147d69f7cd
--- /dev/null
+++ b/spec/lib/gitlab/ci/runner/backoff_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require 'active_support/testing/time_helpers'
+
+RSpec.describe Gitlab::Ci::Runner::Backoff do
+ include ActiveSupport::Testing::TimeHelpers
+
+ describe '#duration' do
+ it 'returns backoff duration from start' do
+ freeze_time do
+ described_class.new(5.minutes.ago).then do |backoff|
+ expect(backoff.duration).to eq 5.minutes
+ end
+ end
+ end
+
+ it 'returns an integer value' do
+ freeze_time do
+ described_class.new(5.seconds.ago).then do |backoff|
+ expect(backoff.duration).to be 5
+ end
+ end
+ end
+
+ it 'returns the smallest number greater than or equal to duration' do
+ freeze_time do
+ described_class.new(0.5.seconds.ago).then do |backoff|
+ expect(backoff.duration).to be 1
+ end
+ end
+ end
+ end
+
+ describe '#slot' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:started, :slot) do
+ 0 | 0
+ 0.1 | 0
+ 0.9 | 0
+ 1 | 0
+ 1.1 | 0
+ 1.9 | 0
+ 2 | 0
+ 2.9 | 0
+ 3 | 0
+ 4 | 1
+ 5 | 1
+ 6 | 1
+ 7 | 1
+ 8 | 2
+ 9 | 2
+ 9.9 | 2
+ 10 | 2
+ 15 | 2
+ 16 | 3
+ 31 | 3
+ 32 | 4
+ 63 | 4
+ 64 | 5
+ 127 | 5
+ 128 | 6
+ 250 | 6
+ 310 | 7
+ 520 | 8
+ 999 | 8
+ end
+
+ with_them do
+ it 'falls into an appropaite backoff slot' do
+ freeze_time do
+ backoff = described_class.new(started.seconds.ago)
+ expect(backoff.slot).to eq slot
+ end
+ end
+ end
+ end
+
+ describe '#to_seconds' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:started, :backoff) do
+ 0 | 1
+ 0.1 | 1
+ 0.9 | 1
+ 1 | 1
+ 1.1 | 1
+ 1.9 | 1
+ 2 | 1
+ 3 | 1
+ 4 | 2
+ 5 | 2
+ 6 | 2
+ 6.5 | 2
+ 7 | 2
+ 8 | 4
+ 9 | 4
+ 9.9 | 4
+ 10 | 4
+ 15 | 4
+ 16 | 8
+ 31 | 8
+ 32 | 16
+ 63 | 16
+ 64 | 32
+ 127 | 32
+ 128 | 64
+ 250 | 64
+ 310 | 64
+ 520 | 64
+ 999 | 64
+ end
+
+ with_them do
+ it 'calculates backoff based on an appropriate slot' do
+ freeze_time do
+ described_class.new(started.seconds.ago).then do |delay|
+ expect(delay.to_seconds).to eq backoff
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
index 92600b21afc..37524afc83d 100644
--- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
@@ -30,15 +30,6 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
it { expect(subject).to have_details }
it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" }
-
- context 'when ci_bridge_pipeline_details is disabled' do
- before do
- stub_feature_flags(ci_bridge_pipeline_details: false)
- end
-
- it { expect(subject).not_to have_details }
- it { expect(subject.details_path).to be_nil }
- end
end
context 'when user does not have access to read downstream pipeline' do
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index 021b777a0ff..d27bb98ba9a 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is created' do
- let(:bridge) { create(:ci_bridge) }
+ let(:bridge) { create_bridge(:created) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Created
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is failed' do
- let(:bridge) { create(:ci_bridge, :failed) }
+ let(:bridge) { create_bridge(:failed) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
@@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
end
end
+
+ context 'when bridge is a manual action' do
+ let(:bridge) { create_bridge(:playable) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Bridge::Manual,
+ Gitlab::Ci::Status::Bridge::Play,
+ Gitlab::Ci::Status::Bridge::Action]
+ end
+
+ it 'fabricates action detailed status' do
+ expect(status).to be_a Gitlab::Ci::Status::Bridge::Action
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq s_('CiStatusText|manual')
+ expect(status.group).to eq 'manual'
+ expect(status.icon).to eq 'status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
+ expect(status.illustration).to include(:image, :size, :title, :content)
+ expect(status.label).to include 'manual play action'
+ expect(status).not_to have_details
+ expect(status.action_path).to include 'play'
+ end
+
+ context 'when user has ability to play action' do
+ before do
+ bridge.downstream_project.add_developer(user)
+ end
+
+ it 'fabricates status that has action' do
+ expect(status).to have_action
+ end
+ end
+
+ context 'when user does not have ability to play action' do
+ it 'fabricates status that has no action' do
+ expect(status).not_to have_action
+ end
+ end
+ end
+
+ private
+
+ def create_bridge(trait)
+ upstream_project = create(:project, :repository)
+ downstream_project = create(:project, :repository)
+ upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project)
+ trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } }
+
+ create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline)
+ end
end
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index a35efae5c57..7fae76f61ea 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Canceled do
describe '#group' do
it { expect(subject.group).to eq 'canceled' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 1ddced923f6..1e54d1ed8c5 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Created do
describe '#group' do
it { expect(subject.group).to eq 'created' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index e8bd728b740..f3f3304b04d 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Failed do
describe '#group' do
it { expect(subject.group).to eq 'failed' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 0e47b19d9c1..1c062a0133d 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Pending do
describe '#group' do
it { expect(subject.group).to eq 'pending' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb
index 6d33eb77560..ec1850c1959 100644
--- a/spec/lib/gitlab/ci/status/preparing_spec.rb
+++ b/spec/lib/gitlab/ci/status/preparing_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Preparing do
describe '#group' do
it { expect(subject.group).to eq 'preparing' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index fbc7bfd81b3..e40d696ee4d 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Running do
describe '#group' do
it { expect(subject.group).to eq 'running' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb
index 4a1dae937ca..8a923faf3f9 100644
--- a/spec/lib/gitlab/ci/status/scheduled_spec.rb
+++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Scheduled do
describe '#group' do
it { expect(subject.group).to eq 'scheduled' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index f402bbe5221..ac3c2f253f7 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Skipped do
describe '#group' do
it { expect(subject.group).to eq 'skipped' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 2d1c50448d4..f2069334abd 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Success do
describe '#group' do
it { expect(subject.group).to eq 'success' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
index de18198c6c2..bb6139accaf 100644
--- a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
+++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
@@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do
describe '#group' do
it { expect(subject.group).to eq 'waiting-for-resource' }
end
+
+ describe '#details_path' do
+ it { expect(subject.details_path).to be_nil }
+ end
end
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
new file mode 100644
index 00000000000..8df739d9245
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,27 @@
+# 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(:user) { create(:admin) }
+ let(:default_branch) { 'master' }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ 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/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..5eec021b9d7
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
+ before do
+ allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([])
+ end
+
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform.latest') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master branch' do
+ it 'creates init, validate and build jobs' do
+ 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)
+ end
+
+ it 'does not creates a deploy and a test job' do
+ expect(build_names).not_to include('deploy')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/checksum_spec.rb b/spec/lib/gitlab/ci/trace/checksum_spec.rb
new file mode 100644
index 00000000000..794794c3f69
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/checksum_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Trace::Checksum do
+ let(:build) { create(:ci_build, :running) }
+
+ subject { described_class.new(build) }
+
+ context 'when build pending state exists' do
+ before do
+ create(:ci_build_pending_state, build: build, trace_checksum: 'crc32:d4777540')
+ end
+
+ context 'when matching persisted trace chunks exist' do
+ before do
+ create_chunk(index: 0, data: 'a' * 128.kilobytes)
+ create_chunk(index: 1, data: 'b' * 128.kilobytes)
+ create_chunk(index: 2, data: 'ccccccccccccccccc')
+ end
+
+ it 'calculates combined trace chunks CRC32 correctly' do
+ expect(subject.chunks_crc32).to eq 3564598592
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when trace chunks were persisted in a wrong order' do
+ before do
+ create_chunk(index: 0, data: 'b' * 128.kilobytes)
+ create_chunk(index: 1, data: 'a' * 128.kilobytes)
+ create_chunk(index: 2, data: 'ccccccccccccccccc')
+ end
+
+ it 'makes trace checksum invalid' do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context 'when one of the trace chunks is missing' do
+ before do
+ create_chunk(index: 0, data: 'a' * 128.kilobytes)
+ create_chunk(index: 2, data: 'ccccccccccccccccc')
+ end
+
+ it 'makes trace checksum invalid' do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context 'when checksums of persisted trace chunks do not match' do
+ before do
+ create_chunk(index: 0, data: 'a' * 128.kilobytes)
+ create_chunk(index: 1, data: 'X' * 128.kilobytes)
+ create_chunk(index: 2, data: 'ccccccccccccccccc')
+ end
+
+ it 'makes trace checksum invalid' do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context 'when persisted trace chunks are missing' do
+ it 'makes trace checksum invalid' do
+ expect(subject.state_crc32).to eq 3564598592
+ expect(subject).not_to be_valid
+ end
+ end
+ end
+
+ context 'when build pending state is missing' do
+ describe '#state_crc32' do
+ it 'returns nil' do
+ expect(subject.state_crc32).to be_nil
+ end
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe '#trace_chunks' do
+ before do
+ create_chunk(index: 0, data: 'abcdefg')
+ end
+
+ it 'does not load raw_data from a database store' do
+ subject.trace_chunks.first.then do |chunk|
+ expect(chunk).to be_database
+ expect { chunk.raw_data }
+ .to raise_error ActiveModel::MissingAttributeError
+ end
+ end
+ end
+
+ describe '#last_chunk' do
+ context 'when there are no chunks' do
+ it 'returns nil' do
+ expect(subject.last_chunk).to be_nil
+ end
+ end
+
+ context 'when there are multiple chunks' do
+ before do
+ create_chunk(index: 1, data: '1234')
+ create_chunk(index: 0, data: 'abcd')
+ end
+
+ it 'returns chunk with the highest index' do
+ expect(subject.last_chunk.chunk_index).to eq 1
+ end
+ end
+ end
+
+ def create_chunk(index:, data:)
+ create(:ci_build_trace_chunk, :persisted, build: build,
+ chunk_index: index,
+ initial_data: data)
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/metrics_spec.rb b/spec/lib/gitlab/ci/trace/metrics_spec.rb
new file mode 100644
index 00000000000..6518d0ab075
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/metrics_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Trace::Metrics, :prometheus do
+ describe '#increment_trace_bytes' do
+ context 'when incrementing by more than one' do
+ it 'increments a single counter' do
+ subject.increment_trace_bytes(10)
+ subject.increment_trace_bytes(20)
+ subject.increment_trace_bytes(30)
+
+ expect(described_class.trace_bytes.get).to eq 60
+ expect(described_class.trace_bytes.values.count).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 171877dbaee..92bf2519588 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do
- let(:build) { create(:ci_build) }
+RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_default: :keep do
+ let_it_be(:project) { create_default(:project) }
+ let_it_be_with_reload(:build) { create(:ci_build) }
let(:trace) { described_class.new(build) }
describe "associations" do
@@ -32,6 +33,16 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do
expect(artifact2.job.trace.raw).to eq(test_data)
end
+
+ it 'reloads the trace in case of a chunk error' do
+ chunk_error = described_class::ChunkedIO::FailedToGetChunkError
+
+ allow_any_instance_of(described_class::Stream)
+ .to receive(:raw).and_raise(chunk_error)
+
+ expect(build).to receive(:reset).and_return(build)
+ expect { trace.raw }.to raise_error(chunk_error)
+ end
end
context 'when live trace feature is disabled' do
@@ -111,4 +122,13 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do
end
end
end
+
+ describe '#lock' do
+ it 'acquires an exclusive lease on the trace' do
+ trace.lock do
+ expect { trace.lock }
+ .to raise_error described_class::LockedError
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
new file mode 100644
index 00000000000..7e3cd7ec254
--- /dev/null
+++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module Gitlab
+ module Ci
+ class YamlProcessor
+ RSpec.describe Result do
+ include StubRequests
+
+ let(:user) { create(:user) }
+ let(:ci_config) { Gitlab::Ci::Config.new(config_content, user: user) }
+ let(:result) { described_class.new(ci_config: ci_config, warnings: ci_config&.warnings) }
+
+ describe '#merged_yaml' do
+ subject(:merged_yaml) { result.merged_yaml }
+
+ let(:config_content) do
+ YAML.dump(
+ include: { remote: 'https://example.com/sample.yml' },
+ test: { stage: 'test', script: 'echo' }
+ )
+ end
+
+ let(:included_yml) do
+ YAML.dump(
+ another_test: { stage: 'test', script: 'echo 2' }
+ )
+ end
+
+ before do
+ stub_full_request('https://example.com/sample.yml').to_return(body: included_yml)
+ end
+
+ it 'returns expanded yaml config' do
+ expanded_config = YAML.safe_load(merged_yaml, [Symbol])
+ included_config = YAML.safe_load(included_yml, [Symbol])
+
+ expect(expanded_config).to include(*included_config.keys)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d596494a987..fb6395e888a 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1361,7 +1361,8 @@ module Gitlab
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
@@ -1383,7 +1384,8 @@ module Gitlab
paths: ["logs/", "binaries/"],
untracked: true,
key: { files: ['file'] },
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
@@ -1402,7 +1404,8 @@ module Gitlab
paths: ['logs/', 'binaries/'],
untracked: true,
key: 'key',
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
@@ -1425,7 +1428,8 @@ module Gitlab
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'] },
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
@@ -1448,7 +1452,8 @@ module Gitlab
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'], prefix: 'prefix' },
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
@@ -1468,7 +1473,8 @@ module Gitlab
paths: ["test/"],
untracked: false,
key: 'local',
- policy: 'pull-push'
+ policy: 'pull-push',
+ when: 'on_success'
)
end
end
@@ -2240,47 +2246,49 @@ module Gitlab
end
describe 'with parent-child pipeline' do
+ let(:config) do
+ YAML.dump({
+ build1: { stage: 'build', script: 'test' },
+ test1: {
+ stage: 'test',
+ trigger: {
+ include: includes
+ }
+ }
+ })
+ end
+
context 'when artifact and job are specified' do
- let(:config) do
- YAML.dump({
- build1: { stage: 'build', script: 'test' },
- test1: { stage: 'test', trigger: {
- include: [{ artifact: 'generated.yml', job: 'build1' }]
- } }
- })
- end
+ let(:includes) { [{ artifact: 'generated.yml', job: 'build1' }] }
it { is_expected.to be_valid }
end
- context 'when job is not specified specified while artifact is' do
- let(:config) do
- YAML.dump({
- build1: { stage: 'build', script: 'test' },
- test1: { stage: 'test', trigger: {
- include: [{ artifact: 'generated.yml' }]
- } }
- })
- end
+ context 'when job is not specified while artifact is' do
+ let(:includes) { [{ artifact: 'generated.yml' }] }
it_behaves_like 'returns errors', /include config must specify the job where to fetch the artifact from/
end
- context 'when include is a string' do
- let(:config) do
- YAML.dump({
- build1: { stage: 'build', script: 'test' },
- test1: {
- stage: 'test',
- trigger: {
- include: 'generated.yml'
- }
- }
- })
+ context 'when project and file are specified' do
+ let(:includes) do
+ [{ file: 'generated.yml', project: 'my-namespace/my-project' }]
end
it { is_expected.to be_valid }
end
+
+ context 'when file is not specified while project is' do
+ let(:includes) { [{ project: 'something' }] }
+
+ it_behaves_like 'returns errors', /include config must specify the file where to fetch the config from/
+ end
+
+ context 'when include is a string' do
+ let(:includes) { 'generated.yml' }
+
+ it { is_expected.to be_valid }
+ end
end
describe "Error handling" do
@@ -2457,13 +2465,13 @@ module Gitlab
context 'returns errors if variables is not a map' do
let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) }
- it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
+ it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
end
context 'returns errors if variables is not a map of key-value strings' do
let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) }
- it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
+ it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
end
context 'returns errors if job when is not on_success, on_failure or always' do
diff --git a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
index efdfc0a980b..6b568320953 100644
--- a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
@@ -42,12 +42,24 @@ RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
expect(null_logger).to receive(:info).with("Looking for orphan LFS files for project #{project.name_with_namespace}")
expect(null_logger).to receive(:info).with("Removed invalid references: 1")
expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:lfs_objects_size])
+ expect(service).to receive(:remove_orphan_references).and_call_original
expect { service.run! }.to change { project.lfs_objects.count }.from(2).to(1)
expect(LfsObjectsProject.exists?(invalid_reference.id)).to be_falsey
end
+ it 'does nothing if the project has no LFS objects' do
+ expect(null_logger).to receive(:info).with(/Looking for orphan LFS files/)
+ expect(null_logger).to receive(:info).with(/Nothing to do/)
+
+ project.lfs_objects_projects.delete_all
+
+ expect(service).not_to receive(:remove_orphan_references)
+
+ service.run!
+ end
+
context 'LFS object is in design repository' do
before do
expect(project.design_repository).to receive(:exists?).and_return(true)
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index f2bc6390032..37349c30224 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -3,18 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::ClosingIssueExtractor do
- let(:project) { create(:project) }
- let(:project2) { create(:project) }
- let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute }
- let(:issue) { create(:issue, project: project) }
- let(:issue2) { create(:issue, project: project2) }
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
let(:cross_reference) { issue2.to_reference(project) }
- let(:fork_cross_reference) { issue.to_reference(forked_project) }
subject { described_class.new(project, project.creator) }
- before do
+ before_all do
project.add_developer(project.creator)
project.add_developer(project2.creator)
project2.add_maintainer(project.creator)
@@ -325,6 +323,9 @@ RSpec.describe Gitlab::ClosingIssueExtractor do
end
context "with a cross-project fork reference" do
+ let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute }
+ let(:fork_cross_reference) { issue.to_reference(forked_project) }
+
subject { described_class.new(forked_project, forked_project.creator) }
it do
@@ -348,8 +349,8 @@ RSpec.describe Gitlab::ClosingIssueExtractor do
end
context 'with multiple references' do
- let(:other_issue) { create(:issue, project: project) }
- let(:third_issue) { create(:issue, project: project) }
+ let_it_be(:other_issue) { create(:issue, project: project) }
+ let_it_be(:third_issue) { create(:issue, project: project) }
let(:reference2) { other_issue.to_reference }
let(:reference3) { third_issue.to_reference }
diff --git a/spec/lib/gitlab/code_navigation_path_spec.rb b/spec/lib/gitlab/code_navigation_path_spec.rb
index 4dc864b158d..206541f7c0d 100644
--- a/spec/lib/gitlab/code_navigation_path_spec.rb
+++ b/spec/lib/gitlab/code_navigation_path_spec.rb
@@ -16,10 +16,6 @@ RSpec.describe Gitlab::CodeNavigationPath do
subject { described_class.new(project, commit_sha).full_json_path_for(path) }
- before do
- stub_feature_flags(code_navigation: project)
- end
-
context 'when a pipeline exist for a sha' do
it 'returns path to a file in the artifact' do
expect(subject).to eq(lsif_path)
@@ -41,15 +37,5 @@ RSpec.describe Gitlab::CodeNavigationPath do
expect(subject).to eq(lsif_path)
end
end
-
- context 'when code_navigation feature is disabled' do
- before do
- stub_feature_flags(code_navigation: false)
- end
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
end
end
diff --git a/spec/lib/gitlab/config/entry/composable_array_spec.rb b/spec/lib/gitlab/config/entry/composable_array_spec.rb
new file mode 100644
index 00000000000..77766cb3b0a
--- /dev/null
+++ b/spec/lib/gitlab/config/entry/composable_array_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Entry::ComposableArray, :aggregate_failures do
+ let(:valid_config) do
+ [
+ {
+ DATABASE_SECRET: 'passw0rd'
+ },
+ {
+ API_TOKEN: 'passw0rd2'
+ }
+ ]
+ end
+
+ let(:config) { valid_config }
+ let(:entry) { described_class.new(config) }
+
+ before do
+ allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node)
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+
+ context 'is invalid' do
+ let(:config) { { hello: :world } }
+
+ it { expect(entry).not_to be_valid }
+ end
+ end
+
+ describe '#compose!' do
+ before do
+ entry.compose!
+ end
+
+ it 'composes child entry with configured value' do
+ expect(entry.value).to eq(config)
+ end
+
+ it 'composes child entries with configured values' do
+ expect(entry[0]).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry[0].description).to eq('node definition')
+ expect(entry[0].key).to eq('node')
+ expect(entry[0].metadata).to eq({})
+ expect(entry[0].parent.class).to eq(Gitlab::Config::Entry::ComposableArray)
+ expect(entry[0].value).to eq(DATABASE_SECRET: 'passw0rd')
+ expect(entry[1]).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry[1].description).to eq('node definition')
+ expect(entry[1].key).to eq('node')
+ expect(entry[1].metadata).to eq({})
+ expect(entry[1].parent.class).to eq(Gitlab::Config::Entry::ComposableArray)
+ expect(entry[1].value).to eq(API_TOKEN: 'passw0rd2')
+ end
+
+ describe '#descendants' do
+ it 'creates descendant nodes' do
+ expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry.descendants.first.value).to eq(DATABASE_SECRET: 'passw0rd')
+ expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry.descendants.second.value).to eq(API_TOKEN: 'passw0rd2')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
new file mode 100644
index 00000000000..15bbf2047c5
--- /dev/null
+++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
+ let(:valid_config) do
+ {
+ DATABASE_SECRET: 'passw0rd',
+ API_TOKEN: 'passw0rd2'
+ }
+ end
+
+ let(:config) { valid_config }
+
+ shared_examples 'composes a hash' do
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+
+ context 'is invalid' do
+ let(:config) { %w[one two] }
+
+ it { expect(entry).not_to be_valid }
+ end
+ end
+
+ describe '#value' do
+ context 'when config is a hash' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+
+ describe '#compose!' do
+ before do
+ entry.compose!
+ end
+
+ it 'composes child entry with configured value' do
+ expect(entry.value).to eq(config)
+ end
+
+ it 'composes child entries with configured values' do
+ expect(entry[:DATABASE_SECRET]).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry[:DATABASE_SECRET].description).to eq('DATABASE_SECRET node definition')
+ expect(entry[:DATABASE_SECRET].key).to eq(:DATABASE_SECRET)
+ expect(entry[:DATABASE_SECRET].metadata).to eq(name: :DATABASE_SECRET)
+ expect(entry[:DATABASE_SECRET].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:DATABASE_SECRET].value).to eq('passw0rd')
+ expect(entry[:API_TOKEN]).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry[:API_TOKEN].description).to eq('API_TOKEN node definition')
+ expect(entry[:API_TOKEN].key).to eq(:API_TOKEN)
+ expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN)
+ expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:API_TOKEN].value).to eq('passw0rd2')
+ end
+
+ describe '#descendants' do
+ it 'creates descendant nodes' do
+ expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry.descendants.first.value).to eq('passw0rd')
+ expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node)
+ expect(entry.descendants.second.value).to eq('passw0rd2')
+ end
+ end
+ end
+ end
+
+ context 'when ComposableHash is instantiated' do
+ let(:entry) { described_class.new(config) }
+
+ before do
+ allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node)
+ end
+
+ it_behaves_like 'composes a hash'
+ end
+
+ context 'when ComposableHash entry is configured in the parent class' do
+ let(:composable_hash_parent_class) do
+ Class.new(Gitlab::Config::Entry::Node) do
+ include ::Gitlab::Config::Entry::Configurable
+
+ entry :secrets, ::Gitlab::Config::Entry::ComposableHash,
+ description: 'Configured secrets for this job',
+ inherit: false,
+ default: { hello: :world },
+ metadata: { composable_class: Gitlab::Config::Entry::Node }
+ end
+ end
+
+ let(:entry) do
+ parent_entry = composable_hash_parent_class.new(secrets: config)
+ parent_entry.compose!
+
+ parent_entry[:secrets]
+ end
+
+ it_behaves_like 'composes a hash'
+
+ it 'creates entry with configuration from parent class' do
+ expect(entry.default).to eq({ hello: :world })
+ expect(entry.metadata).to eq(composable_class: Gitlab::Config::Entry::Node)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index b54fe40bb5f..80bd517ec92 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -262,7 +262,7 @@ RSpec.describe Gitlab::Conflict::File do
end
it 'includes the blob icon for the file' do
- expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
+ expect(conflict_file.as_json[:blob_icon]).to eq('doc-text')
end
context 'with the full_content option passed' do
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index e0a8e2c17a3..a31f34d82d7 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -2,16 +2,20 @@
require 'spec_helper'
-RSpec.describe 'cycle analytics events' do
- let(:project) { create(:project, :repository) }
+RSpec.describe 'cycle analytics events', :aggregate_failures do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, :admin) }
let(:from_date) { 10.days.ago }
- let(:user) { create(:user, :admin) }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
let(:events) do
- CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user })[stage].events
+ CycleAnalytics::ProjectLevel
+ .new(project, options: { from: from_date, current_user: user })[stage]
+ .events
end
+ let(:event) { events.first }
+
before do
setup(context)
end
@@ -19,36 +23,15 @@ RSpec.describe 'cycle analytics events' do
describe '#issue_events' do
let(:stage) { :issue }
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
- end
-
- it 'has a title' do
- expect(events.first[:title]).to eq(context.title)
- end
-
- it 'has the URL' do
- expect(events.first[:url]).not_to be_nil
- end
-
- it 'has an iid' do
- expect(events.first[:iid]).to eq(context.iid.to_s)
- end
-
- it 'has a created_at timestamp' do
- expect(events.first[:created_at]).to end_with('ago')
- end
-
- it "has the author's URL" do
- expect(events.first[:author][:web_url]).not_to be_nil
- end
-
- it "has the author's avatar URL" do
- expect(events.first[:author][:avatar_url]).not_to be_nil
- end
-
- it "has the author's name" do
- expect(events.first[:author][:name]).to eq(context.author.name)
+ it 'has correct attributes' do
+ expect(event[:total_time]).not_to be_empty
+ expect(event[:title]).to eq(context.title)
+ expect(event[:url]).not_to be_nil
+ expect(event[:iid]).to eq(context.iid.to_s)
+ expect(event[:created_at]).to end_with('ago')
+ expect(event[:author][:web_url]).not_to be_nil
+ expect(event[:author][:avatar_url]).not_to be_nil
+ expect(event[:author][:name]).to eq(context.author.name)
end
end
@@ -59,36 +42,15 @@ RSpec.describe 'cycle analytics events' do
create_commit_referencing_issue(context)
end
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
- end
-
- it 'has a title' do
- expect(events.first[:title]).to eq(context.title)
- end
-
- it 'has the URL' do
- expect(events.first[:url]).not_to be_nil
- end
-
- it 'has an iid' do
- expect(events.first[:iid]).to eq(context.iid.to_s)
- end
-
- it 'has a created_at timestamp' do
- expect(events.first[:created_at]).to end_with('ago')
- end
-
- it "has the author's URL" do
- expect(events.first[:author][:web_url]).not_to be_nil
- end
-
- it "has the author's avatar URL" do
- expect(events.first[:author][:avatar_url]).not_to be_nil
- end
-
- it "has the author's name" do
- expect(events.first[:author][:name]).to eq(context.author.name)
+ it 'has correct attributes' do
+ expect(event[:total_time]).not_to be_empty
+ expect(event[:title]).to eq(context.title)
+ expect(event[:url]).not_to be_nil
+ expect(event[:iid]).to eq(context.iid.to_s)
+ expect(event[:created_at]).to end_with('ago')
+ expect(event[:author][:web_url]).not_to be_nil
+ expect(event[:author][:avatar_url]).not_to be_nil
+ expect(event[:author][:name]).to eq(context.author.name)
end
end
@@ -100,32 +62,14 @@ RSpec.describe 'cycle analytics events' do
create_commit_referencing_issue(context)
end
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
- end
-
- it 'has a title' do
- expect(events.first[:title]).to eq('Awesome merge_request')
- end
-
- it 'has an iid' do
- expect(events.first[:iid]).to eq(context.iid.to_s)
- end
-
- it 'has a created_at timestamp' do
- expect(events.first[:created_at]).to end_with('ago')
- end
-
- it "has the author's URL" do
- expect(events.first[:author][:web_url]).not_to be_nil
- end
-
- it "has the author's avatar URL" do
- expect(events.first[:author][:avatar_url]).not_to be_nil
- end
-
- it "has the author's name" do
- expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ it 'has correct attributes' do
+ expect(event[:total_time]).not_to be_empty
+ expect(event[:title]).to eq('Awesome merge_request')
+ expect(event[:iid]).to eq(context.iid.to_s)
+ expect(event[:created_at]).to end_with('ago')
+ expect(event[:author][:web_url]).not_to be_nil
+ expect(event[:author][:avatar_url]).not_to be_nil
+ expect(event[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
@@ -152,40 +96,16 @@ RSpec.describe 'cycle analytics events' do
merge_merge_requests_closing_issue(user, project, context)
end
- it 'has the name' do
- expect(events.first[:name]).not_to be_nil
- end
-
- it 'has the ID' do
- expect(events.first[:id]).not_to be_nil
- end
-
- it 'has the URL' do
- expect(events.first[:url]).not_to be_nil
- end
-
- it 'has the branch name' do
- expect(events.first[:branch]).not_to be_nil
- end
-
- it 'has the branch URL' do
- expect(events.first[:branch][:url]).not_to be_nil
- end
-
- it 'has the short SHA' do
- expect(events.first[:short_sha]).not_to be_nil
- end
-
- it 'has the commit URL' do
- expect(events.first[:commit_url]).not_to be_nil
- end
-
- it 'has the date' do
- expect(events.first[:date]).not_to be_nil
- end
-
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
+ it 'has correct attributes' do
+ expect(event[:name]).not_to be_nil
+ expect(event[:id]).not_to be_nil
+ expect(event[:url]).not_to be_nil
+ expect(event[:branch]).not_to be_nil
+ expect(event[:branch][:url]).not_to be_nil
+ expect(event[:short_sha]).not_to be_nil
+ expect(event[:commit_url]).not_to be_nil
+ expect(event[:date]).not_to be_nil
+ expect(event[:total_time]).not_to be_empty
end
end
@@ -197,40 +117,16 @@ RSpec.describe 'cycle analytics events' do
merge_merge_requests_closing_issue(user, project, context)
end
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
- end
-
- it 'has a title' do
- expect(events.first[:title]).to eq('Awesome merge_request')
- end
-
- it 'has an iid' do
- expect(events.first[:iid]).to eq(context.iid.to_s)
- end
-
- it 'has the URL' do
- expect(events.first[:url]).not_to be_nil
- end
-
- it 'has a state' do
- expect(events.first[:state]).not_to be_nil
- end
-
- it 'has a created_at timestamp' do
- expect(events.first[:created_at]).not_to be_nil
- end
-
- it "has the author's URL" do
- expect(events.first[:author][:web_url]).not_to be_nil
- end
-
- it "has the author's avatar URL" do
- expect(events.first[:author][:avatar_url]).not_to be_nil
- end
-
- it "has the author's name" do
- expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ it 'has correct attributes' do
+ expect(event[:total_time]).not_to be_empty
+ expect(event[:title]).to eq('Awesome merge_request')
+ expect(event[:iid]).to eq(context.iid.to_s)
+ expect(event[:url]).not_to be_nil
+ expect(event[:state]).not_to be_nil
+ expect(event[:created_at]).not_to be_nil
+ expect(event[:author][:web_url]).not_to be_nil
+ expect(event[:author][:avatar_url]).not_to be_nil
+ expect(event[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
@@ -257,58 +153,25 @@ RSpec.describe 'cycle analytics events' do
deploy_master(user, project)
end
- it 'has the name' do
- expect(events.first[:name]).not_to be_nil
- end
-
- it 'has the ID' do
- expect(events.first[:id]).not_to be_nil
- end
-
- it 'has the URL' do
- expect(events.first[:url]).not_to be_nil
- end
-
- it 'has the branch name' do
- expect(events.first[:branch]).not_to be_nil
- end
-
- it 'has the branch URL' do
- expect(events.first[:branch][:url]).not_to be_nil
- end
-
- it 'has the short SHA' do
- expect(events.first[:short_sha]).not_to be_nil
- end
-
- it 'has the commit URL' do
- expect(events.first[:commit_url]).not_to be_nil
- end
-
- it 'has the date' do
- expect(events.first[:date]).not_to be_nil
- end
-
- it 'has the total time' do
- expect(events.first[:total_time]).not_to be_empty
- end
-
- it "has the author's URL" do
- expect(events.first[:author][:web_url]).not_to be_nil
- end
-
- it "has the author's avatar URL" do
- expect(events.first[:author][:avatar_url]).not_to be_nil
- end
-
- it "has the author's name" do
- expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ it 'has correct attributes' do
+ expect(event[:name]).not_to be_nil
+ expect(event[:id]).not_to be_nil
+ expect(event[:url]).not_to be_nil
+ expect(event[:branch]).not_to be_nil
+ expect(event[:branch][:url]).not_to be_nil
+ expect(event[:short_sha]).not_to be_nil
+ expect(event[:commit_url]).not_to be_nil
+ expect(event[:date]).not_to be_nil
+ expect(event[:total_time]).not_to be_empty
+ expect(event[:author][:web_url]).not_to be_nil
+ expect(event[:author][:avatar_url]).not_to be_nil
+ expect(event[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
def setup(context)
milestone = create(:milestone, project: project)
- context.update(milestone: milestone)
+ context.update!(milestone: milestone)
mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb
index c31522c538d..882cede759b 100644
--- a/spec/lib/gitlab/danger/commit_linter_spec.rb
+++ b/spec/lib/gitlab/danger/commit_linter_spec.rb
@@ -323,6 +323,16 @@ RSpec.describe Gitlab::Danger::CommitLinter do
end
end
+ context 'when message includes a value that is surrounded by backticks' do
+ let(:commit_message) { "A commit message `%20`" }
+
+ it 'does not add a problem' do
+ expect(commit_linter).not_to receive(:add_problem)
+
+ commit_linter.lint
+ end
+ end
+
context 'when message includes a short reference' do
[
'A commit message to fix #1234',
@@ -336,7 +346,9 @@ RSpec.describe Gitlab::Danger::CommitLinter do
'A commit message to fix gitlab-org/gitlab#1234',
'A commit message to fix gitlab-org/gitlab!1234',
'A commit message to fix gitlab-org/gitlab&1234',
- 'A commit message to fix gitlab-org/gitlab%1234'
+ 'A commit message to fix gitlab-org/gitlab%1234',
+ 'A commit message to fix "gitlab-org/gitlab%1234"',
+ 'A commit message to fix `gitlab-org/gitlab%1234'
].each do |message|
let(:commit_message) { message }
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index c7d55c396ef..509649f08c6 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -284,7 +284,8 @@ RSpec.describe Gitlab::Danger::Helper do
'.codeclimate.yml' | [:engineering_productivity]
'.gitlab/CODEOWNERS' | [:engineering_productivity]
- 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:backend]
+ 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template]
+ 'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template]
'ee/FOO_VERSION' | [:unknown]
@@ -376,6 +377,7 @@ RSpec.describe Gitlab::Danger::Helper do
:none | ''
:qa | '~QA'
:engineering_productivity | '~"Engineering Productivity" for CI, Danger'
+ :ci_template | '~"ci::templates"'
end
with_them do
@@ -435,6 +437,28 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
+ describe '#draft_mr?' do
+ it 'returns false when `gitlab_helper` is unavailable' do
+ expect(helper).to receive(:gitlab_helper).and_return(nil)
+
+ expect(helper).not_to be_draft_mr
+ end
+
+ it 'returns true for a draft MR' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'Draft: My MR title')
+
+ expect(helper).to be_draft_mr
+ end
+
+ it 'returns false for non draft MR' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'My MR title')
+
+ expect(helper).not_to be_draft_mr
+ end
+ end
+
describe '#cherry_pick_mr?' do
it 'returns false when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index b471e17e2e7..1a900dfba22 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -4,10 +4,13 @@ require 'webmock/rspec'
require 'timecop'
require 'gitlab/danger/roulette'
+require 'active_support/testing/time_helpers'
RSpec.describe Gitlab::Danger::Roulette do
+ include ActiveSupport::Testing::TimeHelpers
+
around do |example|
- Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run }
+ travel_to(Time.utc(2020, 06, 22, 10)) { example.run }
end
let(:backend_available) { true }
@@ -67,14 +70,30 @@ RSpec.describe Gitlab::Danger::Roulette do
)
end
- let(:teammate_json) do
+ let(:ci_template_reviewer) do
+ Gitlab::Danger::Teammate.new(
+ 'username' => 'ci-template-maintainer',
+ 'name' => 'CI Template engineer',
+ 'role' => '~"ci::templates"',
+ 'projects' => { 'gitlab' => 'reviewer ci_template' },
+ 'available' => true,
+ 'tz_offset_hours' => 2.0
+ )
+ end
+
+ let(:teammates) do
[
backend_maintainer.to_h,
frontend_maintainer.to_h,
frontend_reviewer.to_h,
software_engineer_in_test.to_h,
- engineering_productivity_reviewer.to_h
- ].to_json
+ engineering_productivity_reviewer.to_h,
+ ci_template_reviewer.to_h
+ ]
+ end
+
+ let(:teammate_json) do
+ teammates.to_json
end
subject(:roulette) { Object.new.extend(described_class) }
@@ -162,6 +181,14 @@ RSpec.describe Gitlab::Danger::Roulette do
end
end
+ context 'when change contains CI/CD Template category' do
+ let(:categories) { [:ci_template] }
+
+ it 'assigns CI/CD Template reviewer and fallback to backend maintainer' do
+ expect(spins).to eq([described_class::Spin.new(:ci_template, ci_template_reviewer, backend_maintainer, false, false)])
+ end
+ end
+
context 'when change contains test category' do
let(:categories) { [:test] }
@@ -210,6 +237,69 @@ RSpec.describe Gitlab::Danger::Roulette do
end
end
end
+
+ describe 'reviewer suggestion probability' do
+ let(:reviewer) { teammate_with_capability('reviewer', 'reviewer backend') }
+ let(:hungry_reviewer) { teammate_with_capability('hungry_reviewer', 'reviewer backend', hungry: true) }
+ let(:traintainer) { teammate_with_capability('traintainer', 'trainee_maintainer backend') }
+ let(:hungry_traintainer) { teammate_with_capability('hungry_traintainer', 'trainee_maintainer backend', hungry: true) }
+ let(:teammates) do
+ [
+ reviewer.to_h,
+ hungry_reviewer.to_h,
+ traintainer.to_h,
+ hungry_traintainer.to_h
+ ]
+ end
+
+ let(:categories) { [:backend] }
+
+ # This test is testing probability with inherent randomness.
+ # The variance is inversely related to sample size
+ # Given large enough sample size, the variance would be smaller,
+ # but the test would take longer.
+ # Given smaller sample size, the variance would be larger,
+ # but the test would take less time.
+ let!(:sample_size) { 500 }
+ let!(:variance) { 0.1 }
+
+ before do
+ # This test needs actual randomness to simulate probabilities
+ allow(subject).to receive(:new_random).and_return(Random.new)
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: teammate_json)
+ end
+
+ it 'has 1:2:3:4 probability of picking reviewer, hungry_reviewer, traintainer, hungry_traintainer' do
+ picks = Array.new(sample_size).map do
+ spins = subject.spin(project, categories, timezone_experiment: timezone_experiment)
+ spins.first.reviewer.name
+ end
+
+ expect(probability(picks, 'reviewer')).to be_within(variance).of(0.1)
+ expect(probability(picks, 'hungry_reviewer')).to be_within(variance).of(0.2)
+ expect(probability(picks, 'traintainer')).to be_within(variance).of(0.3)
+ expect(probability(picks, 'hungry_traintainer')).to be_within(variance).of(0.4)
+ end
+
+ def probability(picks, role)
+ picks.count(role).to_f / picks.length
+ end
+
+ def teammate_with_capability(name, capability, hungry: false)
+ Gitlab::Danger::Teammate.new(
+ {
+ 'name' => name,
+ 'projects' => {
+ 'gitlab' => capability
+ },
+ 'available' => true,
+ 'hungry' => hungry
+ }
+ )
+ end
+ end
end
RSpec::Matchers.define :match_teammates do |expected|
@@ -265,7 +355,8 @@ RSpec.describe Gitlab::Danger::Roulette do
frontend_reviewer,
frontend_maintainer,
software_engineer_in_test,
- engineering_productivity_reviewer
+ engineering_productivity_reviewer,
+ ci_template_reviewer
])
end
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index 6fd32493d6b..eebe14ed5e1 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -4,6 +4,7 @@ require 'timecop'
require 'rspec-parameterized'
require 'gitlab/danger/teammate'
+require 'active_support/testing/time_helpers'
RSpec.describe Gitlab::Danger::Teammate do
using RSpec::Parameterized::TableSyntax
@@ -148,8 +149,10 @@ RSpec.describe Gitlab::Danger::Teammate do
end
describe '#local_hour' do
+ include ActiveSupport::Testing::TimeHelpers
+
around do |example|
- Timecop.freeze(Time.utc(2020, 6, 23, 10)) { example.run }
+ travel_to(Time.utc(2020, 6, 23, 10)) { example.run }
end
context 'when author is given' do
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index 155e66e2fcd..8fb7ab25b17 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
deployable = deployment.deployable
expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
- expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user)
+ expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.deployed_by)
expected_commit_url = Gitlab::UrlBuilder.build(commit)
data = described_class.build(deployment)
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:environment]).to eq("somewhere")
expect(data[:project]).to eq(project.hook_attrs)
expect(data[:short_sha]).to eq(deployment.short_sha)
- expect(data[:user]).to eq(deployment.user.hook_attrs)
+ expect(data[:user]).to eq(deployment.deployed_by.hook_attrs)
expect(data[:user_url]).to eq(expected_user_url)
expect(data[:commit_url]).to eq(expected_commit_url)
expect(data[:commit_title]).to eq(commit.title)
diff --git a/spec/lib/gitlab/database/background_migration_job_spec.rb b/spec/lib/gitlab/database/background_migration_job_spec.rb
index dd5bf8b512f..42695925a1c 100644
--- a/spec/lib/gitlab/database/background_migration_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration_job_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::BackgroundMigrationJob do
let!(:job1) { create(:background_migration_job, :succeeded, created_at: initial_time, updated_at: initial_time) }
it 'does not update non-pending jobs' do
- Timecop.freeze(initial_time + 1.day) do
+ travel_to(initial_time + 1.day) do
expect { described_class.mark_all_as_succeeded('TestJob', [1, 100]) }
.to change { described_class.succeeded.count }.from(1).to(2)
end
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index 71d3666602f..31a8b4afa03 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::BatchCount do
let_it_be(:fallback) { ::Gitlab::Database::BatchCounter::FALLBACK }
- let_it_be(:small_batch_size) { ::Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE - 1 }
+ let_it_be(:small_batch_size) { calculate_batch_size(::Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE) }
let(:model) { Issue }
let(:column) { :author_id }
@@ -22,6 +22,12 @@ RSpec.describe Gitlab::Database::BatchCount do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction)
end
+ def calculate_batch_size(batch_size)
+ zero_offset_modifier = -1
+
+ batch_size + zero_offset_modifier
+ end
+
shared_examples 'disallowed configurations' do |method|
it 'returns fallback if start is bigger than finish' do
expect(described_class.public_send(method, *args, start: 1, finish: 0)).to eq(fallback)
@@ -45,6 +51,46 @@ RSpec.describe Gitlab::Database::BatchCount do
end
end
+ shared_examples 'when batch fetch query is canceled' do
+ let(:batch_size) { 22_000 }
+ let(:relation) { instance_double(ActiveRecord::Relation) }
+
+ it 'reduces batch size by half and retry fetch' do
+ too_big_batch_relation_mock = instance_double(ActiveRecord::Relation)
+ allow(model).to receive_message_chain(:select, public_send: relation)
+ allow(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size)).and_return(too_big_batch_relation_mock)
+ allow(too_big_batch_relation_mock).to receive(:send).and_raise(ActiveRecord::QueryCanceled)
+
+ expect(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size / 2)).and_return(double(send: 1))
+
+ subject.call(model, column, batch_size: batch_size, start: 0)
+ end
+
+ context 'when all retries fail' do
+ let(:batch_count_query) { 'SELECT COUNT(id) FROM relation WHERE id BETWEEN 0 and 1' }
+
+ before do
+ allow(model).to receive_message_chain(:select, :public_send, where: relation)
+ allow(relation).to receive(:send).and_raise(ActiveRecord::QueryCanceled.new('query timed out'))
+ allow(relation).to receive(:to_sql).and_return(batch_count_query)
+ end
+
+ it 'logs failing query' do
+ expect(Gitlab::AppJsonLogger).to receive(:error).with(
+ event: 'batch_count',
+ relation: model.table_name,
+ operation: operation,
+ operation_args: operation_args,
+ start: 0,
+ mode: mode,
+ query: batch_count_query,
+ message: 'Query has been canceled with message: query timed out'
+ )
+ expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(-1)
+ end
+ end
+ end
+
describe '#batch_count' do
it 'counts table' do
expect(described_class.batch_count(model)).to eq(5)
@@ -86,10 +132,11 @@ RSpec.describe Gitlab::Database::BatchCount do
it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE}" do
min_id = model.minimum(:id)
+ relation = instance_double(ActiveRecord::Relation)
+ allow(model).to receive_message_chain(:select, public_send: relation)
+ batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE)
- expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter|
- expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE + min_id, :itself).once.and_call_original
- end
+ expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
described_class.batch_count(model)
end
@@ -98,6 +145,15 @@ RSpec.describe Gitlab::Database::BatchCount do
subject { described_class.batch_count(model) }
end
+ it_behaves_like 'when batch fetch query is canceled' do
+ let(:mode) { :itself }
+ let(:operation) { :count }
+ let(:operation_args) { nil }
+ let(:column) { nil }
+
+ subject { described_class.method(:batch_count) }
+ end
+
context 'disallowed_configurations' do
include_examples 'disallowed configurations', :batch_count do
let(:args) { [Issue] }
@@ -108,6 +164,24 @@ RSpec.describe Gitlab::Database::BatchCount do
expect { described_class.batch_count(model.distinct(column)) }.to raise_error 'Use distinct count for optimized distinct counting'
end
end
+
+ context 'when a relation is grouped' do
+ let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) }
+
+ before do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1)
+ end
+
+ context 'count by default column' do
+ let(:count) do
+ described_class.batch_count(model.group(column), batch_size: 2)
+ end
+
+ it 'counts grouped records' do
+ expect(count).to eq({ user.id => 4, another_user.id => 2 })
+ end
+ end
+ end
end
describe '#batch_distinct_count' do
@@ -151,10 +225,11 @@ RSpec.describe Gitlab::Database::BatchCount do
it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE}" do
min_id = model.minimum(:id)
+ relation = instance_double(ActiveRecord::Relation)
+ allow(model).to receive_message_chain(:select, public_send: relation)
+ batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE)
- expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter|
- expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE + min_id, :distinct).once.and_call_original
- end
+ expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
described_class.batch_distinct_count(model)
end
@@ -175,6 +250,33 @@ RSpec.describe Gitlab::Database::BatchCount do
end.to raise_error 'Use distinct count only with non id fields'
end
end
+
+ context 'when a relation is grouped' do
+ let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) }
+
+ before do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1)
+ end
+
+ context 'distinct count by non-unique column' do
+ let(:count) do
+ described_class.batch_distinct_count(model.group(column), :project_id, batch_size: 2)
+ end
+
+ it 'counts grouped records' do
+ expect(count).to eq({ user.id => 3, another_user.id => 2 })
+ end
+ end
+ end
+
+ it_behaves_like 'when batch fetch query is canceled' do
+ let(:mode) { :distinct }
+ let(:operation) { :count }
+ let(:operation_args) { nil }
+ let(:column) { nil }
+
+ subject { described_class.method(:batch_distinct_count) }
+ end
end
describe '#batch_sum' do
@@ -209,10 +311,11 @@ RSpec.describe Gitlab::Database::BatchCount do
it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE}" do
min_id = model.minimum(:id)
+ relation = instance_double(ActiveRecord::Relation)
+ allow(model).to receive_message_chain(:select, public_send: relation)
+ batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE)
- expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter|
- expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE + min_id, :itself).once.and_call_original
- end
+ expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
described_class.batch_sum(model, column)
end
@@ -226,5 +329,13 @@ RSpec.describe Gitlab::Database::BatchCount do
let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE }
let(:small_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE - 1 }
end
+
+ it_behaves_like 'when batch fetch query is canceled' do
+ let(:mode) { :itself }
+ let(:operation) { :sum }
+ let(:operation_args) { [column] }
+
+ subject { described_class.method(:batch_sum) }
+ end
end
end
diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb
new file mode 100644
index 00000000000..f2a7d6e69d8
--- /dev/null
+++ b/spec/lib/gitlab/database/bulk_update_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BulkUpdate do
+ describe 'error states' do
+ let(:columns) { %i[title] }
+
+ let_it_be(:mapping) do
+ create_default(:user)
+ create_default(:project)
+
+ i_a, i_b = create_list(:issue, 2)
+
+ {
+ i_a => { title: 'Issue a' },
+ i_b => { title: 'Issue b' }
+ }
+ end
+
+ it 'does not raise errors on valid inputs' do
+ expect { described_class.execute(columns, mapping) }.not_to raise_error
+ end
+
+ it 'expects a non-empty list of column names' do
+ expect { described_class.execute([], mapping) }.to raise_error(ArgumentError)
+ end
+
+ it 'expects all columns to be symbols' do
+ expect { described_class.execute([1], mapping) }.to raise_error(ArgumentError)
+ end
+
+ it 'expects all columns to be valid columns on the tables' do
+ expect { described_class.execute([:foo], mapping) }.to raise_error(ArgumentError)
+ end
+
+ it 'refuses to set ID' do
+ expect { described_class.execute([:id], mapping) }.to raise_error(ArgumentError)
+ end
+
+ it 'expects a non-empty mapping' do
+ expect { described_class.execute(columns, []) }.to raise_error(ArgumentError)
+ end
+
+ it 'expects all map values to be Hash instances' do
+ bad_map = mapping.merge(build(:issue) => 2)
+
+ expect { described_class.execute(columns, bad_map) }.to raise_error(ArgumentError)
+ end
+ end
+
+ it 'is possible to update all objects in a single query' do
+ users = create_list(:user, 3)
+ mapping = users.zip(%w(foo bar baz)).to_h do |u, name|
+ [u, { username: name, admin: true }]
+ end
+
+ expect do
+ described_class.execute(%i[username admin], mapping)
+ end.not_to exceed_query_limit(1)
+
+ # We have optimistically updated the values
+ expect(users).to all(be_admin)
+ expect(users.map(&:username)).to eq(%w(foo bar baz))
+
+ users.each(&:reset)
+
+ # The values are correct on reset
+ expect(users).to all(be_admin)
+ expect(users.map(&:username)).to eq(%w(foo bar baz))
+ end
+
+ it 'is possible to update heterogeneous sets' do
+ create_default(:user)
+ create_default(:project)
+
+ mr_a = create(:merge_request)
+ i_a, i_b = create_list(:issue, 2)
+
+ mapping = {
+ mr_a => { title: 'MR a' },
+ i_a => { title: 'Issue a' },
+ i_b => { title: 'Issue b' }
+ }
+
+ expect do
+ described_class.execute(%i[title], mapping)
+ end.not_to exceed_query_limit(2)
+
+ expect([mr_a, i_a, i_b].map { |x| x.reset.title })
+ .to eq(['MR a', 'Issue a', 'Issue b'])
+ end
+
+ shared_examples 'basic functionality' do
+ it 'sets multiple values' do
+ create_default(:user)
+ create_default(:project)
+
+ i_a, i_b = create_list(:issue, 2)
+
+ mapping = {
+ i_a => { title: 'Issue a' },
+ i_b => { title: 'Issue b' }
+ }
+
+ described_class.execute(%i[title], mapping)
+
+ expect([i_a, i_b].map { |x| x.reset.title })
+ .to eq(['Issue a', 'Issue b'])
+ end
+ end
+
+ include_examples 'basic functionality'
+
+ context 'when prepared statements are configured differently to the normal test environment' do
+ # rubocop: disable RSpec/LeakyConstantDeclaration
+ # This cop is disabled because you cannot call establish_connection on
+ # an anonymous class.
+ class ActiveRecordBasePreparedStatementsInverted < ActiveRecord::Base
+ def self.abstract_class?
+ true # So it gets its own connection
+ end
+ end
+ # rubocop: enable RSpec/LeakyConstantDeclaration
+
+ before_all do
+ c = ActiveRecord::Base.connection.instance_variable_get(:@config)
+ inverted = c.merge(prepared_statements: !ActiveRecord::Base.connection.prepared_statements)
+ ActiveRecordBasePreparedStatementsInverted.establish_connection(inverted)
+ end
+
+ before do
+ allow(ActiveRecord::Base).to receive(:connection_specification_name)
+ .and_return(ActiveRecordBasePreparedStatementsInverted.connection_specification_name)
+ end
+
+ include_examples 'basic functionality'
+ end
+end
diff --git a/spec/lib/gitlab/database/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/concurrent_reindex_spec.rb
deleted file mode 100644
index 4e2c3f547d4..00000000000
--- a/spec/lib/gitlab/database/concurrent_reindex_spec.rb
+++ /dev/null
@@ -1,207 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
- subject { described_class.new(index_name, logger: logger) }
-
- let(:table_name) { '_test_reindex_table' }
- let(:column_name) { '_test_column' }
- let(:index_name) { '_test_reindex_index' }
- let(:logger) { double('logger', debug: nil, info: nil, error: nil ) }
- let(:connection) { ActiveRecord::Base.connection }
-
- before do
- connection.execute(<<~SQL)
- CREATE TABLE #{table_name} (
- id serial NOT NULL PRIMARY KEY,
- #{column_name} integer NOT NULL);
-
- CREATE INDEX #{index_name} ON #{table_name} (#{column_name});
- SQL
- end
-
- context 'when the index does not exist' do
- before do
- connection.execute(<<~SQL)
- DROP INDEX #{index_name}
- SQL
- end
-
- it 'raises an error' do
- expect { subject.execute }.to raise_error(described_class::ReindexError, /does not exist/)
- end
- end
-
- context 'when the index is unique' do
- before do
- connection.execute(<<~SQL)
- DROP INDEX #{index_name};
- CREATE UNIQUE INDEX #{index_name} ON #{table_name} (#{column_name})
- SQL
- end
-
- it 'raises an error' do
- expect do
- subject.execute
- end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/)
- end
- end
-
- context 'replacing the original index with a rebuilt copy' do
- let(:replacement_name) { 'tmp_reindex__test_reindex_index' }
- let(:replaced_name) { 'old_reindex__test_reindex_index' }
-
- let(:create_index) { "CREATE INDEX CONCURRENTLY #{replacement_name} ON public.#{table_name} USING btree (#{column_name})" }
- let(:drop_index) { "DROP INDEX CONCURRENTLY IF EXISTS #{replacement_name}" }
-
- let!(:original_index) { find_index_create_statement }
-
- before do
- allow(subject).to receive(:connection).and_return(connection)
- allow(subject).to receive(:disable_statement_timeout).and_yield
- end
-
- it 'replaces the existing index with an identical index' do
- expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
-
- expect_to_execute_concurrently_in_order(drop_index)
- expect_to_execute_concurrently_in_order(create_index)
-
- expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
- expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
- end
-
- expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
- expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
- expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- subject.execute
-
- check_index_exists
- end
-
- context 'when a dangling index is left from a previous run' do
- before do
- connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
- end
-
- it 'replaces the existing index with an identical index' do
- expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
-
- expect_to_execute_concurrently_in_order(drop_index)
- expect_to_execute_concurrently_in_order(create_index)
-
- expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
- expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
- end
-
- expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
- expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
- expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- subject.execute
-
- check_index_exists
- end
- end
-
- context 'when it fails to create the replacement index' do
- it 'safely cleans up and signals the error' do
- expect_to_execute_concurrently_in_order(drop_index)
-
- expect(connection).to receive(:execute).with(create_index).ordered
- .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/)
-
- check_index_exists
- end
- end
-
- context 'when the replacement index is not valid' do
- it 'safely cleans up and signals the error' do
- expect_to_execute_concurrently_in_order(drop_index)
- expect_to_execute_concurrently_in_order(create_index)
-
- expect(subject).to receive(:replacement_index_valid?).and_return(false)
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- expect { subject.execute }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
-
- check_index_exists
- end
- end
-
- context 'when a database error occurs while swapping the indexes' do
- it 'safely cleans up and signals the error' do
- expect_to_execute_concurrently_in_order(drop_index)
- expect_to_execute_concurrently_in_order(create_index)
-
- expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
- expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
- end
-
- expect(connection).to receive(:execute).ordered
- .with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
- .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/)
-
- check_index_exists
- end
- end
-
- context 'when with_lock_retries fails to acquire the lock' do
- it 'safely cleans up and signals the error' do
- expect_to_execute_concurrently_in_order(drop_index)
- expect_to_execute_concurrently_in_order(create_index)
-
- expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
- expect(instance).to receive(:run).with(raise_on_exhaustion: true)
- .and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
- end
-
- expect_to_execute_concurrently_in_order(drop_index)
-
- expect { subject.execute }.to raise_error(described_class::ReindexError, /exhausted/)
-
- check_index_exists
- end
- end
- end
-
- def expect_to_execute_concurrently_in_order(sql)
- # Indexes cannot be created CONCURRENTLY in a transaction. Since the tests are wrapped in transactions,
- # verify the original call but pass through the non-concurrent form.
- expect(connection).to receive(:execute).with(sql).ordered.and_wrap_original do |method, sql|
- method.call(sql.sub(/CONCURRENTLY/, ''))
- end
- end
-
- def expect_to_execute_in_order(sql)
- expect(connection).to receive(:execute).with(sql).ordered.and_call_original
- end
-
- def find_index_create_statement
- ActiveRecord::Base.connection.select_value(<<~SQL)
- SELECT indexdef
- FROM pg_indexes
- WHERE schemaname = 'public'
- AND indexname = #{ActiveRecord::Base.connection.quote(index_name)}
- SQL
- end
-
- def check_index_exists
- expect(find_index_create_statement).to eq(original_index)
- end
-end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 0bdcca630aa..a8edcc5f7e5 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -699,6 +699,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+ expect(model).to receive(:copy_check_constraints).with(:users, :old, :new)
model.rename_column_concurrently(:users, :old, :new)
end
@@ -761,6 +762,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:change_column_default)
.with(:users, :new, old_column.default)
+ expect(model).to receive(:copy_check_constraints)
+ .with(:users, :old, :new)
+
model.rename_column_concurrently(:users, :old, :new)
end
end
@@ -856,6 +860,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:copy_indexes).with(:users, :new, :old)
expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old)
+ expect(model).to receive(:copy_check_constraints).with(:users, :new, :old)
model.undo_cleanup_concurrent_column_rename(:users, :old, :new)
end
@@ -894,6 +899,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:change_column_default)
.with(:users, :old, new_column.default)
+ expect(model).to receive(:copy_check_constraints)
+ .with(:users, :new, :old)
+
model.undo_cleanup_concurrent_column_rename(:users, :old, :new)
end
end
@@ -925,6 +933,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#undo_change_column_type_concurrently' do
+ it 'reverses the operations of change_column_type_concurrently' do
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
+ expect(model).to receive(:remove_rename_triggers_for_postgresql)
+ .with(:users, /trigger_.{12}/)
+
+ expect(model).to receive(:remove_column).with(:users, "old_for_type_change")
+
+ model.undo_change_column_type_concurrently(:users, :old)
+ end
+ end
+
describe '#cleanup_concurrent_column_type_change' do
it 'cleans up the type changing procedure' do
expect(model).to receive(:cleanup_concurrent_column_rename)
@@ -937,6 +958,94 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#undo_cleanup_concurrent_column_type_change' do
+ context 'in a transaction' do
+ it 'raises RuntimeError' do
+ allow(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.undo_cleanup_concurrent_column_type_change(:users, :old, :new) }
+ .to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ let(:temp_column) { "old_for_type_change" }
+
+ let(:temp_undo_cleanup_column) do
+ identifier = "users_old_for_type_change"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+ "tmp_undo_cleanup_column_#{hashed_identifier}"
+ end
+
+ let(:trigger_name) { model.rename_trigger_name(:users, :old, :old_for_type_change) }
+
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'reverses the operations of cleanup_concurrent_column_type_change' do
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
+ expect(model).to receive(:create_column_from).with(
+ :users,
+ :old,
+ temp_undo_cleanup_column,
+ type: :string,
+ batch_column_name: :id,
+ type_cast_function: nil
+ ).and_return(true)
+
+ expect(model).to receive(:rename_column)
+ .with(:users, :old, temp_column)
+
+ expect(model).to receive(:rename_column)
+ .with(:users, temp_undo_cleanup_column, :old)
+
+ expect(model).to receive(:install_rename_triggers_for_postgresql)
+ .with(trigger_name, '"users"', '"old"', '"old_for_type_change"')
+
+ model.undo_cleanup_concurrent_column_type_change(:users, :old, :string)
+ end
+
+ it 'passes the type_cast_function and batch_column_name' do
+ expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true)
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
+ expect(model).to receive(:create_column_from).with(
+ :users,
+ :old,
+ temp_undo_cleanup_column,
+ type: :string,
+ batch_column_name: :other_batch_column,
+ type_cast_function: :custom_type_cast_function
+ ).and_return(true)
+
+ expect(model).to receive(:rename_column)
+ .with(:users, :old, temp_column)
+
+ expect(model).to receive(:rename_column)
+ .with(:users, temp_undo_cleanup_column, :old)
+
+ expect(model).to receive(:install_rename_triggers_for_postgresql)
+ .with(trigger_name, '"users"', '"old"', '"old_for_type_change"')
+
+ model.undo_cleanup_concurrent_column_type_change(
+ :users,
+ :old,
+ :string,
+ type_cast_function: :custom_type_cast_function,
+ batch_column_name: :other_batch_column
+ )
+ end
+
+ it 'raises an error with invalid batch_column_name' do
+ expect do
+ model.undo_cleanup_concurrent_column_type_change(:users, :old, :new, batch_column_name: :invalid)
+ end.to raise_error(RuntimeError, /Column invalid does not exist on users/)
+ end
+ end
+ end
+
describe '#install_rename_triggers_for_postgresql' do
it 'installs the triggers for PostgreSQL' do
expect(model).to receive(:execute)
@@ -1128,7 +1237,65 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
name: 'index_on_issues_gl_project_id',
length: [],
order: [],
- opclasses: { 'gl_project_id' => 'bar' })
+ opclass: { 'gl_project_id' => 'bar' })
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with multiple columns and custom operator classes' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id foobar),
+ name: 'index_on_issues_project_id_foobar',
+ using: :gin,
+ where: nil,
+ opclasses: { 'project_id' => 'bar', 'foobar' => :gin_trgm_ops },
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id')
+ .and_return([index])
+
+ expect(model).to receive(:add_concurrent_index)
+ .with(:issues,
+ %w(gl_project_id foobar),
+ unique: false,
+ name: 'index_on_issues_gl_project_id_foobar',
+ length: [],
+ order: [],
+ opclass: { 'gl_project_id' => 'bar', 'foobar' => :gin_trgm_ops },
+ using: :gin)
+
+ model.copy_indexes(:issues, :project_id, :gl_project_id)
+ end
+ end
+
+ context 'using an index with multiple columns and a custom operator class on the non affected column' do
+ it 'copies the index' do
+ index = double(:index,
+ columns: %w(project_id foobar),
+ name: 'index_on_issues_project_id_foobar',
+ using: :gin,
+ where: nil,
+ opclasses: { 'foobar' => :gin_trgm_ops },
+ unique: false,
+ lengths: [],
+ orders: [])
+
+ allow(model).to receive(:indexes_for).with(:issues, 'project_id')
+ .and_return([index])
+
+ expect(model).to receive(:add_concurrent_index)
+ .with(:issues,
+ %w(gl_project_id foobar),
+ unique: false,
+ name: 'index_on_issues_gl_project_id_foobar',
+ length: [],
+ order: [],
+ opclass: { 'foobar' => :gin_trgm_ops },
+ using: :gin)
model.copy_indexes(:issues, :project_id, :gl_project_id)
end
@@ -1400,15 +1567,32 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
)
end
- after do
- 'DROP INDEX IF EXISTS test_index;'
- end
-
it 'returns true if an index exists' do
expect(model.index_exists_by_name?(:projects, 'test_index'))
.to be_truthy
end
end
+
+ context 'when an index exists for a table with the same name in another schema' do
+ before do
+ ActiveRecord::Base.connection.execute(
+ 'CREATE SCHEMA new_test_schema'
+ )
+
+ ActiveRecord::Base.connection.execute(
+ 'CREATE TABLE new_test_schema.projects (id integer, name character varying)'
+ )
+
+ ActiveRecord::Base.connection.execute(
+ 'CREATE INDEX test_index_on_name ON new_test_schema.projects (LOWER(name));'
+ )
+ end
+
+ it 'returns false if the index does not exist in the current schema' do
+ expect(model.index_exists_by_name?(:projects, 'test_index_on_name'))
+ .to be_falsy
+ end
+ end
end
describe '#create_or_update_plan_limit' do
@@ -1863,11 +2047,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
ActiveRecord::Base.connection.execute(
'ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID'
)
- end
- after do
ActiveRecord::Base.connection.execute(
- 'ALTER TABLE projects DROP CONSTRAINT IF EXISTS check_1'
+ 'CREATE SCHEMA new_test_schema'
+ )
+
+ ActiveRecord::Base.connection.execute(
+ 'CREATE TABLE new_test_schema.projects (id integer, name character varying)'
+ )
+
+ ActiveRecord::Base.connection.execute(
+ 'ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5)'
)
end
@@ -1885,6 +2075,11 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model.check_constraint_exists?(:users, 'check_1'))
.to be_falsy
end
+
+ it 'returns false if a constraint with the same name exists for the same table in another schema' do
+ expect(model.check_constraint_exists?(:projects, 'check_2'))
+ .to be_falsy
+ end
end
describe '#add_check_constraint' do
@@ -2086,6 +2281,138 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#copy_check_constraints' do
+ context 'inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:column_exists?).and_return(true)
+ end
+
+ let(:old_column_constraints) do
+ [
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'check_d7d49d475d',
+ 'constraint_def' => 'CHECK ((old_column IS NOT NULL))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'check_48560e521e',
+ 'constraint_def' => 'CHECK ((char_length(old_column) <= 255))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'custom_check_constraint',
+ 'constraint_def' => 'CHECK (((old_column IS NOT NULL) AND (another_column IS NULL)))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'not_valid_check_constraint',
+ 'constraint_def' => 'CHECK ((old_column IS NOT NULL)) NOT VALID'
+ }
+ ]
+ end
+
+ it 'copies check constraints from one column to another' do
+ allow(model).to receive(:check_constraints_for)
+ .with(:test_table, :old_column, schema: nil)
+ .and_return(old_column_constraints)
+
+ allow(model).to receive(:not_null_constraint_name).with(:test_table, :new_column)
+ .and_return('check_1')
+
+ allow(model).to receive(:text_limit_name).with(:test_table, :new_column)
+ .and_return('check_2')
+
+ allow(model).to receive(:check_constraint_name)
+ .with(:test_table, :new_column, 'copy_check_constraint')
+ .and_return('check_3')
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(new_column IS NOT NULL)',
+ 'check_1',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(char_length(new_column) <= 255)',
+ 'check_2',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '((new_column IS NOT NULL) AND (another_column IS NULL))',
+ 'check_3',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(new_column IS NOT NULL)',
+ 'check_1',
+ validate: false
+ ).once
+
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end
+
+ it 'does nothing if there are no constraints defined for the old column' do
+ allow(model).to receive(:check_constraints_for)
+ .with(:test_table, :old_column, schema: nil)
+ .and_return([])
+
+ expect(model).not_to receive(:add_check_constraint)
+
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end
+
+ it 'raises an error when the orginating column does not exist' do
+ allow(model).to receive(:column_exists?).with(:test_table, :old_column).and_return(false)
+
+ error_message = /Column old_column does not exist on test_table/
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError, error_message)
+ end
+
+ it 'raises an error when the target column does not exist' do
+ allow(model).to receive(:column_exists?).with(:test_table, :new_column).and_return(false)
+
+ error_message = /Column new_column does not exist on test_table/
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError, error_message)
+ end
+ end
+ end
+
describe '#add_text_limit' do
context 'when it is called with the default options' do
it 'calls add_check_constraint with an infered constraint name and validate: true' do
diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
index 034bf966db7..8a35d8149ad 100644
--- a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
+++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Database::ObsoleteIgnoredColumns do
describe '#execute' do
it 'returns a list of class names and columns pairs' do
- Timecop.freeze(REMOVE_DATE) do
+ travel_to(REMOVE_DATE) do
expect(subject.execute).to eq([
['Testing::A', {
'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0'),
diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
index 334cac653cf..885eef5723e 100644
--- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
let(:partitioning_key) { :created_at }
around do |example|
- Timecop.freeze(Date.parse('2020-08-22')) { example.run }
+ travel_to(Date.parse('2020-08-22')) { example.run }
end
context 'with existing partitions' do
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
index ec3d0a6dbcb..c43b51e10a0 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
@@ -116,23 +116,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
expect(jobs_updated).to eq(1)
end
- context 'when the feature flag is disabled' do
- let(:mock_connection) { double('connection') }
-
- before do
- allow(subject).to receive(:connection).and_return(mock_connection)
- stub_feature_flags(backfill_partitioned_audit_events: false)
- end
-
- it 'exits without attempting to copy data' do
- expect(mock_connection).not_to receive(:execute)
-
- subject.perform(1, 100, source_table, destination_table, unique_key)
-
- expect(destination_model.count).to eq(0)
- end
- end
-
context 'when the job is run within an explicit transaction block' do
let(:mock_connection) { double('connection') }
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 44ef0b307fe..147637cf471 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -213,7 +213,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it 'creates partitions including the next month from today' do
today = Date.new(2020, 5, 8)
- Timecop.freeze(today) do
+ travel_to(today) do
migration.partition_table_by_date source_table, partition_column, min_date: min_date
expect_range_partitions_for(partitioned_table, {
@@ -233,7 +233,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
context 'without min_date, max_date' do
it 'creates partitions for the current and next month' do
current_date = Date.new(2020, 05, 22)
- Timecop.freeze(current_date.to_time) do
+ travel_to(current_date.to_time) do
migration.partition_table_by_date source_table, partition_column
expect_range_partitions_for(partitioned_table, {
@@ -514,6 +514,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true)
allow(migration).to receive(:copy_missed_records)
allow(migration).to receive(:execute).with(/VACUUM/)
+ allow(migration).to receive(:execute).with(/^(RE)?SET/)
end
it 'finishes remaining jobs for the correct table' do
@@ -567,6 +568,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
allow(Gitlab::BackgroundMigration).to receive(:steal)
allow(migration).to receive(:execute).with(/VACUUM/)
+ allow(migration).to receive(:execute).with(/^(RE)?SET/)
end
it 'idempotently cleans up after failed background migrations' do
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
new file mode 100644
index 00000000000..1da67a5a6c0
--- /dev/null
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresIndex do
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE INDEX foo_idx ON public.users (name);
+ CREATE UNIQUE INDEX bar_key ON public.users (id);
+
+ CREATE TABLE example_table (id serial primary key);
+ SQL
+ end
+
+ def find(name)
+ described_class.by_identifier(name)
+ end
+
+ describe '.by_identifier' do
+ it 'finds the index' do
+ expect(find('public.foo_idx')).to be_a(Gitlab::Database::PostgresIndex)
+ end
+
+ it 'raises an error if not found' do
+ expect { find('public.idontexist') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises ArgumentError if given a non-fully qualified index name' do
+ expect { find('foo') }.to raise_error(ArgumentError, /not fully qualified/)
+ end
+ end
+
+ describe '.regular' do
+ it 'only non-unique indexes' do
+ expect(described_class.regular).to all(have_attributes(unique: false))
+ end
+
+ it 'only non partitioned indexes ' do
+ expect(described_class.regular).to all(have_attributes(partitioned: false))
+ end
+
+ it 'only indexes that dont serve an exclusion constraint' do
+ expect(described_class.regular).to all(have_attributes(exclusion: false))
+ end
+ end
+
+ describe '.not_match' do
+ it 'excludes indexes matching the given regex' do
+ expect(described_class.not_match('^bar_k').map(&:name)).to all(match(/^(?!bar_k).*/))
+ end
+
+ it 'matches indexes without this prefix regex' do
+ expect(described_class.not_match('^bar_k')).not_to be_empty
+ end
+ end
+
+ describe '.random_few' do
+ it 'limits to two records by default' do
+ expect(described_class.random_few(2).size).to eq(2)
+ end
+ end
+
+ describe '#unique?' do
+ it 'returns true for a unique index' do
+ expect(find('public.bar_key')).to be_unique
+ end
+
+ it 'returns false for a regular, non-unique index' do
+ expect(find('public.foo_idx')).not_to be_unique
+ end
+
+ it 'returns true for a primary key index' do
+ expect(find('public.example_table_pkey')).to be_unique
+ end
+ end
+
+ describe '#valid_index?' do
+ it 'returns true if the index is invalid' do
+ expect(find('public.foo_idx')).to be_valid_index
+ end
+
+ it 'returns false if the index is marked as invalid' do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ UPDATE pg_index SET indisvalid=false
+ FROM pg_class
+ WHERE pg_class.relname = 'foo_idx' AND pg_index.indexrelid = pg_class.oid
+ SQL
+
+ expect(find('public.foo_idx')).not_to be_valid_index
+ end
+ end
+
+ describe '#to_s' do
+ it 'returns the index name' do
+ expect(find('public.foo_idx').to_s).to eq('foo_idx')
+ end
+ end
+
+ describe '#name' do
+ it 'returns the name' do
+ expect(find('public.foo_idx').name).to eq('foo_idx')
+ end
+ end
+
+ describe '#schema' do
+ it 'returns the index schema' do
+ expect(find('public.foo_idx').schema).to eq('public')
+ end
+ end
+
+ describe '#definition' do
+ it 'returns the index definition' do
+ expect(find('public.foo_idx').definition).to eq('CREATE INDEX foo_idx ON public.users USING btree (name)')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
new file mode 100644
index 00000000000..2d6765aac2e
--- /dev/null
+++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
+ subject { described_class.new(index, logger: logger) }
+
+ let(:table_name) { '_test_reindex_table' }
+ let(:column_name) { '_test_column' }
+ let(:index_name) { '_test_reindex_index' }
+ let(:index) { instance_double(Gitlab::Database::PostgresIndex, indexrelid: 42, name: index_name, schema: 'public', partitioned?: false, unique?: false, exclusion?: false, definition: 'CREATE INDEX _test_reindex_index ON public._test_reindex_table USING btree (_test_column)') }
+ let(:logger) { double('logger', debug: nil, info: nil, error: nil ) }
+ let(:connection) { ActiveRecord::Base.connection }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial NOT NULL PRIMARY KEY,
+ #{column_name} integer NOT NULL);
+
+ CREATE INDEX #{index.name} ON #{table_name} (#{column_name});
+ SQL
+ end
+
+ context 'when the index is unique' do
+ before do
+ allow(index).to receive(:unique?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect do
+ subject.perform
+ end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/)
+ end
+ end
+
+ context 'when the index is partitioned' do
+ before do
+ allow(index).to receive(:partitioned?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect do
+ subject.perform
+ end.to raise_error(described_class::ReindexError, /partitioned indexes are currently not supported/)
+ end
+ end
+
+ context 'when the index serves an exclusion constraint' do
+ before do
+ allow(index).to receive(:exclusion?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect do
+ subject.perform
+ end.to raise_error(described_class::ReindexError, /indexes serving an exclusion constraint are currently not supported/)
+ end
+ end
+
+ context 'when the index is a lingering temporary index from a previous reindexing run' do
+ context 'with the temporary index prefix' do
+ let(:index_name) { 'tmp_reindex_something' }
+
+ it 'raises an error' do
+ expect do
+ subject.perform
+ end.to raise_error(described_class::ReindexError, /left-over temporary index/)
+ end
+ end
+
+ context 'with the replaced index prefix' do
+ let(:index_name) { 'old_reindex_something' }
+
+ it 'raises an error' do
+ expect do
+ subject.perform
+ end.to raise_error(described_class::ReindexError, /left-over temporary index/)
+ end
+ end
+ end
+
+ context 'replacing the original index with a rebuilt copy' do
+ let(:replacement_name) { 'tmp_reindex_42' }
+ let(:replaced_name) { 'old_reindex_42' }
+
+ let(:create_index) { "CREATE INDEX CONCURRENTLY #{replacement_name} ON public.#{table_name} USING btree (#{column_name})" }
+ let(:drop_index) do
+ <<~SQL
+ DROP INDEX CONCURRENTLY
+ IF EXISTS "public"."#{replacement_name}"
+ SQL
+ end
+
+ let!(:original_index) { find_index_create_statement }
+
+ it 'integration test: executing full index replacement without mocks' do
+ allow(connection).to receive(:execute).and_wrap_original do |method, sql|
+ method.call(sql.sub(/CONCURRENTLY/, ''))
+ end
+
+ subject.perform
+
+ check_index_exists
+ end
+
+ context 'mocked specs' do
+ before do
+ allow(subject).to receive(:connection).and_return(connection)
+ allow(connection).to receive(:execute).and_call_original
+ end
+
+ it 'replaces the existing index with an identical index' do
+ expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice
+
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
+ end
+
+ expect_index_rename(index.name, replaced_name)
+ expect_index_rename(replacement_name, index.name)
+ expect_index_rename(replaced_name, replacement_name)
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ subject.perform
+
+ check_index_exists
+ end
+
+ context 'when a dangling index is left from a previous run' do
+ before do
+ connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
+ end
+
+ it 'replaces the existing index with an identical index' do
+ expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').exactly(3).times
+
+ expect_to_execute_concurrently_in_order(drop_index)
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
+ end
+
+ expect_index_rename(index.name, replaced_name)
+ expect_index_rename(replacement_name, index.name)
+ expect_index_rename(replaced_name, replacement_name)
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ subject.perform
+
+ check_index_exists
+ end
+ end
+
+ context 'when it fails to create the replacement index' do
+ it 'safely cleans up and signals the error' do
+ expect(connection).to receive(:execute).with(create_index).ordered
+ .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/)
+
+ check_index_exists
+ end
+ end
+
+ context 'when the replacement index is not valid' do
+ it 'safely cleans up and signals the error' do
+ replacement_index = double('replacement index', valid_index?: false)
+ allow(Gitlab::Database::PostgresIndex).to receive(:find_by).with(schema: 'public', name: replacement_name).and_return(nil, replacement_index)
+
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ expect { subject.perform }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
+
+ check_index_exists
+ end
+ end
+
+ context 'when a database error occurs while swapping the indexes' do
+ it 'safely cleans up and signals the error' do
+ replacement_index = double('replacement index', valid_index?: true)
+ allow(Gitlab::Database::PostgresIndex).to receive(:find_by).with(schema: 'public', name: replacement_name).and_return(nil, replacement_index)
+
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
+ end
+
+ expect_index_rename(index.name, replaced_name).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/)
+
+ check_index_exists
+ end
+ end
+
+ context 'when with_lock_retries fails to acquire the lock' do
+ it 'safely cleans up and signals the error' do
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: true)
+ .and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
+ end
+
+ expect_to_execute_concurrently_in_order(drop_index)
+
+ expect { subject.perform }.to raise_error(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, /exhausted/)
+
+ check_index_exists
+ end
+ end
+ end
+ end
+
+ def expect_to_execute_concurrently_in_order(sql)
+ # Indexes cannot be created CONCURRENTLY in a transaction. Since the tests are wrapped in transactions,
+ # verify the original call but pass through the non-concurrent form.
+ expect(connection).to receive(:execute).with(sql).ordered.and_wrap_original do |method, sql|
+ method.call(sql.sub(/CONCURRENTLY/, ''))
+ end
+ end
+
+ def expect_index_rename(from, to)
+ expect(connection).to receive(:execute).with(<<~SQL).ordered
+ ALTER INDEX "public"."#{from}"
+ RENAME TO "#{to}"
+ SQL
+ end
+
+ def find_index_create_statement
+ ActiveRecord::Base.connection.select_value(<<~SQL)
+ SELECT indexdef
+ FROM pg_indexes
+ WHERE schemaname = 'public'
+ AND indexname = #{ActiveRecord::Base.connection.quote(index.name)}
+ SQL
+ end
+
+ def check_index_exists
+ expect(find_index_create_statement).to eq(original_index)
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
new file mode 100644
index 00000000000..f45d959c0de
--- /dev/null
+++ b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reindexing::Coordinator do
+ include ExclusiveLeaseHelpers
+
+ describe '.perform' do
+ subject { described_class.new(indexes).perform }
+
+ let(:indexes) { [instance_double(Gitlab::Database::PostgresIndex), instance_double(Gitlab::Database::PostgresIndex)] }
+ let(:reindexers) { [instance_double(Gitlab::Database::Reindexing::ConcurrentReindex), instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)] }
+
+ let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
+ let(:lease_key) { 'gitlab/database/reindexing/coordinator' }
+ let(:lease_timeout) { 1.day }
+ let(:uuid) { 'uuid' }
+
+ before do
+ allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield
+
+ indexes.zip(reindexers).each do |index, reindexer|
+ allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer)
+ allow(reindexer).to receive(:perform)
+ end
+ end
+
+ it 'performs concurrent reindexing for each index' do
+ indexes.zip(reindexers).each do |index, reindexer|
+ expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer)
+ expect(reindexer).to receive(:perform)
+ end
+
+ subject
+ end
+
+ it 'keeps track of actions and creates ReindexAction records' do
+ indexes.each do |index|
+ expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield
+ end
+
+ subject
+ end
+
+ context 'locking' do
+ it 'acquires a lock while reindexing' do
+ indexes.each do |index|
+ expect(lease).to receive(:try_obtain).ordered.and_return(uuid)
+ action = instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)
+ expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).ordered.with(index).and_return(action)
+ expect(action).to receive(:perform).ordered
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid)
+ end
+
+ subject
+ end
+
+ it 'does does not perform reindexing actions if lease is not granted' do
+ indexes.each do |index|
+ expect(lease).to receive(:try_obtain).ordered.and_return(false)
+ expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new)
+ end
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
new file mode 100644
index 00000000000..efb5b8463a1
--- /dev/null
+++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do
+ let(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil) }
+ let(:size_after) { 512 }
+
+ it 'yields to the caller' do
+ expect { |b| described_class.keep_track_of(index, &b) }.to yield_control
+ end
+
+ def find_record
+ described_class.find_by(index_identifier: index.identifier)
+ end
+
+ it 'creates the record with a start time and updates its end time' do
+ freeze_time do
+ described_class.keep_track_of(index) do
+ expect(find_record.action_start).to be_within(1.second).of(Time.zone.now)
+
+ travel(10.seconds)
+ end
+
+ duration = find_record.action_end - find_record.action_start
+
+ expect(duration).to be_within(1.second).of(10.seconds)
+ end
+ end
+
+ it 'creates the record with its status set to :started and updates its state to :finished' do
+ described_class.keep_track_of(index) do
+ expect(find_record).to be_started
+ end
+
+ expect(find_record).to be_finished
+ end
+
+ it 'creates the record with the indexes start size and updates its end size' do
+ described_class.keep_track_of(index) do
+ expect(find_record.ondisk_size_bytes_start).to eq(index.ondisk_size_bytes)
+
+ expect(index).to receive(:reload).once
+ allow(index).to receive(:ondisk_size_bytes).and_return(size_after)
+ end
+
+ expect(find_record.ondisk_size_bytes_end).to eq(size_after)
+ end
+
+ context 'in case of errors' do
+ it 'sets the state to failed' do
+ expect do
+ described_class.keep_track_of(index) do
+ raise 'something went wrong'
+ end
+ end.to raise_error(/something went wrong/)
+
+ expect(find_record).to be_failed
+ end
+
+ it 'records the end time' do
+ freeze_time do
+ expect do
+ described_class.keep_track_of(index) do
+ raise 'something went wrong'
+ end
+ end.to raise_error(/something went wrong/)
+
+ expect(find_record.action_end).to be_within(1.second).of(Time.zone.now)
+ end
+ end
+
+ it 'records the resulting index size' do
+ expect(index).to receive(:reload).once
+ allow(index).to receive(:ondisk_size_bytes).and_return(size_after)
+
+ expect do
+ described_class.keep_track_of(index) do
+ raise 'something went wrong'
+ end
+ end.to raise_error(/something went wrong/)
+
+ expect(find_record.ondisk_size_bytes_end).to eq(size_after)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
new file mode 100644
index 00000000000..86b3c029944
--- /dev/null
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reindexing do
+ include ExclusiveLeaseHelpers
+
+ describe '.perform' do
+ subject { described_class.perform(indexes) }
+
+ let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
+ let(:indexes) { double }
+
+ it 'delegates to Coordinator' do
+ expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator)
+ expect(coordinator).to receive(:perform)
+
+ subject
+ end
+ end
+
+ describe '.candidate_indexes' do
+ subject { described_class.candidate_indexes }
+
+ it 'retrieves regular indexes that are no left-overs from previous runs' do
+ result = double
+ expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.not_match.not_match').with(no_args).with('^tmp_reindex_').with('^old_reindex_').and_return(result)
+
+ expect(subject).to eq(result)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/similarity_score_spec.rb b/spec/lib/gitlab/database/similarity_score_spec.rb
index e36a4f610e1..cf75e5a72d9 100644
--- a/spec/lib/gitlab/database/similarity_score_spec.rb
+++ b/spec/lib/gitlab/database/similarity_score_spec.rb
@@ -90,4 +90,15 @@ RSpec.describe Gitlab::Database::SimilarityScore do
expect(subject).to eq(%w[different same gitlab-danger])
end
end
+
+ describe 'annotation' do
+ it 'annotates the generated SQL expression' do
+ expression = Gitlab::Database::SimilarityScore.build_expression(search: 'test', rules: [
+ { column: Arel.sql('path'), multiplier: 1 },
+ { column: Arel.sql('name'), multiplier: 0.8 }
+ ])
+
+ expect(Gitlab::Database::SimilarityScore).to be_order_by_similarity(expression)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 2cc6e175500..220ae705e71 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -104,9 +104,69 @@ RSpec.describe Gitlab::Database::WithLockRetries do
end
context 'after 3 iterations' do
- let(:retry_count) { 4 }
+ it_behaves_like 'retriable exclusive lock on `projects`' do
+ let(:retry_count) { 4 }
+ end
+
+ context 'setting the idle transaction timeout' do
+ context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
+ it 'does not disable the idle transaction timeout' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
+ allow(subject).to receive(:run_block_with_transaction).once
+
+ expect(subject).not_to receive(:disable_idle_in_transaction_timeout)
+
+ subject.run {}
+ end
+ end
- it_behaves_like 'retriable exclusive lock on `projects`'
+ context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
+ it 'disables the idle transaction timeout so the code can sleep and retry' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+
+ n = 0
+ allow(subject).to receive(:run_block_with_transaction).twice do
+ n += 1
+ raise(ActiveRecord::LockWaitTimeout) if n == 1
+ end
+
+ expect(subject).to receive(:disable_idle_in_transaction_timeout).once
+
+ subject.run {}
+ end
+ end
+ end
+ end
+
+ context 'after the retries are exhausted' do
+ let(:timing_configuration) do
+ [
+ [1.second, 1.second]
+ ]
+ end
+
+ context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
+ it 'does not disable the lock_timeout' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
+
+ expect(subject).not_to receive(:disable_lock_timeout)
+
+ subject.run {}
+ end
+ end
+
+ context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
+ it 'disables the lock_timeout' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+ allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
+
+ expect(subject).to receive(:disable_lock_timeout)
+
+ subject.run {}
+ end
+ end
end
context 'after the retries, without setting lock_timeout' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 420aa0a8df6..3175040167b 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -39,6 +39,12 @@ RSpec.describe Gitlab::Database do
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 '.postgresql?' do
subject { described_class.postgresql? }
@@ -70,25 +76,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.postgresql_9_or_less?' do
- it 'returns true when using postgresql 8.4' do
- allow(described_class).to receive(:version).and_return('8.4')
- expect(described_class.postgresql_9_or_less?).to eq(true)
- end
-
- it 'returns true when using PostgreSQL 9.6' do
- allow(described_class).to receive(:version).and_return('9.6')
-
- expect(described_class.postgresql_9_or_less?).to eq(true)
- end
-
- it 'returns false when using PostgreSQL 10 or newer' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.postgresql_9_or_less?).to eq(false)
- 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')
@@ -150,68 +137,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.pg_wal_lsn_diff' do
- it 'returns old name when using PostgreSQL 9.6' do
- allow(described_class).to receive(:version).and_return('9.6')
-
- expect(described_class.pg_wal_lsn_diff).to eq('pg_xlog_location_diff')
- end
-
- it 'returns new name when using PostgreSQL 10 or newer' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.pg_wal_lsn_diff).to eq('pg_wal_lsn_diff')
- end
- end
-
- describe '.pg_current_wal_insert_lsn' do
- it 'returns old name when using PostgreSQL 9.6' do
- allow(described_class).to receive(:version).and_return('9.6')
-
- expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_xlog_insert_location')
- end
-
- it 'returns new name when using PostgreSQL 10 or newer' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_wal_insert_lsn')
- end
- end
-
- describe '.pg_last_wal_receive_lsn' do
- it 'returns old name when using PostgreSQL 9.6' do
- allow(described_class).to receive(:version).and_return('9.6')
-
- expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_xlog_receive_location')
- end
-
- it 'returns new name when using PostgreSQL 10 or newer' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_wal_receive_lsn')
- end
- end
-
- describe '.pg_last_wal_replay_lsn' do
- it 'returns old name when using PostgreSQL 9.6' do
- allow(described_class).to receive(:version).and_return('9.6')
-
- expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_xlog_replay_location')
- end
-
- it 'returns new name when using PostgreSQL 10 or newer' do
- allow(described_class).to receive(:version).and_return('10')
-
- expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_wal_replay_lsn')
- end
- end
-
- describe '.pg_last_xact_replay_timestamp' do
- it 'returns pg_last_xact_replay_timestamp' do
- expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp')
- end
- end
-
describe '.nulls_last_order' do
it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'}
it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'}
@@ -433,6 +358,20 @@ RSpec.describe Gitlab::Database do
end
end
+ describe '.get_write_location' do
+ it 'returns a string' 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 '#true_value' do
it 'returns correct value' do
expect(described_class.true_value).to eq "'t'"
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
index bd60c24859c..72a66b0451e 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
described_class.new(merge_request.merge_request_diff,
batch_page,
batch_size,
- collection_default_args)
+ **collection_default_args)
end
end
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index 7e926f86096..f6810d7a966 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -43,7 +43,8 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
describe '#decorate' do
# Manually creates a Diff::File object to avoid triggering the cache on
- # the FileCollection::MergeRequestDiff
+ # the FileCollection::MergeRequestDiff
+ #
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
@@ -73,6 +74,37 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
expect(rich_texts).to all(be_html_safe)
end
+
+ context "when diff_file is uncached due to default_max_patch_bytes change" do
+ before do
+ expect(cache).to receive(:read_file).at_least(:once).and_return([])
+
+ # Stub out the application's default and current patch size limits. We
+ # want them to be different, and the diff file to be sized between
+ # the 2 values.
+ #
+ diff_file_size_kb = (diff_file.diff.diff.bytesize * 10)
+
+ stub_const("#{diff_file.diff.class}::DEFAULT_MAX_PATCH_BYTES", diff_file_size_kb - 1 )
+ expect(diff_file.diff.class).to receive(:patch_safe_limit_bytes).and_return(diff_file_size_kb + 1)
+ expect(diff_file.diff.class)
+ .to receive(:patch_safe_limit_bytes)
+ .with(diff_file.diff.class::DEFAULT_MAX_PATCH_BYTES)
+ .and_call_original
+ end
+
+ it "manually writes highlighted lines to the cache" do
+ expect(cache).to receive(:write_to_redis_hash).and_call_original
+
+ cache.decorate(diff_file)
+ end
+
+ it "assigns highlighted diff lines to the DiffFile" do
+ expect(diff_file.highlighted_diff_lines.size).to be > 5
+
+ cache.decorate(diff_file)
+ end
+ end
end
shared_examples 'caches missing entries' do
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 07b8070be30..ef448ee96a4 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -65,24 +65,15 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
end
end
- [true, false].each do |state_tracking_enabled|
- context "and current user can update noteable #{state_tracking_enabled ? 'enabled' : 'disabled'}" do
- before do
- stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
-
- project.add_developer(user)
- end
+ context "and current user can update noteable" do
+ before do
+ project.add_developer(user)
+ end
- it 'does not raise an error' do
- if state_tracking_enabled
- expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1)
- else
- # One system note is created for the 'close' event
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- end
+ it 'does not raise an error' do
+ expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1)
- expect(noteable.reload).to be_closed
- end
+ expect(noteable.reload).to be_closed
end
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
index 01e2fe8ce17..40669f06371 100644
--- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
@@ -25,13 +25,17 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d
let!(:lease) { stub_exclusive_lease(unique_key, 'uuid') }
it 'calls the given block' do
- expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false)
+ expect { |b| class_instance.in_lock(unique_key, &b) }
+ .to yield_with_args(false, an_instance_of(described_class::SleepingLock))
end
it 'calls the given block continuously' do
- expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false)
- expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false)
- expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false)
+ expect { |b| class_instance.in_lock(unique_key, &b) }
+ .to yield_with_args(false, an_instance_of(described_class::SleepingLock))
+ expect { |b| class_instance.in_lock(unique_key, &b) }
+ .to yield_with_args(false, an_instance_of(described_class::SleepingLock))
+ expect { |b| class_instance.in_lock(unique_key, &b) }
+ .to yield_with_args(false, an_instance_of(described_class::SleepingLock))
end
it 'cancels the exclusive lease after the block' do
@@ -74,7 +78,8 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d
expect(lease).to receive(:try_obtain).exactly(3).times { nil }
expect(lease).to receive(:try_obtain).once { unique_key }
- expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(true)
+ expect { |b| class_instance.in_lock(unique_key, &b) }
+ .to yield_with_args(true, an_instance_of(described_class::SleepingLock))
end
end
end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 9bc865f4d29..e93593d348f 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Experimentation do
+RSpec.describe Gitlab::Experimentation, :snowplow do
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
test_experiment: {
@@ -69,12 +69,26 @@ RSpec.describe Gitlab::Experimentation do
end
end
+ describe '#push_frontend_experiment' do
+ it 'pushes an experiment to the frontend' do
+ gon = instance_double('gon')
+ experiments = { experiments: { 'myExperiment' => true } }
+
+ stub_experiment_for_user(my_experiment: true)
+ allow(controller).to receive(:gon).and_return(gon)
+
+ expect(gon).to receive(:push).with(experiments, true)
+
+ controller.push_frontend_experiment(:my_experiment)
+ end
+ end
+
describe '#experiment_enabled?' do
subject { controller.experiment_enabled?(:test_experiment) }
context 'cookie is not present' do
- it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of nil' do
- expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, nil)
+ it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of nil' do
+ expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, nil)
controller.experiment_enabled?(:test_experiment)
end
end
@@ -85,22 +99,22 @@ RSpec.describe Gitlab::Experimentation do
get :index
end
- it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
+ it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
# 'abcd1234'.hex % 100 = 76
- expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, 76)
+ expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, 76)
controller.experiment_enabled?(:test_experiment)
end
end
it 'returns true when DNT: 0 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_user?) { true }
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
controller.request.headers['DNT'] = '0'
is_expected.to be_truthy
end
it 'returns false when DNT: 1 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_user?) { true }
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
controller.request.headers['DNT'] = '1'
is_expected.to be_falsy
@@ -127,13 +141,14 @@ RSpec.describe Gitlab::Experimentation do
end
it 'tracks the event with the right parameters' do
- expect(Gitlab::Tracking).to receive(:event).with(
- 'Team',
- 'start',
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
property: 'experimental_group',
- value: 'team_id'
+ value: 1
)
- controller.track_experiment_event(:test_experiment, 'start', 'team_id')
end
end
@@ -143,13 +158,43 @@ RSpec.describe Gitlab::Experimentation do
end
it 'tracks the event with the right parameters' do
- expect(Gitlab::Tracking).to receive(:event).with(
- 'Team',
- 'start',
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1
+ )
+ end
+ end
+
+ context 'do not track is disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'does track the event' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
property: 'control_group',
- value: 'team_id'
+ value: 1
)
- controller.track_experiment_event(:test_experiment, 'start', 'team_id')
+ end
+ end
+
+ context 'do not track enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not track the event' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_no_snowplow_event
end
end
end
@@ -160,8 +205,9 @@ RSpec.describe Gitlab::Experimentation do
end
it 'does not track the event' do
- expect(Gitlab::Tracking).not_to receive(:event)
controller.track_experiment_event(:test_experiment, 'start')
+
+ expect_no_snowplow_event
end
end
end
@@ -220,6 +266,36 @@ RSpec.describe Gitlab::Experimentation do
)
end
end
+
+ context 'do not track disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ property: 'control_group'
+ }
+ )
+ end
+ end
+
+ context 'do not track enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not push data to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+
+ expect(Gon.method_defined?(:tracking_data)).to be_falsey
+ end
+ end
end
context 'when the experiment is disabled' do
@@ -294,6 +370,39 @@ RSpec.describe Gitlab::Experimentation do
controller.record_experiment_user(:test_experiment)
end
end
+
+ context 'do not track' do
+ before do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
+ end
+ end
+
+ context 'is disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'calls add_user on the Experiment model' do
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+
+ context 'is enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not call add_user on the Experiment model' do
+ expect(::Experiment).not_to receive(:add_user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+ end
end
describe '#experiment_tracking_category_and_group' do
@@ -336,8 +445,8 @@ RSpec.describe Gitlab::Experimentation do
end
end
- describe '.enabled_for_user?' do
- subject { described_class.enabled_for_user?(:test_experiment, experimentation_subject_index) }
+ describe '.enabled_for_value?' do
+ subject { described_class.enabled_for_value?(:test_experiment, experimentation_subject_index) }
let(:experimentation_subject_index) { 9 }
@@ -377,4 +486,32 @@ RSpec.describe Gitlab::Experimentation do
end
end
end
+
+ describe '.enabled_for_attribute?' do
+ subject { described_class.enabled_for_attribute?(:test_experiment, attribute) }
+
+ let(:attribute) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7
+
+ context 'experiment is disabled' do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(false)
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'experiment is enabled' do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
+ end
+
+ it { is_expected.to be true }
+
+ context 'outside enabled ratio' do
+ let(:attribute) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17
+
+ it { is_expected.to be false }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index e1bcf4aeeb1..9271f635b14 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -85,9 +85,9 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
}
end
- let(:stale_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } }
- let(:active_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } }
- let(:future_sha) { Timecop.freeze(100.days.since) { create_commit } }
+ let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } }
+ let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } }
+ let(:future_sha) { travel_to(100.days.since) { create_commit } }
before do
repository.create_branch('stale-1', stale_sha)
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index b202015464f..1a3c332a21b 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -9,8 +9,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
MutatingConstantIterator.class_eval do
include Enumerable
+ attr_reader :size
+
def initialize(count, value)
@count = count
+ @size = count
@value = value
end
@@ -517,21 +520,39 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
.to yield_with_args(an_instance_of(Gitlab::Git::Diff))
end
- it 'prunes diffs that are quite big' do
- diff = nil
+ context 'single-file collections' do
+ it 'does not prune diffs' do
+ diff = nil
- subject.each do |d|
- diff = d
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).not_to eq('')
end
+ end
+
+ context 'multi-file collections' do
+ let(:iterator) { [{ diff: 'b' }, { diff: 'a' * 20480 }]}
+
+ it 'prunes diffs that are quite big' do
+ diff = nil
- expect(diff.diff).to eq('')
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).to eq('')
+ end
end
context 'when go over safe limits on files' do
let(:iterator) { [fake_diff(1, 1)] * 4 }
before do
- stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
+ allow(Gitlab::Git::DiffCollection)
+ .to receive(:default_limits)
+ .and_return({ max_files: 2, max_lines: max_lines })
end
it 'prunes diffs by default even little ones' do
@@ -556,7 +577,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
end
before do
- stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ allow(Gitlab::Git::DiffCollection)
+ .to receive(:default_limits)
+ .and_return({ max_files: max_files, max_lines: 80 })
end
it 'prunes diffs by default even little ones' do
@@ -581,7 +604,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
end
before do
- stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ allow(Gitlab::Git::DiffCollection)
+ .to receive(:default_limits)
+ .and_return({ max_files: max_files, max_lines: 80 })
end
it 'prunes diffs by default even little ones' do
@@ -665,8 +690,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
end
before do
- stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS',
- { max_files: max_files, max_lines: 80 })
+ allow(Gitlab::Git::DiffCollection)
+ .to receive(:default_limits)
+ .and_return({ max_files: max_files, max_lines: 80 })
end
it 'considers size of diffs before the offset for prunning' do
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 117c519e98d..980a52bb61e 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -284,13 +284,21 @@ EOT
end
describe '#line_count' do
- it 'returns the correct number of lines' do
- diff = described_class.new(gitaly_diff)
+ let(:diff) { described_class.new(gitaly_diff) }
+ it 'returns the correct number of lines' do
expect(diff.line_count).to eq(7)
end
end
+ describe "#diff_bytesize" do
+ let(:diff) { described_class.new(gitaly_diff) }
+
+ it "returns the size of the diff in bytes" do
+ expect(diff.diff_bytesize).to eq(diff.diff.bytesize)
+ end
+ end
+
describe '#too_large?' do
it 'returns true for a diff that is too large' do
diff = described_class.new(diff: 'a' * 204800)
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index c8fbc674c73..e1873c6ddb5 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Git::ObjectPool do
describe '#create' do
before do
- subject.create
+ subject.create # rubocop:disable Rails/SaveBang
end
context "when the pool doesn't exist yet" do
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Git::ObjectPool do
context 'when the pool already exists' do
it 'raises an FailedPrecondition' do
expect do
- subject.create
+ subject.create # rubocop:disable Rails/SaveBang
end.to raise_error(GRPC::FailedPrecondition)
end
end
diff --git a/spec/lib/gitlab/git/remote_mirror_spec.rb b/spec/lib/gitlab/git/remote_mirror_spec.rb
index 423c4aa9620..92504b7aafe 100644
--- a/spec/lib/gitlab/git/remote_mirror_spec.rb
+++ b/spec/lib/gitlab/git/remote_mirror_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Git::RemoteMirror do
.to receive(:update_remote_mirror)
.with(ref_name, ['master'], ssh_key: 'KEY', known_hosts: 'KNOWN HOSTS', keep_divergent_refs: true)
- remote_mirror.update
+ remote_mirror.update # rubocop:disable Rails/SaveBang
end
it 'wraps gitaly errors' do
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Git::RemoteMirror do
.to receive(:update_remote_mirror)
.and_raise(StandardError)
- expect { remote_mirror.update }.to raise_error(StandardError)
+ expect { remote_mirror.update }.to raise_error(StandardError) # rubocop:disable Rails/SaveBang
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 73eecd3401a..6dfa791f70b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:expected_extension) { 'tar.gz' }
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
- let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
+ let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) }
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
@@ -133,12 +133,32 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
expect(metadata['ArchivePrefix']).to eq(expected_prefix)
end
- it 'sets ArchivePath to the expected globally-unique path' do
- # This is really important from a security perspective. Think carefully
- # before changing it: https://gitlab.com/gitlab-org/gitlab-foss/issues/45689
- expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
+ context 'when :include_lfs_blobs_in_archive feature flag is disabled' do
+ let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
- expect(metadata['ArchivePath']).to eq(expected_path)
+ before do
+ stub_feature_flags(include_lfs_blobs_in_archive: false)
+ end
+
+ it 'sets ArchivePath to the expected globally-unique path' do
+ # This is really important from a security perspective. Think carefully
+ # before changing it: https://gitlab.com/gitlab-org/gitlab-foss/issues/45689
+ expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
+
+ expect(metadata['ArchivePath']).to eq(expected_path)
+ end
+ end
+
+ context 'when :include_lfs_blobs_in_archive feature flag is enabled' do
+ before do
+ stub_feature_flags(include_lfs_blobs_in_archive: true)
+ end
+
+ it 'sets ArchivePath to the expected globally-unique path' do
+ expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
+
+ expect(metadata['ArchivePath']).to eq(expected_path)
+ end
end
context 'path is set' do
@@ -1630,13 +1650,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:right_branch) { 'test-master' }
let(:first_parent_ref) { 'refs/heads/test-master' }
let(:target_ref) { 'refs/merge-requests/999/merge' }
+ let(:allow_conflicts) { false }
before do
repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref)
end
def merge_to_ref
- repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref)
+ repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref, allow_conflicts)
end
it 'generates a commit in the target_ref' do
@@ -2079,7 +2100,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) }
before do
- object_pool.create
+ object_pool.create # rubocop:disable Rails/SaveBang
end
it 'does not raise an error when disconnecting a non-linked repository' do
diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
index 4f6a3fb823e..16cea1dc1a3 100644
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
@@ -7,7 +7,7 @@ require 'tempfile'
RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:feature_flag_name) { 'feature-flag-name' }
+ let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file }
before(:all) do
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
end
end
- context 'when feature flag is not persisted' do
+ context 'when feature flag is not persisted', stub_feature_flags: false do
context 'when running puma with multiple threads' do
before do
allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(true)
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index a88097705f6..36bff42d937 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -51,6 +51,11 @@ RSpec.describe Gitlab::Git::Wiki do
expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1'
expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
end
+
+ it 'returns nil for invalid arguments' do
+ expect(subject.page(title: '')).to be_nil
+ expect(subject.page(title: 'foo', version: ':')).to be_nil
+ end
end
describe '#delete_page' do
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index 3b8b5fd82c6..8c481cdee08 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -232,29 +232,6 @@ RSpec.describe Gitlab::GitAccessSnippet do
end
end
- context 'when geo is enabled', if: Gitlab.ee? do
- let(:user) { snippet.author }
- let!(:primary_node) { FactoryBot.create(:geo_node, :primary) }
-
- before do
- allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
- allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true)
- end
-
- # Without override, push access would return Gitlab::GitAccessResult::CustomAction
- it 'skips geo for snippet' do
- expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
- end
-
- context 'when user is migration bot' do
- let(:user) { migration_bot }
-
- it 'skips geo for snippet' do
- expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
- end
- end
- end
-
context 'when changes are specific' do
let(:changes) { "2d1db523e11e777e49377cfb22d368deec3f0793 ddd0f15ae83993f5cb66a927a28673882e99100b master" }
let(:user) { snippet.author }
@@ -283,7 +260,7 @@ RSpec.describe Gitlab::GitAccessSnippet do
service = double
expect(service).to receive(:validate!).and_return(nil)
- expect(Snippet).to receive(:max_file_limit).with(user).and_return(5)
+ expect(Snippet).to receive(:max_file_limit).and_return(5)
expect(Gitlab::Checks::PushFileCountCheck).to receive(:new).with(anything, hash_including(limit: 5)).and_return(service)
push_access_check
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 85567ab2e55..21607edbc32 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -420,6 +420,13 @@ RSpec.describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_forbidden('Your account has been blocked.')
end
+ it 'disallows users that are blocked pending approval to pull' do
+ project.add_maintainer(user)
+ user.block_pending_approval
+
+ expect { pull_access_check }.to raise_forbidden('Your account is pending approval from your administrator and hence blocked.')
+ end
+
it 'disallows deactivated users to pull' do
project.add_maintainer(user)
user.deactivate!
@@ -428,14 +435,12 @@ RSpec.describe Gitlab::GitAccess do
end
context 'when the project repository does not exist' do
- it 'returns not found' do
+ before do
project.add_guest(user)
- repo = project.repository
- Gitlab::GitalyClient::StorageSettings.allow_disk_access { FileUtils.rm_rf(repo.path) }
-
- # Sanity check for rm_rf
- expect(repo.exists?).to eq(false)
+ allow(project.repository).to receive(:exists?).and_return(false)
+ end
+ it 'returns not found' do
expect { pull_access_check }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.')
end
end
@@ -917,6 +922,12 @@ RSpec.describe Gitlab::GitAccess do
project.add_developer(user)
end
+ it 'disallows users that are blocked pending approval to push' do
+ user.block_pending_approval
+
+ expect { push_access_check }.to raise_forbidden('Your account is pending approval from your administrator and hence blocked.')
+ end
+
it 'does not allow deactivated users to push' do
user.deactivate!
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 688089f4862..b78d99269d3 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -3,17 +3,17 @@
require 'spec_helper'
RSpec.describe Gitlab::GitAccessWiki do
- let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
- let_it_be(:project) { create(:project, :wiki_repo) }
let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
+ let_it_be(:wiki) { create(:project_wiki, project: project) }
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
+ let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil }
- let(:authentication_abilities) do
- [
- :read_project,
- :download_code,
- :push_code
- ]
+
+ let(:access) do
+ described_class.new(user, wiki, 'web',
+ authentication_abilities: authentication_abilities,
+ redirected_path: redirected_path)
end
describe '#push_access_check' do
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::GitAccessWiki do
context 'when the repository does not exist' do
before do
- allow(project.wiki).to receive(:repository).and_return(double('Repository', exists?: false))
+ allow(wiki.repository).to receive(:exists?).and_return(false)
end
it_behaves_like 'not-found git access' do
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 9581b017839..f977fe1638f 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -13,6 +13,10 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
let(:client) { described_class.new(repository) }
describe '#diff_from_parent' do
+ before do
+ stub_feature_flags(increased_diff_limits: false)
+ end
+
context 'when a commit has a parent' do
it 'sends an RPC request with the parent ID as left commit' do
request = Gitaly::CommitDiffRequest.new(
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index b974f456914..ce01566b870 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -88,9 +88,10 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:ref) { 'refs/merge-requests/x/merge' }
let(:message) { 'validación' }
+ let(:allow_conflicts) { false }
let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') }
- subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) }
+ subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref, allow_conflicts) }
it 'sends a user_merge_to_ref message' do
expect_any_instance_of(Gitaly::OperationService::Stub)
diff --git a/spec/lib/gitlab/gitpod_spec.rb b/spec/lib/gitlab/gitpod_spec.rb
index f4dda42aeb4..717e396f942 100644
--- a/spec/lib/gitlab/gitpod_spec.rb
+++ b/spec/lib/gitlab/gitpod_spec.rb
@@ -4,30 +4,29 @@ require 'spec_helper'
RSpec.describe Gitlab::Gitpod do
let_it_be(:user) { create(:user) }
- let(:feature_scope) { true }
before do
stub_feature_flags(gitpod: feature_scope)
end
- describe '.feature_conditional?' do
- subject { described_class.feature_conditional? }
-
- context 'when feature is enabled globally' do
- it { is_expected.to be_falsey }
- end
+ describe '.feature_available?' do
+ subject { described_class.feature_available? }
- context 'when feature is enabled only to a resource' do
- let(:feature_scope) { user }
+ context 'when feature has not been set' do
+ let(:feature_scope) { nil }
it { is_expected.to be_truthy }
end
- end
- describe '.feature_available?' do
- subject { described_class.feature_available? }
+ context 'when feature is disabled' do
+ let(:feature_scope) { false }
+
+ it { is_expected.to be_falsey }
+ end
context 'when feature is enabled globally' do
+ let(:feature_scope) { true }
+
it { is_expected.to be_truthy }
end
@@ -43,7 +42,15 @@ RSpec.describe Gitlab::Gitpod do
subject { described_class.feature_enabled?(current_user) }
+ context 'when feature has not been set' do
+ let(:feature_scope) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
context 'when feature is enabled globally' do
+ let(:feature_scope) { true }
+
it { is_expected.to be_truthy }
end
diff --git a/spec/lib/gitlab/gl_repository/identifier_spec.rb b/spec/lib/gitlab/gl_repository/identifier_spec.rb
index e95aaaa6690..e0622e30e7a 100644
--- a/spec/lib/gitlab/gl_repository/identifier_spec.rb
+++ b/spec/lib/gitlab/gl_repository/identifier_spec.rb
@@ -35,14 +35,14 @@ RSpec.describe Gitlab::GlRepository::Identifier do
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project.id }
let(:identifier) { "wiki-#{record_id}" }
- let(:expected_container) { project }
+ let(:expected_container) { project.wiki }
let(:expected_type) { Gitlab::GlRepository::WIKI }
end
it_behaves_like 'parsing gl_repository identifier' do
let(:record_id) { project.id }
let(:identifier) { "project-#{record_id}-wiki" }
- let(:expected_container) { project }
+ let(:expected_container) { project.wiki }
let(:expected_type) { Gitlab::GlRepository::WIKI }
end
end
@@ -87,7 +87,8 @@ RSpec.describe Gitlab::GlRepository::Identifier do
'project-wibble-wiki',
'wiki-1-project',
'snippet',
- 'project-1-wiki-bar'
+ 'project-1-wiki-bar',
+ 'project-1-project'
]
end
@@ -96,10 +97,5 @@ RSpec.describe Gitlab::GlRepository::Identifier do
expect { described_class.parse(identifier) }.to raise_error(described_class::InvalidIdentifier)
end
end
-
- it 'raises InvalidIdentifier on project-1-project' do
- pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/219192'
- expect { described_class.parse('project-1-project') }.to raise_error(described_class::InvalidIdentifier)
- end
end
end
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
index 3fa636a1cf0..629e6c96858 100644
--- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -41,12 +41,14 @@ RSpec.describe Gitlab::GlRepository::RepoType do
end
describe Gitlab::GlRepository::WIKI do
+ let(:wiki) { project.wiki }
+
it_behaves_like 'a repo type' do
- let(:expected_id) { project.id }
+ let(:expected_id) { wiki.project.id }
let(:expected_identifier) { "wiki-#{expected_id}" }
let(:expected_suffix) { '.wiki' }
- let(:expected_container) { project }
- let(:expected_repository) { ::Repository.new(project.wiki.full_path, project, shard: project.wiki.repository_storage, disk_path: project.wiki.disk_path, repo_type: Gitlab::GlRepository::WIKI) }
+ let(:expected_container) { wiki }
+ let(:expected_repository) { ::Repository.new(wiki.full_path, wiki, shard: wiki.repository_storage, disk_path: wiki.disk_path, repo_type: Gitlab::GlRepository::WIKI) }
end
it 'knows its type' do
diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb
index 3733d545155..05914f92c01 100644
--- a/spec/lib/gitlab/gl_repository_spec.rb
+++ b/spec/lib/gitlab/gl_repository_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ::Gitlab::GlRepository do
end
it 'parses a project wiki gl_repository' do
- expect(described_class.parse("wiki-#{project.id}")).to eq([project, project, Gitlab::GlRepository::WIKI])
+ expect(described_class.parse("wiki-#{project.id}")).to eq([project.wiki, project, Gitlab::GlRepository::WIKI])
end
it 'parses a snippet gl_repository' do
diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb
index 95db6b2b4e0..3d3f381b6d2 100644
--- a/spec/lib/gitlab/gon_helper_spec.rb
+++ b/spec/lib/gitlab/gon_helper_spec.rb
@@ -10,6 +10,10 @@ RSpec.describe Gitlab::GonHelper do
end
describe '#push_frontend_feature_flag' do
+ before do
+ skip_feature_flags_yaml_validation
+ end
+
it 'pushes a feature flag to the frontend' do
gon = instance_double('gon')
thing = stub_feature_flag_gate('thing')
diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
index e68c1446502..9538c4bae2b 100644
--- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
end
it 'returns the correct duration in seconds' do
- Timecop.freeze(start_time) do
+ travel_to(start_time) do
subject.before
expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f })
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
index efe6c27c463..7576523ce52 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -19,24 +19,29 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
options.reverse_merge!(null: true)
field :test_field, field_type,
authorize: field_authorizations,
- resolve: -> (_, _, _) { resolved_value },
**options
+
+ define_method :test_field do
+ resolved_value
+ end
end
end
- let(:current_user) { double(:current_user) }
-
subject(:service) { described_class.new(field) }
describe '#authorized_resolve' do
- let(:presented_object) { double('presented object') }
- let(:presented_type) { double('parent type', object: presented_object) }
- let(:query_type) { GraphQL::ObjectType.new }
- let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
- let(:query_context) { OpenStruct.new(schema: schema) }
- let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema, context: query_context), values: { current_user: current_user }, object: nil) }
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:presented_object) { 'presented object' }
+ let_it_be(:query_type) { GraphQL::ObjectType.new }
+ let_it_be(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
+ let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) }
+ let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) }
+
+ let(:type_class) { type_with_field(custom_type, :read_field, presented_object) }
+ let(:type_instance) { type_class.authorized_new(presented_object, context) }
+ let(:field) { type_class.fields['testField'].to_graphql }
- subject(:resolved) { service.authorized_resolve.call(presented_type, {}, context) }
+ subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) }
context 'scalar types' do
shared_examples 'checking permissions on the presented object' do
@@ -48,7 +53,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
expect(resolved).to eq('Resolved value')
end
- it "returns nil if the value wasn't authorized" do
+ it 'returns nil if the value was not authorized' do
allow(Ability).to receive(:allowed?).and_return false
expect(resolved).to be_nil
@@ -56,28 +61,28 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end
context 'when the field is a built-in scalar type' do
- let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) }
let(:expected_permissions) { [:read_field] }
it_behaves_like 'checking permissions on the presented object'
end
context 'when the field is a list of scalar types' do
- let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) }
let(:expected_permissions) { [:read_field] }
it_behaves_like 'checking permissions on the presented object'
end
context 'when the field is sub-classed scalar type' do
- let(:field) { type_with_field(Types::TimeType, :read_field).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field(Types::TimeType, :read_field) }
let(:expected_permissions) { [:read_field] }
it_behaves_like 'checking permissions on the presented object'
end
context 'when the field is a list of sub-classed scalar types' do
- let(:field) { type_with_field([Types::TimeType], :read_field).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field([Types::TimeType], :read_field) }
let(:expected_permissions) { [:read_field] }
it_behaves_like 'checking permissions on the presented object'
@@ -86,7 +91,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
context 'when the field is a connection' do
context 'when it resolves to nil' do
- let(:field) { type_with_field(Types::QueryType.connection_type, :read_field, nil).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) }
it 'does not fail when authorizing' do
expect(resolved).to be_nil
@@ -97,7 +102,11 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
context 'when the field is a specific type' do
let(:custom_type) { type(:read_type) }
let(:object_in_field) { double('presented in field') }
- let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields['testField'].to_graphql }
+
+ let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) }
+ let(:type_instance) { type_class.authorized_new(object_in_field, context) }
+
+ subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) }
it 'checks both field & type permissions' do
spy_ability_check_for(:read_field, object_in_field, passed: true)
@@ -114,7 +123,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end
context 'when the field is not nullable' do
- let(:field) { type_with_field(custom_type, [], object_in_field, null: false).fields['testField'].to_graphql }
+ let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) }
it 'returns nil when viewing is not allowed' do
spy_ability_check_for(:read_type, object_in_field, passed: false)
@@ -127,7 +136,9 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
let(:object_1) { double('presented in field 1') }
let(:object_2) { double('presented in field 2') }
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
- let(:field) { type_with_field([custom_type], :read_field, presented_types).fields['testField'].to_graphql }
+
+ let(:type_class) { type_with_field([custom_type], :read_field, presented_types) }
+ let(:type_instance) { type_class.authorized_new(presented_types, context) }
it 'checks all permissions' do
allow(Ability).to receive(:allowed?) { true }
diff --git a/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb b/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb
deleted file mode 100644
index af604e1c7d5..00000000000
--- a/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Gitlab::Graphql::MarkdownField::Resolver do
- include Gitlab::Routing
- let(:resolver) { described_class.new(:note) }
-
- describe '#proc' do
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project) }
- let(:note) do
- create(:note,
- note: "Referencing #{issue.to_reference(full: true)}")
- end
-
- it 'renders markdown correctly' do
- expect(resolver.proc.call(note, {}, {})).to include(issue_path(issue))
- end
-
- context 'when the issue is not publicly accessible' do
- let(:project) { create(:project, :private) }
-
- it 'hides the references from users that are not allowed to see the reference' do
- expect(resolver.proc.call(note, {}, {})).not_to include(issue_path(issue))
- end
-
- it 'shows the reference to users that are allowed to see it' do
- expect(resolver.proc.call(note, {}, { current_user: project.owner }))
- .to include(issue_path(issue))
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index e3da925376e..82090f992eb 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Graphql::MarkdownField do
+ include Gitlab::Routing
+
describe '.markdown_field' do
it 'creates the field with some default attributes' do
field = class_with_markdown_field(:test_html, null: true, method: :hello).fields['testHtml']
@@ -13,7 +15,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
end
context 'developer warnings' do
- let(:expected_error) { /Only `method` is allowed to specify the markdown field/ }
+ let_it_be(:expected_error) { /Only `method` is allowed to specify the markdown field/ }
it 'raises when passing a resolver' do
expect { class_with_markdown_field(:test_html, null: true, resolver: 'not really') }
@@ -27,30 +29,61 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
end
context 'resolving markdown' do
- let(:note) { build(:note, note: '# Markdown!') }
- let(:thing_with_markdown) { double('markdown thing', object: note) }
- let(:expected_markdown) { '<h1 data-sourcepos="1:1-1:11" dir="auto">Markdown!</h1>' }
- let(:query_type) { GraphQL::ObjectType.new }
- let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
- let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
+ let_it_be(:note) { build(:note, note: '# Markdown!') }
+ let_it_be(:expected_markdown) { '<h1 data-sourcepos="1:1-1:11" dir="auto">Markdown!</h1>' }
+ let_it_be(:query_type) { GraphQL::ObjectType.new }
+ let_it_be(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
+ let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) }
+ let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: {}, object: nil) }
+
+ let(:type_class) { class_with_markdown_field(:note_html, null: false) }
+ let(:type_instance) { type_class.authorized_new(note, context) }
+ let(:field) { type_class.fields['noteHtml'] }
it 'renders markdown from the same property as the field name without the `_html` suffix' do
- field = class_with_markdown_field(:note_html, null: false).fields['noteHtml']
+ expect(field.to_graphql.resolve(type_instance, {}, context)).to eq(expected_markdown)
+ end
+
+ context 'when a `method` argument is passed' do
+ let(:type_class) { class_with_markdown_field(:test_html, null: false, method: :note) }
+ let(:field) { type_class.fields['testHtml'] }
- expect(field.to_graphql.resolve(thing_with_markdown, {}, context)).to eq(expected_markdown)
+ it 'renders markdown from a specific property' do
+ expect(field.to_graphql.resolve(type_instance, {}, context)).to eq(expected_markdown)
+ end
end
- it 'renders markdown from a specific property when a `method` argument is passed' do
- field = class_with_markdown_field(:test_html, null: false, method: :note).fields['testHtml']
+ describe 'basic verification that references work' do
+ let_it_be(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { build(:note, note: "Referencing #{issue.to_reference(full: true)}") }
+
+ it 'renders markdown correctly' do
+ expect(field.to_graphql.resolve(type_instance, {}, context)).to include(issue_path(issue))
+ end
+
+ context 'when the issue is not publicly accessible' do
+ let_it_be(:project) { create(:project, :private) }
+
+ it 'hides the references from users that are not allowed to see the reference' do
+ expect(field.to_graphql.resolve(type_instance, {}, context)).not_to include(issue_path(issue))
+ end
+
+ it 'shows the reference to users that are allowed to see it' do
+ context = GraphQL::Query::Context.new(query: query, values: { current_user: project.owner }, object: nil)
+ type_instance = type_class.authorized_new(note, context)
- expect(field.to_graphql.resolve(thing_with_markdown, {}, context)).to eq(expected_markdown)
+ expect(field.to_graphql.resolve(type_instance, {}, context)).to include(issue_path(issue))
+ end
+ end
end
end
end
def class_with_markdown_field(name, **args)
- Class.new(GraphQL::Schema::Object) do
+ Class.new(Types::BaseObject) do
prepend Gitlab::Graphql::MarkdownField
+ graphql_name 'MarkdownFieldTest'
markdown_field name, **args
end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb
new file mode 100644
index 00000000000..b45bb8b79d9
--- /dev/null
+++ b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Graphql::Pagination::Keyset::LastItems do
+ let_it_be(:merge_request) { create(:merge_request) }
+ let(:scope) { MergeRequest.order_merged_at_asc.with_order_id_desc }
+
+ subject { described_class.take_items(*args) }
+
+ context 'when the `count` parameter is nil' do
+ let(:args) { [scope, nil] }
+
+ it 'returns a single record' do
+ expect(subject).to eq(merge_request)
+ end
+ end
+
+ context 'when the `count` parameter is given' do
+ let(:args) { [scope, 1] }
+
+ it 'returns an array' do
+ expect(subject).to eq([merge_request])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
index 444c10074a0..eb28e6c8c0a 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
@@ -63,6 +63,29 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do
expect(order_list.first.sort_direction).to eq :desc
end
end
+
+ context 'when ordering by CASE', :aggregate_failuers do
+ let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) }
+
+ it 'assigns the right attribute name, named function, and direction' do
+ expect(order_list.count).to eq 1
+ expect(order_list.first.attribute_name).to eq 'case_order_value'
+ expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case)
+ expect(order_list.first.sort_direction).to eq :asc
+ end
+ end
+
+ context 'when ordering by ARRAY_POSITION', :aggregate_failuers do
+ let(:array_position) { Arel::Nodes::NamedFunction.new('ARRAY_POSITION', [Arel.sql("ARRAY[1,0]::smallint[]"), Project.arel_table[:auto_cancel_pending_pipelines]]) }
+ let(:relation) { Project.order(array_position.asc) }
+
+ it 'assigns the right attribute name, named function, and direction' do
+ expect(order_list.count).to eq 1
+ expect(order_list.first.attribute_name).to eq 'array_position'
+ expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction)
+ expect(order_list.first.sort_direction).to eq :asc
+ end
+ end
end
describe '#validate_ordering' do
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb
index c7e7db4d535..fa631aa5666 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb
@@ -136,11 +136,12 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::QueryBuilder do
let(:relation) { Project.sorted_by_similarity_desc('test', include_in_select: true) }
let(:arel_table) { Project.arel_table }
let(:decoded_cursor) { { 'similarity' => 0.5, 'id' => 100 } }
+ let(:similarity_function_call) { Gitlab::Database::SimilarityScore::SIMILARITY_FUNCTION_CALL_WITH_ANNOTATION }
let(:similarity_sql) do
[
- '(SIMILARITY(COALESCE("projects"."path", \'\'), \'test\') * CAST(\'1\' AS numeric))',
- '(SIMILARITY(COALESCE("projects"."name", \'\'), \'test\') * CAST(\'0.7\' AS numeric))',
- '(SIMILARITY(COALESCE("projects"."description", \'\'), \'test\') * CAST(\'0.2\' AS numeric))'
+ "(#{similarity_function_call}(COALESCE(\"projects\".\"path\", ''), 'test') * CAST('1' AS numeric))",
+ "(#{similarity_function_call}(COALESCE(\"projects\".\"name\", ''), 'test') * CAST('0.7' AS numeric))",
+ "(#{similarity_function_call}(COALESCE(\"projects\".\"description\", ''), 'test') * CAST('0.2' AS numeric))"
].join(' + ')
end
diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
index 89d2ab8bb87..c8432513185 100644
--- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
+++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
end
it 'returns a duration in seconds' do
- allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2])
+ allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2, [[], []]])
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
allow(Gitlab::GraphqlLogger).to receive(:info)
diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb
index 045c922783a..009f66d2108 100644
--- a/spec/lib/gitlab/group_search_results_spec.rb
+++ b/spec/lib/gitlab/group_search_results_spec.rb
@@ -17,10 +17,17 @@ RSpec.describe Gitlab::GroupSearchResults do
describe 'issues search' do
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
+ let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
+
let(:query) { 'foo' }
let(:scope) { 'issues' }
+ before do
+ project.add_developer(user)
+ end
+
include_examples 'search results filtered by state'
+ include_examples 'search results filtered by confidential'
end
describe 'merge_requests search' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3126d87a0d6..5ee7fb2adbf 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -30,6 +30,7 @@ issues:
- metrics
- timelogs
- issuable_severity
+- issuable_sla
- issue_assignees
- closed_by
- epic_issue
@@ -51,6 +52,7 @@ issues:
- status_page_published_incident
- namespace
- note_authors
+- issue_email_participants
events:
- author
- project
@@ -158,6 +160,8 @@ merge_requests:
- assignees
- reviews
- approval_rules
+- approval_merge_request_rule_sources
+- approval_project_rules
- approvals
- approvers
- approver_users
@@ -242,6 +246,7 @@ ci_pipelines:
- latest_builds_report_results
- messages
- pipeline_artifacts
+- latest_statuses
ci_refs:
- project
- ci_pipelines
@@ -300,6 +305,7 @@ protected_branches:
- push_access_levels
- unprotect_access_levels
- approval_project_rules
+- required_code_owners_sections
protected_tags:
- project
- create_access_levels
@@ -408,6 +414,7 @@ project:
- stages
- ci_refs
- builds
+- processables
- runner_projects
- runners
- variables
@@ -465,6 +472,8 @@ project:
- feature_usage
- approval_rules
- approval_merge_request_rules
+- approval_merge_request_rule_sources
+- approval_project_rules
- approvers
- approver_users
- audit_events
@@ -536,6 +545,8 @@ project:
- vulnerability_historical_statistics
- product_analytics_events
- pipeline_artifacts
+- terraform_states
+- alert_management_http_integrations
award_emoji:
- awardable
- user
@@ -703,3 +714,5 @@ system_note_metadata:
- description_version
status_page_published_incident:
- issue
+issuable_sla:
+ - issue
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 93b6f93f0ec..d084b9d7f7e 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -10,14 +10,17 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
# all items are properly serialized while traversing the simple hash.
subject { Gitlab::Json.parse(Gitlab::Json.generate(described_class.new(project, tree).execute)) }
- let!(:project) { setup_project }
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { setup_project }
let(:shared) { project.import_export_shared }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:tree) { reader.project_tree }
- before do
+ before_all do
project.add_maintainer(user)
+ end
+
+ before do
allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD')
allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA')
end
@@ -224,7 +227,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
group: group,
approvals_before_merge: 1
)
- allow(project).to receive(:commit).and_return(Commit.new(RepoHelpers.sample_commit, project))
issue = create(:issue, assignees: [user], project: project)
snippet = create(:project_snippet, project: project)
diff --git a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
index eb9a3fa9bd8..6b2f80cc80a 100644
--- a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb
@@ -5,16 +5,19 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Group::RelationFactory do
let(:group) { create(:group) }
let(:members_mapper) { double('members_mapper').as_null_object }
- let(:user) { create(:admin) }
+ let(:admin) { create(:admin) }
+ let(:importer_user) { admin }
let(:excluded_keys) { [] }
let(:created_object) do
- described_class.create(relation_sym: relation_sym,
- relation_hash: relation_hash,
- members_mapper: members_mapper,
- object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
- user: user,
- importable: group,
- excluded_keys: excluded_keys)
+ described_class.create(
+ relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
+ user: importer_user,
+ importable: group,
+ excluded_keys: excluded_keys
+ )
end
context 'label object' do
@@ -24,18 +27,18 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do
let(:relation_hash) do
{
- 'id' => 123456,
- 'title' => 'Bruffefunc',
- 'color' => '#1d2da4',
- 'project_id' => nil,
- 'created_at' => '2019-11-20T17:02:20.546Z',
- 'updated_at' => '2019-11-20T17:02:20.546Z',
- 'template' => false,
+ 'id' => 123456,
+ 'title' => 'Bruffefunc',
+ 'color' => '#1d2da4',
+ 'project_id' => nil,
+ 'created_at' => '2019-11-20T17:02:20.546Z',
+ 'updated_at' => '2019-11-20T17:02:20.546Z',
+ 'template' => false,
'description' => 'Description',
- 'group_id' => original_group_id,
- 'type' => 'GroupLabel',
- 'priorities' => [],
- 'textColor' => '#FFFFFF'
+ 'group_id' => original_group_id,
+ 'type' => 'GroupLabel',
+ 'priorities' => [],
+ 'textColor' => '#FFFFFF'
}
end
@@ -60,58 +63,28 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do
end
end
- context 'Notes user references' do
- let(:relation_sym) { :notes }
- let(:new_user) { create(:user) }
- let(:exported_member) do
- {
- 'id' => 111,
- 'access_level' => 30,
- 'source_id' => 1,
- 'source_type' => 'Namespace',
- 'user_id' => 3,
- 'notification_level' => 3,
- 'created_at' => '2016-11-18T09:29:42.634Z',
- 'updated_at' => '2016-11-18T09:29:42.634Z',
- 'user' => {
- 'id' => 999,
- 'email' => new_user.email,
- 'username' => new_user.username
- }
- }
- end
-
+ it_behaves_like 'Notes user references' do
+ let(:importable) { group }
let(:relation_hash) do
{
- 'id' => 4947,
- 'note' => 'note',
+ 'id' => 4947,
+ 'note' => 'note',
'noteable_type' => 'Epic',
- 'author_id' => 999,
- 'created_at' => '2016-11-18T09:29:42.634Z',
- 'updated_at' => '2016-11-18T09:29:42.634Z',
- 'project_id' => 1,
- 'attachment' => {
+ 'author_id' => 999,
+ 'created_at' => '2016-11-18T09:29:42.634Z',
+ 'updated_at' => '2016-11-18T09:29:42.634Z',
+ 'project_id' => 1,
+ 'attachment' => {
'url' => nil
},
- 'noteable_id' => 377,
- 'system' => true,
- 'author' => {
+ 'noteable_id' => 377,
+ 'system' => true,
+ 'author' => {
'name' => 'Administrator'
},
'events' => []
}
end
-
- let(:members_mapper) do
- Gitlab::ImportExport::MembersMapper.new(
- exported_members: [exported_member],
- user: user,
- importable: group)
- end
-
- it 'maps the right author to the imported note' do
- expect(created_object.author).to eq(new_user)
- end
end
def random_id
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 9737a0f39fc..7a9e7d8afba 100644
--- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe 'Test coverage of the Project Import' do
project.issues.notes.events
project.issues.notes.events.push_event_payload
project.issues.milestone.events.push_event_payload
+ project.issues.issuable_sla
project.issues.issue_milestones
project.issues.issue_milestones.milestone
project.issues.resource_label_events.label.priorities
diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
index a347d835428..e208a1c383c 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
@@ -102,4 +102,14 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do
end
end
end
+
+ describe '#clear_consumed_relations' do
+ let(:dir_path) { fixture }
+
+ subject { ndjson_reader.clear_consumed_relations }
+
+ it 'returns empty set' do
+ expect(subject).to be_empty
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index db76eb9538b..55b4f7479b8 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -74,14 +74,6 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
}
)
end
-
- it 'does not save a json file if feature is disabled' do
- stub_feature_flags(export_lfs_objects_projects: false)
-
- saver.save
-
- expect(File.exist?(lfs_json_file)).to eq(false)
- end
end
end
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index 31cf2362628..50bc6a30044 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -3,19 +3,22 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
- let(:group) { create(:group) }
+ let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
let(:members_mapper) { double('members_mapper').as_null_object }
- let(:user) { create(:admin) }
+ let(:admin) { create(:admin) }
+ let(:importer_user) { admin }
let(:excluded_keys) { [] }
let(:created_object) do
- described_class.create(relation_sym: relation_sym,
- relation_hash: relation_hash,
- object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
- members_mapper: members_mapper,
- user: user,
- importable: project,
- excluded_keys: excluded_keys)
+ described_class.create(
+ relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
+ members_mapper: members_mapper,
+ user: importer_user,
+ importable: project,
+ excluded_keys: excluded_keys
+ )
end
before do
@@ -113,9 +116,9 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
"created_at" => "2016-11-18T09:29:42.634Z",
"updated_at" => "2016-11-18T09:29:42.634Z",
"user" => {
- "id" => user.id,
- "email" => user.email,
- "username" => user.username
+ "id" => admin.id,
+ "email" => admin.email,
+ "username" => admin.username
}
}
end
@@ -123,7 +126,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
let(:members_mapper) do
Gitlab::ImportExport::MembersMapper.new(
exported_members: [exported_member],
- user: user,
+ user: importer_user,
importable: project)
end
@@ -134,9 +137,9 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
'source_branch' => "feature_conflict",
'source_project_id' => project.id,
'target_project_id' => project.id,
- 'author_id' => user.id,
- 'assignee_id' => user.id,
- 'updated_by_id' => user.id,
+ 'author_id' => admin.id,
+ 'assignee_id' => admin.id,
+ 'updated_by_id' => admin.id,
'title' => "MR1",
'created_at' => "2016-06-14T15:02:36.568Z",
'updated_at' => "2016-06-14T15:02:56.815Z",
@@ -151,11 +154,11 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
end
it 'has preloaded author' do
- expect(created_object.author).to equal(user)
+ expect(created_object.author).to equal(admin)
end
it 'has preloaded updated_by' do
- expect(created_object.updated_by).to equal(user)
+ expect(created_object.updated_by).to equal(admin)
end
it 'has preloaded source project' do
@@ -264,27 +267,8 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
end
end
- context 'Notes user references' do
- let(:relation_sym) { :notes }
- let(:new_user) { create(:user) }
- let(:exported_member) do
- {
- "id" => 111,
- "access_level" => 30,
- "source_id" => 1,
- "source_type" => "Project",
- "user_id" => 3,
- "notification_level" => 3,
- "created_at" => "2016-11-18T09:29:42.634Z",
- "updated_at" => "2016-11-18T09:29:42.634Z",
- "user" => {
- "id" => 999,
- "email" => new_user.email,
- "username" => new_user.username
- }
- }
- end
-
+ it_behaves_like 'Notes user references' do
+ let(:importable) { project }
let(:relation_hash) do
{
"id" => 4947,
@@ -305,17 +289,6 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
"events" => []
}
end
-
- let(:members_mapper) do
- Gitlab::ImportExport::MembersMapper.new(
- exported_members: [exported_member],
- user: user,
- importable: project)
- end
-
- it 'maps the right author to the imported note' do
- expect(created_object.author).to eq(new_user)
- end
end
context 'encrypted attributes' do
diff --git a/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb
new file mode 100644
index 00000000000..82f59245519
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::Project::Sample::DateCalculator do
+ describe '#closest date to average' do
+ subject { described_class.new(dates).closest_date_to_average }
+
+ context 'when dates are empty' do
+ let(:dates) { [] }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when dates are not empty' do
+ let(:dates) { [[nil, '2020-01-01 00:00:00 +0000'], [nil, '2021-01-01 00:00:00 +0000'], [nil, '2022-01-01 23:59:59 +0000']] }
+
+ it { is_expected.to eq(Time.zone.parse('2021-01-01 00:00:00 +0000')) }
+ end
+ end
+
+ describe '#calculate_by_closest_date_to_average' do
+ let(:calculator) { described_class.new([]) }
+ let(:date) { Time.current }
+
+ subject { calculator.calculate_by_closest_date_to_average(date) }
+
+ context 'when average date is nil' do
+ before do
+ allow(calculator).to receive(:closest_date_to_average).and_return(nil)
+ end
+
+ it { is_expected.to eq(date) }
+ end
+
+ context 'when average date is in the past' do
+ before do
+ allow(calculator).to receive(:closest_date_to_average).and_return(date - 365.days)
+ allow(Time).to receive(:current).and_return(date)
+ end
+
+ it { is_expected.to eq(date + 365.days) }
+ end
+
+ context 'when average date is in the future' do
+ before do
+ allow(calculator).to receive(:closest_date_to_average).and_return(date + 10.days)
+ end
+
+ it { is_expected.to eq(date) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb
new file mode 100644
index 00000000000..f173345a4c6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+# This spec is a lightweight version of:
+# * project/tree_restorer_spec.rb
+#
+# In depth testing is being done in the above specs.
+# This spec tests that restore of the sample project works
+# but does not have 100% relation coverage.
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer do
+ include_context 'relation tree restorer shared context'
+
+ let(:sample_data_relation_tree_restorer) do
+ described_class.new(
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: object_builder,
+ members_mapper: members_mapper,
+ relation_factory: relation_factory,
+ reader: reader,
+ importable: importable,
+ importable_path: importable_path,
+ importable_attributes: attributes
+ )
+ end
+
+ subject { sample_data_relation_tree_restorer.restore }
+
+ shared_examples 'import project successfully' do
+ it 'restores project tree' do
+ expect(subject).to eq(true)
+ end
+
+ describe 'imported project' do
+ let(:project) { Project.find_by_path('project') }
+
+ before do
+ subject
+ end
+
+ it 'has the project attributes and relations', :aggregate_failures do
+ expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
+ expect(project.issues.count).to eq(10)
+ expect(project.milestones.count).to eq(3)
+ expect(project.labels.count).to eq(2)
+ expect(project.project_feature).not_to be_nil
+ end
+
+ it 'has issues with correctly updated due dates' do
+ due_dates = due_dates(project.issues)
+
+ expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days])
+ end
+
+ it 'has milestones with correctly updated due dates' do
+ due_dates = due_dates(project.milestones)
+
+ expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days])
+ end
+
+ def due_dates(relations)
+ due_dates = relations.map { |relation| relation['due_date'] }
+ due_dates.compact!
+ due_dates.sort
+ end
+ end
+ end
+
+ context 'when restoring a project' do
+ let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:importable_name) { 'project' }
+ let(:importable_path) { 'project' }
+ let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
+ let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+
+ context 'using ndjson reader' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+
+ it_behaves_like 'import project successfully'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index f75494aa7c7..c05968c9a85 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -1040,6 +1040,41 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
it_behaves_like 'project tree restorer work properly', :legacy_reader, true
it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
+
+ context 'Sample Data JSON' do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+
+ before do
+ setup_import_export_config('sample_data')
+ setup_reader(:ndjson_reader)
+ end
+
+ context 'with sample_data_template' do
+ before do
+ allow(project).to receive_message_chain(:import_data, :data, :dig).with('sample_data') { true }
+ end
+
+ it 'initialize SampleDataRelationTreeRestorer' do
+ expect_next_instance_of(Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer) do |restorer|
+ expect(restorer).to receive(:restore).and_return(true)
+ end
+
+ expect(project_tree_restorer.restore).to eq(true)
+ end
+ end
+
+ context 'without sample_data_template' do
+ it 'initialize RelationTreeRestorer' do
+ expect_next_instance_of(Gitlab::ImportExport::RelationTreeRestorer) do |restorer|
+ expect(restorer).to receive(:restore).and_return(true)
+ end
+
+ expect(project_tree_restorer.restore).to eq(true)
+ end
+ end
+ end
end
context 'disable ndjson import' do
diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
index ddc96b83208..bd9ac6d6697 100644
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
@@ -10,15 +10,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
- include ImportExport::CommonUtil
-
- let(:user) { create(:user) }
- let(:shared) { Gitlab::ImportExport::Shared.new(importable) }
- let(:attributes) { relation_reader.consume_attributes(importable_name) }
-
- let(:members_mapper) do
- Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable)
- end
+ include_context 'relation tree restorer shared context'
let(:relation_tree_restorer) do
described_class.new(
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index ace4449042e..b32ae60fbcc 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -36,21 +36,20 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
expect(subject.restore).to be_truthy
end
- context 'when the repository creation fails' do
- before do
- allow_next_instance_of(Repositories::DestroyService) do |instance|
+ context 'when the repository already exists' do
+ it 'deletes the existing repository before importing' do
+ allow(project.repository).to receive(:exists?).and_return(true)
+ allow(project.repository).to receive(:path).and_return('repository_path')
+
+ expect_next_instance_of(Repositories::DestroyService) do |instance|
expect(instance).to receive(:execute).and_call_original
end
- end
-
- it 'logs the error' do
- allow(project.repository)
- .to receive(:create_from_bundle)
- .and_raise('9:CreateRepositoryFromBundle: target directory is non-empty')
- expect(shared).to receive(:error).and_call_original
+ expect(shared.logger).to receive(:info).with(
+ message: 'Deleting existing "repository_path" to re-import it.'
+ )
- expect(subject.restore).to be_falsey
+ expect(subject.restore).to be_truthy
end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 5ca7c5b7a91..e3d1f2c9368 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -855,3 +855,6 @@ ProjectSecuritySetting:
- auto_fix_sast
- created_at
- updated_at
+IssuableSla:
+ - issue_id
+ - due_at
diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb
index d96152e47ea..a6170c146ab 100644
--- a/spec/lib/gitlab/issuables_count_for_state_spec.rb
+++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb
@@ -4,14 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::IssuablesCountForState do
let(:finder) do
- double(:finder, count_by_state: { opened: 2, closed: 1 })
+ double(:finder, current_user: nil, params: {}, count_by_state: { opened: 2, closed: 1 })
end
- let(:counter) { described_class.new(finder) }
+ let(:project) { nil }
+ let(:fast_fail) { nil }
+ let(:counter) { described_class.new(finder, project, fast_fail: fast_fail) }
describe 'project given' do
let(:project) { build(:project) }
- let(:counter) { described_class.new(finder, project) }
it 'provides the project' do
expect(counter.project).to eq(project)
@@ -50,5 +51,19 @@ RSpec.describe Gitlab::IssuablesCountForState do
it 'returns 0 when using an invalid state name as a String' do
expect(counter['kittens']).to be_zero
end
+
+ context 'fast_fail enabled' do
+ let(:fast_fail) { true }
+
+ it 'returns the expected value' do
+ expect(counter[:closed]).to eq(1)
+ end
+
+ it 'returns -1 when the database times out' do
+ expect(finder).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
+
+ expect(counter[:closed]).to eq(-1)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index 7aa0a3485fb..a9edb2b530b 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -2,23 +2,26 @@
require 'spec_helper'
-RSpec.describe Gitlab::JobWaiter do
+RSpec.describe Gitlab::JobWaiter, :redis do
describe '.notify' do
it 'pushes the jid to the named queue' do
- key = 'gitlab:job_waiter:foo'
- jid = 1
+ key = described_class.new.key
- redis = double('redis')
- expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
- expect(redis).to receive(:lpush).with(key, jid)
+ described_class.notify(key, 123)
- described_class.notify(key, jid)
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.ttl(key)).to be > 0
+ end
end
end
describe '#wait' do
let(:waiter) { described_class.new(2) }
+ before do
+ allow_any_instance_of(described_class).to receive(:wait).and_call_original
+ end
+
it 'returns when all jobs have been completed' do
described_class.notify(waiter.key, 'a')
described_class.notify(waiter.key, 'b')
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 90c11f29855..7b6d143dda9 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
let(:api_url) { 'https://kubernetes.example.com/prefix' }
let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } }
- let(:client) { described_class.new(api_url, kubeclient_options) }
+ let(:client) { described_class.new(api_url, **kubeclient_options) }
before do
stub_kubeclient_discover(api_url)
@@ -133,7 +133,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
end
it 'falls back to default options, but allows overriding' do
- client = Gitlab::Kubernetes::KubeClient.new(api_url, {})
+ client = described_class.new(api_url)
defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS
expect(client.kubeclient_options[:timeouts]).to eq(defaults[:timeouts])
@@ -347,6 +347,34 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '#get_ingresses' do
+ let(:extensions_client) { client.extensions_client }
+ let(:networking_client) { client.networking_client }
+
+ include_examples 'redirection not allowed', 'get_ingresses'
+ include_examples 'dns rebinding not allowed', 'get_ingresses'
+
+ it 'delegates to the extensions client' do
+ expect(extensions_client).to receive(:get_ingresses)
+
+ client.get_ingresses
+ end
+
+ context 'extensions does not have deployments for Kubernetes 1.22+ clusters' do
+ before do
+ WebMock
+ .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+ .to_return(kube_response(kube_1_22_extensions_v1beta1_discovery_body))
+ end
+
+ it 'delegates to the apps client' do
+ expect(networking_client).to receive(:get_ingresses)
+
+ client.get_ingresses
+ end
+ end
+ end
+
describe 'istio API group' do
let(:istio_client) { client.istio_client }
diff --git a/spec/lib/gitlab/lfs/client_spec.rb b/spec/lib/gitlab/lfs/client_spec.rb
index 03563a632d6..1c50a2a7500 100644
--- a/spec/lib/gitlab/lfs/client_spec.rb
+++ b/spec/lib/gitlab/lfs/client_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Gitlab::Lfs::Client do
let(:username) { 'user' }
let(:password) { 'password' }
let(:credentials) { { user: username, password: password, auth_method: 'password' } }
+ let(:git_lfs_content_type) { 'application/vnd.git-lfs+json' }
+ let(:git_lfs_user_agent) { "GitLab #{Gitlab::VERSION} LFS client" }
let(:basic_auth_headers) do
{ 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}" }
@@ -21,6 +23,18 @@ RSpec.describe Gitlab::Lfs::Client do
}
end
+ let(:verify_action) do
+ {
+ "href" => "#{base_url}/some/file/verify",
+ "header" => {
+ "Key" => "value"
+ }
+ }
+ end
+
+ let(:authorized_upload_action) { upload_action.tap { |action| action['header']['Authorization'] = 'foo' } }
+ let(:authorized_verify_action) { verify_action.tap { |action| action['header']['Authorization'] = 'foo' } }
+
subject(:lfs_client) { described_class.new(base_url, credentials: credentials) }
describe '#batch' do
@@ -34,10 +48,10 @@ RSpec.describe Gitlab::Lfs::Client do
).to_return(
status: 200,
body: { 'objects' => 'anything', 'transfer' => 'basic' }.to_json,
- headers: { 'Content-Type' => 'application/vnd.git-lfs+json' }
+ headers: { 'Content-Type' => git_lfs_content_type }
)
- result = lfs_client.batch('upload', objects)
+ result = lfs_client.batch!('upload', objects)
expect(stub).to have_been_requested
expect(result).to eq('objects' => 'anything', 'transfer' => 'basic')
@@ -48,7 +62,7 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400)
- expect { lfs_client.batch('upload', objects) }.to raise_error(/Failed/)
+ expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/)
end
end
@@ -56,7 +70,7 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400)
- expect { lfs_client.batch('upload', objects) }.to raise_error(/Failed/)
+ expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/)
end
end
@@ -68,17 +82,23 @@ RSpec.describe Gitlab::Lfs::Client do
).to_return(
status: 200,
body: { 'transfer' => 'carrier-pigeon' }.to_json,
- headers: { 'Content-Type' => 'application/vnd.git-lfs+json' }
+ headers: { 'Content-Type' => git_lfs_content_type }
)
- expect { lfs_client.batch('upload', objects) }.to raise_error(/Unsupported transfer/)
+ expect { lfs_client.batch!('upload', objects) }.to raise_error(/Unsupported transfer/)
end
end
def stub_batch(objects:, headers:, operation: 'upload', transfer: 'basic')
- objects = objects.map { |o| { oid: o.oid, size: o.size } }
+ objects = objects.as_json(only: [:oid, :size])
body = { operation: operation, 'transfers': [transfer], objects: objects }.to_json
+ headers = {
+ 'Accept' => git_lfs_content_type,
+ 'Content-Type' => git_lfs_content_type,
+ 'User-Agent' => git_lfs_user_agent
+ }.merge(headers)
+
stub_request(:post, base_url + '/info/lfs/objects/batch').with(body: body, headers: headers)
end
end
@@ -90,7 +110,7 @@ RSpec.describe Gitlab::Lfs::Client do
it "makes an HTTP PUT with expected parameters" do
stub_upload(object: object, headers: upload_action['header']).to_return(status: 200)
- lfs_client.upload(object, upload_action, authenticated: true)
+ lfs_client.upload!(object, upload_action, authenticated: true)
end
end
@@ -101,7 +121,20 @@ RSpec.describe Gitlab::Lfs::Client do
headers: basic_auth_headers.merge(upload_action['header'])
).to_return(status: 200)
- lfs_client.upload(object, upload_action, authenticated: false)
+ lfs_client.upload!(object, upload_action, authenticated: false)
+
+ expect(stub).to have_been_requested
+ end
+ end
+
+ context 'request is not marked as authenticated but includes an authorization header' do
+ it 'prefers the provided authorization header' do
+ stub = stub_upload(
+ object: object,
+ headers: authorized_upload_action['header']
+ ).to_return(status: 200)
+
+ lfs_client.upload!(object, authorized_upload_action, authenticated: false)
expect(stub).to have_been_requested
end
@@ -110,13 +143,13 @@ RSpec.describe Gitlab::Lfs::Client do
context 'LFS object has no file' do
let(:object) { LfsObject.new }
- it 'makes an HJTT PUT with expected parameters' do
+ it 'makes an HTTP PUT with expected parameters' do
stub = stub_upload(
object: object,
headers: upload_action['header']
).to_return(status: 200)
- lfs_client.upload(object, upload_action, authenticated: true)
+ lfs_client.upload!(object, upload_action, authenticated: true)
expect(stub).to have_been_requested
end
@@ -126,7 +159,7 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_upload(object: object, headers: upload_action['header']).to_return(status: 400)
- expect { lfs_client.upload(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
end
end
@@ -134,15 +167,88 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_upload(object: object, headers: upload_action['header']).to_return(status: 500)
- expect { lfs_client.upload(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
end
end
def stub_upload(object:, headers:)
+ headers = {
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Length' => object.size.to_s,
+ 'User-Agent' => git_lfs_user_agent
+ }.merge(headers)
+
stub_request(:put, upload_action['href']).with(
body: object.file.read,
headers: headers.merge('Content-Length' => object.size.to_s)
)
end
end
+
+ describe "#verify" do
+ let_it_be(:object) { create(:lfs_object) }
+
+ context 'server returns 200 OK to an authenticated request' do
+ it "makes an HTTP POST with expected parameters" do
+ stub_verify(object: object, headers: verify_action['header']).to_return(status: 200)
+
+ lfs_client.verify!(object, verify_action, authenticated: true)
+ end
+ end
+
+ context 'server returns 200 OK to an unauthenticated request' do
+ it "makes an HTTP POST with expected parameters" do
+ stub = stub_verify(
+ object: object,
+ headers: basic_auth_headers.merge(upload_action['header'])
+ ).to_return(status: 200)
+
+ lfs_client.verify!(object, verify_action, authenticated: false)
+
+ expect(stub).to have_been_requested
+ end
+ end
+
+ context 'request is not marked as authenticated but includes an authorization header' do
+ it 'prefers the provided authorization header' do
+ stub = stub_verify(
+ object: object,
+ headers: authorized_verify_action['header']
+ ).to_return(status: 200)
+
+ lfs_client.verify!(object, authorized_verify_action, authenticated: false)
+
+ expect(stub).to have_been_requested
+ end
+ end
+
+ context 'server returns 400 error' do
+ it 'raises an error' do
+ stub_verify(object: object, headers: verify_action['header']).to_return(status: 400)
+
+ expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/)
+ end
+ end
+
+ context 'server returns 500 error' do
+ it 'raises an error' do
+ stub_verify(object: object, headers: verify_action['header']).to_return(status: 500)
+
+ expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/)
+ end
+ end
+
+ def stub_verify(object:, headers:)
+ headers = {
+ 'Accept' => git_lfs_content_type,
+ 'Content-Type' => git_lfs_content_type,
+ 'User-Agent' => git_lfs_user_agent
+ }.merge(headers)
+
+ stub_request(:post, verify_action['href']).with(
+ body: object.to_json(only: [:oid, :size]),
+ headers: headers
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 9b8b2c1417a..4b40e8960b2 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
# Needs to be at least LfsToken::DEFAULT_EXPIRE_TIME + 60 seconds
# in order to check whether it is valid 1 minute after it has expired
- Timecop.freeze(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do
+ travel_to(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do
expect(lfs_token.token_valid?(expired_token)).to be false
end
end
diff --git a/spec/lib/gitlab/manifest_import/manifest_spec.rb b/spec/lib/gitlab/manifest_import/manifest_spec.rb
index 2e8753b0880..352120c079d 100644
--- a/spec/lib/gitlab/manifest_import/manifest_spec.rb
+++ b/spec/lib/gitlab/manifest_import/manifest_spec.rb
@@ -12,19 +12,7 @@ RSpec.describe Gitlab::ManifestImport::Manifest do
end
context 'missing or invalid attributes' do
- let(:file) { Tempfile.new('foo') }
-
- before do
- content = <<~EOS
- <manifest>
- <remote review="invalid-url" />
- <project name="platform/build"/>
- </manifest>
- EOS
-
- file.write(content)
- file.rewind
- end
+ let(:file) { File.open(Rails.root.join('spec/fixtures/invalid_manifest.xml')) }
it { expect(manifest.valid?).to be false }
diff --git a/spec/lib/gitlab/manifest_import/metadata_spec.rb b/spec/lib/gitlab/manifest_import/metadata_spec.rb
new file mode 100644
index 00000000000..c8158d3e148
--- /dev/null
+++ b/spec/lib/gitlab/manifest_import/metadata_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_state do
+ let(:user) { double(id: 1) }
+ let(:repositories) do
+ [
+ { id: 'test1', url: 'http://demo.host/test1' },
+ { id: 'test2', url: 'http://demo.host/test2' }
+ ]
+ end
+
+ describe '#save' do
+ it 'stores data in Redis with an expiry of EXPIRY_TIME' do
+ status = described_class.new(user)
+ repositories_key = 'manifest_import:metadata:user:1:repositories'
+ group_id_key = 'manifest_import:metadata:user:1:group_id'
+
+ status.save(repositories, 2)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.ttl(repositories_key)).to be_within(5).of(described_class::EXPIRY_TIME)
+ expect(redis.ttl(group_id_key)).to be_within(5).of(described_class::EXPIRY_TIME)
+ end
+ end
+ end
+
+ describe '#repositories' do
+ it 'allows repositories to round-trip with symbol keys' do
+ status = described_class.new(user)
+
+ status.save(repositories, 2)
+
+ expect(status.repositories).to eq(repositories)
+ end
+
+ it 'uses the fallback when there is nothing in Redis' do
+ fallback = { manifest_import_repositories: repositories }
+ status = described_class.new(user, fallback: fallback)
+
+ expect(status.repositories).to eq(repositories)
+ end
+ end
+
+ describe '#group_id' do
+ it 'returns the group ID as an integer' do
+ status = described_class.new(user)
+
+ status.save(repositories, 2)
+
+ expect(status.group_id).to eq(2)
+ end
+
+ it 'uses the fallback when there is nothing in Redis' do
+ fallback = { manifest_import_group_id: 3 }
+ status = described_class.new(user, fallback: fallback)
+
+ expect(status.group_id).to eq(3)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
index 09d5e048f6a..ff8f5797f9d 100644
--- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb
@@ -8,9 +8,16 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
describe '#execute' do
let(:project) { create(:project) }
let(:dashboard_path) { 'path/to/dashboard.yml' }
+ let(:prometheus_adapter) { double('adapter', clear_prometheus_reactive_cache!: nil) }
subject { described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path) }
+ before do
+ allow_next_instance_of(::Clusters::Applications::ScheduleUpdateService) do |update_service|
+ allow(update_service).to receive(:execute)
+ end
+ end
+
context 'valid dashboard' do
let(:dashboard_hash) { load_sample_dashboard }
@@ -21,20 +28,32 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
end
context 'with existing metrics' do
+ let(:existing_metric_attributes) do
+ {
+ project: project,
+ identifier: 'metric_b',
+ title: 'overwrite',
+ y_label: 'overwrite',
+ query: 'overwrite',
+ unit: 'overwrite',
+ legend: 'overwrite',
+ dashboard_path: dashboard_path
+ }
+ end
+
let!(:existing_metric) do
- create(:prometheus_metric, {
- project: project,
- identifier: 'metric_b',
- title: 'overwrite',
- y_label: 'overwrite',
- query: 'overwrite',
- unit: 'overwrite',
- legend: 'overwrite'
- })
+ create(:prometheus_metric, existing_metric_attributes)
+ end
+
+ let!(:existing_alert) do
+ alert = create(:prometheus_alert, project: project, prometheus_metric: existing_metric)
+ existing_metric.prometheus_alerts << alert
+
+ alert
end
it 'updates existing PrometheusMetrics' do
- described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute
+ subject.execute
expect(existing_metric.reload.attributes.with_indifferent_access).to include({
title: 'Super Chart B',
@@ -49,6 +68,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
expect { subject.execute }.to change { PrometheusMetric.count }.by(2)
end
+ it 'updates affected environments' do
+ expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with(
+ existing_alert.environment.cluster_prometheus_adapter,
+ project
+ ).and_return(double('ScheduleUpdateService', execute: true))
+
+ subject.execute
+ end
+
context 'with stale metrics' do
let!(:stale_metric) do
create(:prometheus_metric,
@@ -59,11 +87,45 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
)
end
+ let!(:stale_alert) do
+ alert = create(:prometheus_alert, project: project, prometheus_metric: stale_metric)
+ stale_metric.prometheus_alerts << alert
+
+ alert
+ end
+
+ it 'updates existing PrometheusMetrics' do
+ subject.execute
+
+ expect(existing_metric.reload.attributes.with_indifferent_access).to include({
+ title: 'Super Chart B',
+ y_label: 'y_label',
+ query: 'query',
+ unit: 'unit',
+ legend: 'Legend Label'
+ })
+ end
+
it 'deletes stale metrics' do
subject.execute
expect { stale_metric.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
+
+ it 'deletes stale alert' do
+ subject.execute
+
+ expect { stale_alert.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'updates affected environments' do
+ expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with(
+ existing_alert.environment.cluster_prometheus_adapter,
+ project
+ ).and_return(double('ScheduleUpdateService', execute: true))
+
+ subject.execute
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index 69b779d36eb..631325402d9 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
end
it 'increments requests count' do
- expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get')
+ expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'unknown')
subject.call(env)
end
@@ -32,75 +32,55 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
end
it 'measures execution time' do
- expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: '200', method: 'get' }, a_positive_execution_time)
+ expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
Timecop.scale(3600) { subject.call(env) }
end
context 'request is a health check endpoint' do
- it 'increments health endpoint counter' do
- env['PATH_INFO'] = '/-/liveness'
+ ['/-/liveness', '/-/liveness/', '/-/%6D%65%74%72%69%63%73'].each do |path|
+ context "when path is #{path}" do
+ before do
+ env['PATH_INFO'] = path
+ end
- expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get')
+ it 'increments health endpoint counter rather than overall counter' do
+ expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get', status: 200)
+ expect(described_class).not_to receive(:http_request_total)
- subject.call(env)
- end
-
- context 'with trailing slash' do
- before do
- env['PATH_INFO'] = '/-/liveness/'
- end
-
- it 'increments health endpoint counter' do
- expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get')
-
- subject.call(env)
- end
- end
-
- context 'with percent encoded values' do
- before do
- env['PATH_INFO'] = '/-/%6D%65%74%72%69%63%73' # /-/metrics
- end
+ subject.call(env)
+ end
- it 'increments health endpoint counter' do
- expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get')
+ it 'does not record the request duration' do
+ expect(described_class).not_to receive(:http_request_duration_seconds)
- subject.call(env)
+ subject.call(env)
+ end
end
end
end
context 'request is not a health check endpoint' do
- it 'does not increment health endpoint counter' do
- env['PATH_INFO'] = '/-/ordinary-requests'
-
- expect(described_class).not_to receive(:http_health_requests_total)
-
- subject.call(env)
- end
-
- context 'path info is a root path' do
- before do
- env['PATH_INFO'] = '/-/'
- end
-
- it 'does not increment health endpoint counter' do
- expect(described_class).not_to receive(:http_health_requests_total)
-
- subject.call(env)
- end
- end
-
- context 'path info is a subpath' do
- before do
- env['PATH_INFO'] = '/-/health/subpath'
- end
-
- it 'does not increment health endpoint counter' do
- expect(described_class).not_to receive(:http_health_requests_total)
-
- subject.call(env)
+ ['/-/ordinary-requests', '/-/', '/-/health/subpath'].each do |path|
+ context "when path is #{path}" do
+ before do
+ env['PATH_INFO'] = path
+ end
+
+ it 'increments overall counter rather than health endpoint counter' do
+ expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'unknown')
+ expect(described_class).not_to receive(:http_health_requests_total)
+
+ subject.call(env)
+ end
+
+ it 'records the request duration' do
+ expect(described_class)
+ .to receive_message_chain(:http_request_duration_seconds, :observe)
+ .with({ method: 'get' }, a_positive_execution_time)
+
+ subject.call(env)
+ end
end
end
end
@@ -121,7 +101,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
end
it 'increments requests count' do
- expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get')
+ expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'unknown')
expect { subject.call(env) }.to raise_error(StandardError)
end
@@ -133,13 +113,32 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
end
end
+ context 'when a feature category header is present' do
+ before do
+ allow(app).to receive(:call).and_return([200, { described_class::FEATURE_CATEGORY_HEADER => 'issue_tracking' }, nil])
+ end
+
+ it 'adds the feature category to the labels for http_request_total' do
+ expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'issue_tracking')
+
+ subject.call(env)
+ end
+
+ it 'does not record a feature category for health check endpoints' do
+ env['PATH_INFO'] = '/-/liveness'
+
+ expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get', status: 200)
+ expect(described_class).not_to receive(:http_request_total)
+
+ subject.call(env)
+ end
+ end
+
describe '.initialize_http_request_duration_seconds' do
it "sets labels" do
expected_labels = []
- described_class::HTTP_METHODS.each do |method, statuses|
- statuses.each do |status|
- expected_labels << { method: method, status: status.to_s }
- end
+ described_class::HTTP_METHODS.each do |method|
+ expected_labels << { method: method }
end
described_class.initialize_http_request_duration_seconds
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 1fffef53a82..7bac041cd65 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -135,6 +135,17 @@ RSpec.describe Gitlab::Middleware::Go do
it_behaves_like 'unauthorized'
end
+
+ context 'with a blacklisted ip' do
+ it 'returns forbidden' do
+ expect(Gitlab::Auth).to receive(:find_for_git_client).and_raise(Gitlab::Auth::IpBlacklisted)
+ response = go
+
+ expect(response[0]).to eq(403)
+ expect(response[1]['Content-Length']).to be_nil
+ expect(response[2]).to eq([''])
+ end
+ end
end
end
end
@@ -176,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do
it 'returns 404' do
response = go
+
expect(response[0]).to eq(404)
expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>}
- expect(response[2].body).to eq([expected_body])
+ expect(response[2]).to eq([expected_body])
end
end
@@ -251,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>}
- expect(response[2].body).to eq([expected_body])
+ expect(response[2]).to eq([expected_body])
end
end
end
diff --git a/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb b/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb
new file mode 100644
index 00000000000..76a5174817e
--- /dev/null
+++ b/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require "rack/test"
+
+RSpec.describe Gitlab::Middleware::HandleNullBytes do
+ let(:null_byte) { "\u0000" }
+ let(:error_400) { [400, {}, ["Bad Request"]] }
+ let(:app) { double(:app) }
+
+ subject { described_class.new(app) }
+
+ before do
+ allow(app).to receive(:call) do |args|
+ args
+ end
+ end
+
+ def env_for(params = {})
+ Rack::MockRequest.env_for('/', { params: params })
+ end
+
+ context 'with null bytes in params' do
+ it 'rejects null bytes in a top level param' do
+ env = env_for(name: "null#{null_byte}byte")
+
+ expect(subject.call(env)).to eq error_400
+ end
+
+ it "responds with 400 BadRequest for hashes with strings" do
+ env = env_for(name: { inner_key: "I am #{null_byte} bad" })
+
+ expect(subject.call(env)).to eq error_400
+ end
+
+ it "responds with 400 BadRequest for arrays with strings" do
+ env = env_for(name: ["I am #{null_byte} bad"])
+
+ expect(subject.call(env)).to eq error_400
+ end
+
+ it "responds with 400 BadRequest for arrays containing hashes with string values" do
+ env = env_for(name: [
+ {
+ inner_key: "I am #{null_byte} bad"
+ }
+ ])
+
+ expect(subject.call(env)).to eq error_400
+ end
+
+ it "gives up and does not 400 with too deeply nested params" do
+ env = env_for(name: [
+ {
+ inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{null_byte} bad" }] }
+ }
+ ])
+
+ expect(subject.call(env)).not_to eq error_400
+ end
+ end
+
+ context 'without null bytes in params' do
+ it "does not respond with a 400 for strings" do
+ env = env_for(name: "safe name")
+
+ expect(subject.call(env)).not_to eq error_400
+ end
+
+ it "does not respond with a 400 with no params" do
+ env = env_for
+
+ expect(subject.call(env)).not_to eq error_400
+ end
+ end
+
+ context 'when disabled via env flag' do
+ before do
+ stub_env('REJECT_NULL_BYTES', '1')
+ end
+
+ it 'does not respond with a 400 no matter what' do
+ env = env_for(name: "null#{null_byte}byte")
+
+ expect(subject.call(env)).not_to eq error_400
+ end
+ end
+end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index cdb48024531..a9dae72f4db 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Middleware::RailsQueueDuration do
expect(transaction).to receive(:observe).with(:gitlab_rails_queue_duration_seconds, 1)
- Timecop.freeze(Time.at(3)) do
+ travel_to(Time.at(3)) do
expect(middleware.call(env)).to eq('yay')
end
end
diff --git a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb
index 2d1a9b2eee2..18342fd78ac 100644
--- a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb
+++ b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb
@@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do
end
context 'with no cookies' do
- let(:cookies) { nil }
+ let(:cookies) { "" }
it 'does not add headers' do
response = do_request
- expect(response['Set-Cookie']).to be_nil
+ expect(response['Set-Cookie']).to eq("")
end
end
diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
index be20f0194f7..c9a23170137 100644
--- a/spec/lib/gitlab/pagination/offset_pagination_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
let(:request_context) { double("request_context") }
- subject do
+ subject(:paginator) do
described_class.new(request_context)
end
@@ -119,6 +119,34 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
subject.paginate(resource)
end
end
+
+ it 'does not return the total headers when excluding them' do
+ expect_no_header('X-Total')
+ expect_no_header('X-Total-Pages')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+
+ paginator.paginate(resource, exclude_total_headers: true)
+ end
+ end
+
+ context 'when resource is a paginatable array' do
+ let(:resource) { Kaminari.paginate_array(Project.all.to_a) }
+
+ it_behaves_like 'response with pagination headers'
+
+ it 'only returns the requested resources' do
+ expect(paginator.paginate(resource).count).to eq(2)
+ end
+
+ it 'does not return total headers when excluding them' do
+ expect_no_header('X-Total')
+ expect_no_header('X-Total-Pages')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+
+ paginator.paginate(resource, exclude_total_headers: true)
+ end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index fe0735b8043..a76ad1f6f4c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -265,9 +265,15 @@ RSpec.describe Gitlab::ProjectSearchResults do
let_it_be(:project) { create(:project, :public) }
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
+ let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
let(:query) { 'foo' }
+ before do
+ project.add_developer(user)
+ end
+
include_examples 'search results filtered by state'
+ include_examples 'search results filtered by confidential'
end
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index fa45c605b1b..98bd2efdbc6 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -8,9 +8,9 @@ RSpec.describe Gitlab::ProjectTemplate do
expected = %w[
rails spring express iosswift dotnetcore android
gomicro gatsby hugo jekyll plainhtml gitbook
- hexo sse_middleman nfhugo nfjekyll nfplainhtml
- nfgitbook nfhexo salesforcedx serverless_framework
- jsonnet cluster_management
+ hexo sse_middleman gitpod_spring_petclinic nfhugo
+ nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
+ serverless_framework jsonnet cluster_management
]
expect(described_class.all).to be_an(Array)
diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb
index 8abc944eeb1..b2350eff9f9 100644
--- a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do
around do |example|
- Timecop.freeze(Time.local(2008, 9, 1, 12, 0, 0)) { example.run }
+ travel_to(Time.local(2008, 9, 1, 12, 0, 0)) { example.run }
end
include_examples 'additional metrics query' do
diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
index 4683c4eae28..66b93d0dd72 100644
--- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Prometheus::Queries::DeploymentQuery do
around do |example|
time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0)
- Timecop.freeze(time_without_subsecond_values) { example.run }
+ travel_to(time_without_subsecond_values) { example.run }
end
it 'sends appropriate queries to prometheus' do
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index 1422d48152a..1dbdb892a5d 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -4,12 +4,12 @@ require 'spec_helper'
RSpec.describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
+ let_it_be_with_refind(:environment) { create(:environment) }
let(:project) { environment.project }
- let(:environment) { create(:environment) }
let(:slug) { environment.slug }
let(:params) { {} }
- subject { described_class.call(environment, params) }
+ subject { described_class.call(environment, **params) }
it { is_expected.to include(ci_environment_slug: slug) }
it { is_expected.to include(ci_project_name: project.name) }
diff --git a/spec/lib/gitlab/redis/hll_spec.rb b/spec/lib/gitlab/redis/hll_spec.rb
index cbf78f23036..e452e5b2f52 100644
--- a/spec/lib/gitlab/redis/hll_spec.rb
+++ b/spec/lib/gitlab/redis/hll_spec.rb
@@ -39,6 +39,24 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do
end
end
end
+
+ context 'when adding entries' do
+ let(:metric) { 'test-{metric}' }
+
+ it 'supports single value' do
+ track_event(metric, 1)
+
+ expect(count_unique_events([metric])).to eq(1)
+ end
+
+ it 'supports multiple values' do
+ stub_const("#{described_class.name}::HLL_BATCH_SIZE", 2)
+
+ track_event(metric, [1, 2, 3, 4, 5])
+
+ expect(count_unique_events([metric])).to eq(5)
+ end
+ end
end
describe '.count' do
@@ -94,13 +112,13 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do
expect(unique_counts).to eq(4)
end
+ end
- def track_event(key, value, expiry = 1.day)
- described_class.add(key: key, value: value, expiry: expiry)
- end
+ def track_event(key, value, expiry = 1.day)
+ described_class.add(key: key, value: value, expiry: expiry)
+ end
- def count_unique_events(keys)
- described_class.count(keys: keys)
- end
+ def count_unique_events(keys)
+ described_class.count(keys: keys)
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 88c3315150b..1c56e489a94 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -99,6 +99,36 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('foo-') }
end
+ describe '.build_trace_section_regex' do
+ subject { described_class.build_trace_section_regex }
+
+ context 'without options' do
+ example = "section_start:1600445393032:NAME\r\033\[0K"
+
+ it { is_expected.to match(example) }
+ it { is_expected.to match("section_end:12345678:aBcDeFg1234\r\033\[0K") }
+ it { is_expected.to match("section_start:0:sect_for_alpha-v1.0\r\033\[0K") }
+ it { is_expected.not_to match("section_start:section:0\r\033\[0K") }
+ it { is_expected.not_to match("section_:1600445393032:NAME\r\033\[0K") }
+ it { is_expected.not_to match(example.upcase) }
+ end
+
+ context 'with options' do
+ it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true]\r\033\[0K") }
+ it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true, example_option=false]\r\033\[0K") }
+ it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true,example_option=false]\r\033\[0K") }
+ it { is_expected.to match("section_start:1600445393032:NAME[numeric_option=1234567]\r\033\[0K") }
+ # Without splitting the regex in one for start and one for end,
+ # this is possible, however, it is ignored for section_end.
+ it { is_expected.to match("section_end:1600445393032:NAME[collapsed=true]\r\033\[0K") }
+ it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed=[]]]\r\033\[0K") }
+ it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed = true]\r\033\[0K") }
+ it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed = true, example_option=false]\r\033\[0K") }
+ it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed=true, example_option=false]\r\033\[0K") }
+ it { is_expected.not_to match("section_start:1600445393032:NAME[]\r\033\[0K") }
+ end
+ end
+
describe '.container_repository_name_regex' do
subject { described_class.container_repository_name_regex }
@@ -317,6 +347,22 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
+ describe '.nuget_version_regex' do
+ subject { described_class.nuget_version_regex }
+
+ it { is_expected.to match('1.2.3') }
+ it { is_expected.to match('1.2.3.4') }
+ it { is_expected.to match('1.2.3.4-stable.1') }
+ it { is_expected.to match('1.2.3-beta') }
+ it { is_expected.to match('1.2.3-alpha.3') }
+ it { is_expected.to match('1.0.7+r3456') }
+ it { is_expected.not_to match('1') }
+ it { is_expected.not_to match('1.2') }
+ it { is_expected.not_to match('1./2.3') }
+ it { is_expected.not_to match('../../../../../1.2.3') }
+ it { is_expected.not_to match('%2e%2e%2f1.2.3') }
+ end
+
describe '.pypi_version_regex' do
subject { described_class.pypi_version_regex }
@@ -384,6 +430,140 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
+ describe '.debian_package_name_regex' do
+ subject { described_class.debian_package_name_regex }
+
+ it { is_expected.to match('0ad') }
+ it { is_expected.to match('g++') }
+ it { is_expected.to match('lua5.1') }
+ it { is_expected.to match('samba') }
+
+ # may not be empty string
+ it { is_expected.not_to match('') }
+ # must start with an alphanumeric character
+ it { is_expected.not_to match('-a') }
+ it { is_expected.not_to match('+a') }
+ it { is_expected.not_to match('.a') }
+ it { is_expected.not_to match('_a') }
+ # only letters, digits and characters '-+._'
+ it { is_expected.not_to match('a~') }
+ it { is_expected.not_to match('aé') }
+
+ # More strict Lintian regex
+ # at least 2 chars
+ it { is_expected.not_to match('a') }
+ # lowercase only
+ it { is_expected.not_to match('Aa') }
+ it { is_expected.not_to match('aA') }
+ # No underscore
+ it { is_expected.not_to match('a_b') }
+ end
+
+ describe '.debian_version_regex' do
+ subject { described_class.debian_version_regex }
+
+ context 'valid versions' do
+ it { is_expected.to match('1.0') }
+ it { is_expected.to match('1.0~alpha1') }
+ it { is_expected.to match('2:4.9.5+dfsg-5+deb10u1') }
+ end
+
+ context 'dpkg errors' do
+ # version string is empty
+ it { is_expected.not_to match('') }
+ # version string has embedded spaces
+ it { is_expected.not_to match('1 0') }
+ # epoch in version is empty
+ it { is_expected.not_to match(':1.0') }
+ # epoch in version is not number
+ it { is_expected.not_to match('a:1.0') }
+ # epoch in version is negative
+ it { is_expected.not_to match('-1:1.0') }
+ # epoch in version is too big
+ it { is_expected.not_to match('9999999999:1.0') }
+ # nothing after colon in version number
+ it { is_expected.not_to match('2:') }
+ # revision number is empty
+ # Note: we are less strict here
+ # it { is_expected.not_to match('1.0-') }
+ # version number is empty
+ it { is_expected.not_to match('-1') }
+ it { is_expected.not_to match('2:-1') }
+ end
+
+ context 'dpkg warnings' do
+ # version number does not start with digit
+ it { is_expected.not_to match('a') }
+ it { is_expected.not_to match('a1.0') }
+ # invalid character in version number
+ it { is_expected.not_to match('1_0') }
+ # invalid character in revision number
+ it { is_expected.not_to match('1.0-1_0') }
+ end
+
+ context 'dpkg accepts' do
+ # dpkg accepts leading or trailing space
+ it { is_expected.not_to match(' 1.0') }
+ it { is_expected.not_to match('1.0 ') }
+ # dpkg accepts multiple colons
+ it { is_expected.not_to match('1:2:3') }
+ end
+ end
+
+ describe '.debian_architecture_regex' do
+ subject { described_class.debian_architecture_regex }
+
+ it { is_expected.to match('amd64') }
+ it { is_expected.to match('kfreebsd-i386') }
+
+ # may not be empty string
+ it { is_expected.not_to match('') }
+ # must start with an alphanumeric
+ it { is_expected.not_to match('-a') }
+ it { is_expected.not_to match('+a') }
+ it { is_expected.not_to match('.a') }
+ it { is_expected.not_to match('_a') }
+ # only letters, digits and characters '-'
+ it { is_expected.not_to match('a+b') }
+ it { is_expected.not_to match('a.b') }
+ it { is_expected.not_to match('a_b') }
+ it { is_expected.not_to match('a~') }
+ it { is_expected.not_to match('aé') }
+
+ # More strict
+ # Enforce lowercase
+ it { is_expected.not_to match('AMD64') }
+ it { is_expected.not_to match('Amd64') }
+ it { is_expected.not_to match('aMD64') }
+ end
+
+ describe '.debian_distribution_regex' do
+ subject { described_class.debian_distribution_regex }
+
+ it { is_expected.to match('buster') }
+ it { is_expected.to match('buster-updates') }
+ it { is_expected.to match('Debian10.5') }
+
+ # Do not allow slash, even if this exists in the wild
+ it { is_expected.not_to match('jessie/updates') }
+
+ # Do not allow Unicode
+ it { is_expected.not_to match('hé') }
+ end
+
+ describe '.debian_component_regex' do
+ subject { described_class.debian_component_regex }
+
+ it { is_expected.to match('main') }
+ it { is_expected.to match('non-free') }
+
+ # Do not allow slash
+ it { is_expected.not_to match('non/free') }
+
+ # Do not allow Unicode
+ it { is_expected.not_to match('hé') }
+ end
+
describe '.semver_regex' do
subject { described_class.semver_regex }
@@ -434,4 +614,45 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
it { is_expected.not_to match('') }
end
+
+ describe '.generic_package_name_regex' do
+ subject { described_class.generic_package_name_regex }
+
+ it { is_expected.to match('123') }
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1') }
+ it { is_expected.not_to match('../../foo') }
+ it { is_expected.not_to match('..\..\foo') }
+ it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') }
+ it { is_expected.not_to match('$foo/bar') }
+ it { is_expected.not_to match('my file name') }
+ it { is_expected.not_to match('!!()()') }
+ end
+
+ describe '.generic_package_file_name_regex' do
+ subject { described_class.generic_package_file_name_regex }
+
+ it { is_expected.to match('123') }
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1.jar') }
+ it { is_expected.not_to match('../../foo') }
+ it { is_expected.not_to match('..\..\foo') }
+ it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') }
+ it { is_expected.not_to match('$foo/bar') }
+ it { is_expected.not_to match('my file name') }
+ it { is_expected.not_to match('!!()()') }
+ end
+
+ describe '.prefixed_semver_regex' do
+ subject { described_class.prefixed_semver_regex }
+
+ it { is_expected.to match('v1.2.3') }
+ it { is_expected.to match('v1.2.3-beta') }
+ it { is_expected.to match('v1.2.3-alpha.3') }
+ it { is_expected.not_to match('v1') }
+ it { is_expected.not_to match('v1.2') }
+ it { is_expected.not_to match('v1./2.3') }
+ it { is_expected.not_to match('v../../../../../1.2.3') }
+ it { is_expected.not_to match('v%2e%2e%2f1.2.3') }
+ end
end
diff --git a/spec/lib/gitlab/relative_positioning/mover_spec.rb b/spec/lib/gitlab/relative_positioning/mover_spec.rb
index c49230c2415..dafd34585a8 100644
--- a/spec/lib/gitlab/relative_positioning/mover_spec.rb
+++ b/spec/lib/gitlab/relative_positioning/mover_spec.rb
@@ -37,18 +37,11 @@ RSpec.describe RelativePositioning::Mover do
end
def set_positions(positions)
- vals = issues.zip(positions).map do |issue, pos|
- issue.relative_position = pos
- "(#{issue.id}, #{pos})"
- end.join(', ')
-
- Issue.connection.exec_query(<<~SQL, 'set-positions')
- WITH cte(cte_id, new_pos) AS (
- SELECT * FROM (VALUES #{vals}) as t (id, pos)
- )
- UPDATE issues SET relative_position = new_pos FROM cte WHERE id = cte_id
- ;
- SQL
+ mapping = issues.zip(positions).to_h do |issue, pos|
+ [issue, { relative_position: pos }]
+ end
+
+ ::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping)
end
def ids_in_position_order
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index 05f32459164..912efa6a5db 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe ::Gitlab::RepoPath do
end
it 'parses a full wiki project path' do
- expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, project, Gitlab::GlRepository::WIKI, nil])
+ expect(described_class.parse(project.wiki.repository.full_path)).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, nil])
end
it 'parses a personal snippet repository path' do
@@ -36,7 +36,7 @@ RSpec.describe ::Gitlab::RepoPath do
end
it 'parses a relative wiki path' do
- expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, nil])
+ expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, nil])
end
it 'parses a relative path starting with /' do
@@ -49,7 +49,7 @@ RSpec.describe ::Gitlab::RepoPath do
end
it 'parses a relative wiki path' do
- expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, redirect_route])
+ expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, redirect_route])
end
it 'parses a relative path starting with /' do
diff --git a/spec/lib/gitlab/repository_size_checker_spec.rb b/spec/lib/gitlab/repository_size_checker_spec.rb
index 9b2c02b1190..bd030d81d97 100644
--- a/spec/lib/gitlab/repository_size_checker_spec.rb
+++ b/spec/lib/gitlab/repository_size_checker_spec.rb
@@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::RepositorySizeChecker do
+ let_it_be(:namespace) { nil }
let(:current_size) { 0 }
let(:limit) { 50 }
let(:enabled) { true }
subject do
described_class.new(
- current_size_proc: -> { current_size },
- limit: limit,
+ current_size_proc: -> { current_size.megabytes },
+ limit: limit.megabytes,
+ namespace: namespace,
enabled: enabled
)
end
@@ -18,7 +20,7 @@ RSpec.describe Gitlab::RepositorySizeChecker do
describe '#enabled?' do
context 'when enabled' do
it 'returns true' do
- expect(subject.enabled?).to be_truthy
+ expect(subject.enabled?).to eq(true)
end
end
@@ -26,7 +28,7 @@ RSpec.describe Gitlab::RepositorySizeChecker do
let(:limit) { 0 }
it 'returns false' do
- expect(subject.enabled?).to be_falsey
+ expect(subject.enabled?).to eq(false)
end
end
end
@@ -35,59 +37,20 @@ RSpec.describe Gitlab::RepositorySizeChecker do
let(:current_size) { 49 }
it 'returns true when changes go over' do
- expect(subject.changes_will_exceed_size_limit?(2)).to be_truthy
+ expect(subject.changes_will_exceed_size_limit?(2.megabytes)).to eq(true)
end
it 'returns false when changes do not go over' do
- expect(subject.changes_will_exceed_size_limit?(1)).to be_falsey
+ expect(subject.changes_will_exceed_size_limit?(1.megabytes)).to eq(false)
end
end
describe '#above_size_limit?' do
- context 'when size is above the limit' do
- let(:current_size) { 100 }
-
- it 'returns true' do
- expect(subject.above_size_limit?).to be_truthy
- end
- end
-
- it 'returns false when not over the limit' do
- expect(subject.above_size_limit?).to be_falsey
- end
+ include_examples 'checker size above limit'
+ include_examples 'checker size not over limit'
end
describe '#exceeded_size' do
- context 'when current size is below or equal to the limit' do
- let(:current_size) { 50 }
-
- it 'returns zero' do
- expect(subject.exceeded_size).to eq(0)
- end
- end
-
- context 'when current size is over the limit' do
- let(:current_size) { 51 }
-
- it 'returns zero' do
- expect(subject.exceeded_size).to eq(1)
- end
- end
-
- context 'when change size will be over the limit' do
- let(:current_size) { 50 }
-
- it 'returns zero' do
- expect(subject.exceeded_size(1)).to eq(1)
- end
- end
-
- context 'when change size will not be over the limit' do
- let(:current_size) { 49 }
-
- it 'returns zero' do
- expect(subject.exceeded_size(1)).to eq(0)
- end
- end
+ include_examples 'checker size exceeded'
end
end
diff --git a/spec/lib/gitlab/repository_size_error_message_spec.rb b/spec/lib/gitlab/repository_size_error_message_spec.rb
index b6b975143c9..53b5ed5518f 100644
--- a/spec/lib/gitlab/repository_size_error_message_spec.rb
+++ b/spec/lib/gitlab/repository_size_error_message_spec.rb
@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::RepositorySizeErrorMessage do
+ let_it_be(:namespace) { build(:namespace) }
let(:checker) do
Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { 15.megabytes },
+ namespace: namespace,
limit: 10.megabytes
)
end
@@ -13,6 +15,10 @@ RSpec.describe Gitlab::RepositorySizeErrorMessage do
let(:message) { checker.error_message }
let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' }
+ before do
+ allow(namespace).to receive(:total_repository_size_excess).and_return(0)
+ end
+
describe 'error messages' do
describe '#commit_error' do
it 'returns the correct message' do
diff --git a/spec/lib/gitlab/sample_data_template_spec.rb b/spec/lib/gitlab/sample_data_template_spec.rb
new file mode 100644
index 00000000000..7d0d415b3af
--- /dev/null
+++ b/spec/lib/gitlab/sample_data_template_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SampleDataTemplate do
+ describe '.all' do
+ it 'returns all templates' do
+ expected = %w[
+ basic
+ serenity_valley
+ ]
+
+ expect(described_class.all).to be_an(Array)
+ expect(described_class.all.map(&:name)).to match_array(expected)
+ end
+ end
+
+ describe '.find' do
+ subject { described_class.find(query) }
+
+ context 'when there is a match' do
+ let(:query) { :basic }
+
+ it { is_expected.to be_a(described_class) }
+ end
+
+ context 'when there is no match' do
+ let(:query) { 'no-match' }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
+ describe '.archive_directory' do
+ subject { described_class.archive_directory }
+
+ it { is_expected.to be_a Pathname }
+ end
+
+ describe 'validate all templates' do
+ let_it_be(:admin) { create(:admin) }
+
+ described_class.all.each do |template|
+ it "#{template.name} has a valid archive" do
+ archive = template.archive_path
+
+ expect(File.exist?(archive)).to be(true)
+ end
+
+ context 'with valid parameters' do
+ it 'can be imported' do
+ params = {
+ template_name: template.name,
+ namespace_id: admin.namespace.id,
+ path: template.name
+ }
+
+ project = Projects::CreateFromTemplateService.new(admin, params).execute
+
+ expect(project).to be_valid
+ expect(project).to be_persisted
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search/recent_issues_spec.rb b/spec/lib/gitlab/search/recent_issues_spec.rb
index 19a41d2aa38..c6d93173dc0 100644
--- a/spec/lib/gitlab/search/recent_issues_spec.rb
+++ b/spec/lib/gitlab/search/recent_issues_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
RSpec.describe ::Gitlab::Search::RecentIssues do
- def create_item(content:, project:)
- create(:issue, title: content, project: project)
+ let(:parent_type) { :project }
+
+ def create_item(content:, parent:)
+ create(:issue, title: content, project: parent)
end
it_behaves_like 'search recent items'
diff --git a/spec/lib/gitlab/search/recent_merge_requests_spec.rb b/spec/lib/gitlab/search/recent_merge_requests_spec.rb
index c6678ce0342..1da3e1425d9 100644
--- a/spec/lib/gitlab/search/recent_merge_requests_spec.rb
+++ b/spec/lib/gitlab/search/recent_merge_requests_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
RSpec.describe ::Gitlab::Search::RecentMergeRequests do
- def create_item(content:, project:)
- create(:merge_request, :unique_branches, title: content, target_project: project, source_project: project)
+ let(:parent_type) { :project }
+
+ def create_item(content:, parent:)
+ create(:merge_request, :unique_branches, title: content, target_project: parent, source_project: parent)
end
it_behaves_like 'search recent items'
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index b4cf6a568b4..57be9e93af2 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -11,9 +11,11 @@ RSpec.describe Gitlab::SearchResults do
let_it_be(:issue) { create(:issue, project: project, title: 'foo') }
let_it_be(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:merge_request) { create(:merge_request, source_project: project, title: 'foo') }
+ let(:query) { 'foo' }
let(:filters) { {} }
+ let(:sort) { nil }
- subject(:results) { described_class.new(user, 'foo', Project.order(:id), filters: filters) }
+ subject(:results) { described_class.new(user, query, Project.order(:id), sort: sort, filters: filters) }
context 'as a user with access' do
before do
@@ -58,6 +60,25 @@ RSpec.describe Gitlab::SearchResults do
end
end
+ describe '#highlight_map' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:scope, :expected) do
+ 'projects' | {}
+ 'issues' | {}
+ 'merge_requests' | {}
+ 'milestones' | {}
+ 'users' | {}
+ 'unknown' | {}
+ end
+
+ with_them do
+ it 'returns the expected highlight_map' do
+ expect(results.highlight_map(scope)).to eq(expected)
+ end
+ end
+ end
+
describe '#formatted_limited_count' do
using RSpec::Parameterized::TableSyntax
@@ -137,10 +158,12 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#merge_requests' do
+ let(:scope) { 'merge_requests' }
+
it 'includes project filter by default' do
expect(results).to receive(:project_ids_relation).and_call_original
- results.objects('merge_requests')
+ results.objects(scope)
end
it 'skips project filter if default project context is used' do
@@ -148,24 +171,34 @@ RSpec.describe Gitlab::SearchResults do
expect(results).not_to receive(:project_ids_relation)
- results.objects('merge_requests')
+ results.objects(scope)
end
context 'filtering' do
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
- let(:scope) { 'merge_requests' }
let(:query) { 'foo' }
include_examples 'search results filtered by state'
end
+
+ context 'ordering' do
+ let(:query) { 'sorted' }
+ let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
+ let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
+ let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
+
+ include_examples 'search results sorted'
+ end
end
describe '#issues' do
+ let(:scope) { 'issues' }
+
it 'includes project filter by default' do
expect(results).to receive(:project_ids_relation).and_call_original
- results.objects('issues')
+ results.objects(scope)
end
it 'skips project filter if default project context is used' do
@@ -173,16 +206,25 @@ RSpec.describe Gitlab::SearchResults do
expect(results).not_to receive(:project_ids_relation)
- results.objects('issues')
+ results.objects(scope)
end
context 'filtering' do
- let(:scope) { 'issues' }
-
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') }
+ let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
include_examples 'search results filtered by state'
+ include_examples 'search results filtered by confidential'
+ end
+
+ context 'ordering' do
+ let(:query) { 'sorted' }
+ let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
+ let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
+ let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
+
+ include_examples 'search results sorted'
end
end
diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/lib/gitlab/sidekiq_cluster_spec.rb
index 5dd913aebb0..5517abe1010 100644
--- a/spec/lib/gitlab/sidekiq_cluster_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::SidekiqCluster do
allow(Process).to receive(:spawn).and_return(1)
expect(described_class).to receive(:wait_async).with(1)
- expect(described_class.start_sidekiq(%w(foo), options)).to eq(1)
+ expect(described_class.start_sidekiq(%w(foo), **options)).to eq(1)
end
it 'handles duplicate queue names' do
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::SidekiqCluster do
.and_return(1)
expect(described_class).to receive(:wait_async).with(1)
- expect(described_class.start_sidekiq(%w(foo foo bar baz), options)).to eq(1)
+ expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(1)
end
it 'runs the sidekiq process in a new process group' do
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::SidekiqCluster do
.and_return(1)
allow(described_class).to receive(:wait_async)
- expect(described_class.start_sidekiq(%w(foo bar baz), options)).to eq(1)
+ expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(1)
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
index bde19fa7552..ca473462d2e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
include ApplicationWorker
+ feature_category :foo
worker_context user: nil
def perform(identifier, *args)
@@ -56,6 +57,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
expect(TestWorker.contexts['identifier'].keys).not_to include('meta.user')
end
+ it 'takes the feature category from the worker' do
+ TestWorker.perform_async('identifier', 1)
+
+ expect(TestWorker.contexts['identifier']).to include('meta.feature_category' => 'foo')
+ end
+
it "doesn't fail for unknown workers" do
expect { OtherWorker.perform_async }.not_to raise_error
end
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index e1ae26a4d9e..2177b2be6d6 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -21,6 +21,12 @@ RSpec.describe Gitlab::SnippetSearchResults do
end
end
+ describe '#highlight_map' do
+ it 'returns the expected highlight map' do
+ expect(results.highlight_map('snippet_titles')).to eq({})
+ end
+ end
+
describe '#objects' do
it 'uses page and per_page to paginate results' do
snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo')
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 220ac2ff6da..9bf6f0b82bc 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -3,6 +3,43 @@
require 'spec_helper'
RSpec.describe Gitlab::SQL::Pattern do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.fuzzy_search' do
+ let_it_be(:issue1) { create(:issue, title: 'noise foo noise', description: 'noise bar noise') }
+ let_it_be(:issue2) { create(:issue, title: 'noise baz noise', description: 'noise foo noise') }
+ let_it_be(:issue3) { create(:issue, title: 'Oh', description: 'Ah') }
+
+ subject(:fuzzy_search) { Issue.fuzzy_search(query, columns) }
+
+ where(:query, :columns, :expected) do
+ 'foo' | [Issue.arel_table[:title]] | %i[issue1]
+
+ 'foo' | %i[title] | %i[issue1]
+ 'foo' | %w[title] | %i[issue1]
+ 'foo' | %i[description] | %i[issue2]
+ 'foo' | %i[title description] | %i[issue1 issue2]
+ 'bar' | %i[title description] | %i[issue1]
+ 'baz' | %i[title description] | %i[issue2]
+ 'qux' | %i[title description] | []
+
+ 'oh' | %i[title description] | %i[issue3]
+ 'OH' | %i[title description] | %i[issue3]
+ 'ah' | %i[title description] | %i[issue3]
+ 'AH' | %i[title description] | %i[issue3]
+ 'oh' | %i[title] | %i[issue3]
+ 'ah' | %i[description] | %i[issue3]
+ end
+
+ with_them do
+ let(:expected_issues) { expected.map { |sym| send(sym) } }
+
+ it 'finds the expected issues' do
+ expect(fuzzy_search).to match_array(expected_issues)
+ end
+ end
+ end
+
describe '.to_pattern' do
subject(:to_pattern) { User.to_pattern(query) }
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb
new file mode 100644
index 00000000000..9ce6007165b
--- /dev/null
+++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Global do
+ let(:global) { described_class.new(hash) }
+ let(:default_image_upload_path_value) { 'source/images' }
+
+ let(:default_mounts_value) do
+ [
+ {
+ source: 'source',
+ target: ''
+ }
+ ]
+ end
+
+ let(:default_static_site_generator_value) { 'middleman' }
+
+ shared_examples_for 'valid default configuration' do
+ describe '#compose!' do
+ before do
+ global.compose!
+ end
+
+ it 'creates nodes hash' do
+ expect(global.descendants).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.descendants.count).to eq 3
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.descendants.map(&:class)).to match_array(expected_node_object_classes)
+ end
+
+ it 'sets a description containing "Static Site Editor" for all nodes' do
+ expect(global.descendants.map(&:description)).to all(match(/Static Site Editor/))
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+ end
+
+ context 'when not composed' do
+ describe '#static_site_generator_value' do
+ it 'returns nil' do
+ expect(global.static_site_generator_value).to be nil
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is leaf' do
+ expect(global).to be_leaf
+ end
+ end
+ end
+
+ context 'when composed' do
+ before do
+ global.compose!
+ end
+
+ describe '#errors' do
+ it 'has no errors' do
+ expect(global.errors).to be_empty
+ end
+ end
+
+ describe '#image_upload_path_value' do
+ it 'returns correct values' do
+ expect(global.image_upload_path_value).to eq(default_image_upload_path_value)
+ end
+ end
+
+ describe '#mounts_value' do
+ it 'returns correct values' do
+ expect(global.mounts_value).to eq(default_mounts_value)
+ end
+ end
+
+ describe '#static_site_generator_value' do
+ it 'returns correct values' do
+ expect(global.static_site_generator_value).to eq(default_static_site_generator_value)
+ end
+ end
+ end
+ end
+
+ describe '.nodes' do
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a(Hash)
+ end
+
+ context 'when filtering all the entry/node names' do
+ it 'contains the expected node names' do
+ expected_node_names = %i[
+ image_upload_path
+ mounts
+ static_site_generator
+ ]
+ expect(described_class.nodes.keys).to match_array(expected_node_names)
+ end
+ end
+ end
+
+ context 'when configuration is valid' do
+ context 'when some entries defined' do
+ let(:expected_node_object_classes) do
+ [
+ Gitlab::StaticSiteEditor::Config::FileConfig::Entry::ImageUploadPath,
+ Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mounts,
+ Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator
+ ]
+ end
+
+ let(:hash) do
+ {
+ image_upload_path: default_image_upload_path_value,
+ mounts: default_mounts_value,
+ static_site_generator: default_static_site_generator_value
+ }
+ end
+
+ it_behaves_like 'valid default configuration'
+ end
+ end
+
+ context 'when value is an empty hash' do
+ let(:expected_node_object_classes) do
+ [
+ Gitlab::Config::Entry::Unspecified,
+ Gitlab::Config::Entry::Unspecified,
+ Gitlab::Config::Entry::Unspecified
+ ]
+ end
+
+ let(:hash) { {} }
+
+ it_behaves_like 'valid default configuration'
+ end
+
+ context 'when configuration is not valid' do
+ before do
+ global.compose!
+ end
+
+ context 'when a single entry is invalid' do
+ let(:hash) do
+ { image_upload_path: { not_a_string: true } }
+ end
+
+ describe '#errors' do
+ it 'reports errors' do
+ expect(global.errors)
+ .to include 'image_upload_path config should be a string'
+ end
+ end
+ end
+
+ context 'when a multiple entries are invalid' do
+ let(:hash) do
+ {
+ image_upload_path: { not_a_string: true },
+ static_site_generator: { not_a_string: true }
+ }
+ end
+
+ describe '#errors' do
+ it 'reports errors' do
+ expect(global.errors)
+ .to match_array([
+ 'image_upload_path config should be a string',
+ 'static_site_generator config should be a string',
+ "static_site_generator config should be 'middleman'"
+ ])
+ end
+ end
+ end
+
+ context 'when there is an invalid key' do
+ let(:hash) do
+ { invalid_key: true }
+ end
+
+ describe '#errors' do
+ it 'reports errors' do
+ expect(global.errors)
+ .to include 'global config contains unknown keys: invalid_key'
+ end
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'returns error about invalid type' do
+ expect(global.errors.first).to match /should be a hash/
+ end
+ end
+ end
+
+ describe '#specified?' do
+ it 'is concrete entry that is defined' do
+ expect(global.specified?).to be true
+ end
+ end
+
+ describe '#[]' do
+ before do
+ global.compose!
+ end
+
+ let(:hash) do
+ { static_site_generator: default_static_site_generator_value }
+ end
+
+ context 'when entry exists' do
+ it 'returns correct entry' do
+ expect(global[:static_site_generator])
+ .to be_an_instance_of Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator
+ expect(global[:static_site_generator].value).to eq default_static_site_generator_value
+ end
+ end
+
+ context 'when entry does not exist' do
+ it 'always return unspecified node' do
+ expect(global[:some][:unknown][:node])
+ .not_to be_specified
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb
new file mode 100644
index 00000000000..c2b7fbf6f98
--- /dev/null
+++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::ImageUploadPath do
+ subject(:image_upload_path_entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'with a valid config' do
+ let(:config) { 'an-image-upload-path' }
+
+ it { is_expected.to be_valid }
+
+ describe '#value' do
+ it 'returns a image_upload_path key' do
+ expect(image_upload_path_entry.value).to eq config
+ end
+ end
+ end
+
+ context 'with an invalid config' do
+ let(:config) { { not_a_string: true } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports errors about wrong type' do
+ expect(image_upload_path_entry.errors)
+ .to include 'image upload path config should be a string'
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default image_upload_path' do
+ expect(described_class.default).to eq 'source/images'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb
new file mode 100644
index 00000000000..04248fc60a5
--- /dev/null
+++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mount do
+ subject(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'with a valid config' do
+ context 'and target is a non-empty string' do
+ let(:config) do
+ {
+ source: 'source',
+ target: 'sub-site'
+ }
+ end
+
+ it { is_expected.to be_valid }
+
+ describe '#value' do
+ it 'returns mount configuration' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+
+ context 'and target is an empty string' do
+ let(:config) do
+ {
+ source: 'source',
+ target: ''
+ }
+ end
+
+ it { is_expected.to be_valid }
+
+ describe '#value' do
+ it 'returns mount configuration' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+ end
+
+ context 'with an invalid config' do
+ context 'when source is not a string' do
+ let(:config) { { source: 123, target: 'target' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'mount source should be a string'
+ end
+ end
+
+ context 'when source is not present' do
+ let(:config) { { target: 'target' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include "mount source can't be blank"
+ end
+ end
+
+ context 'when target is not a string' do
+ let(:config) { { source: 'source', target: 123 } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'mount target should be a string'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { test: 100 } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'mount config contains unknown keys: test'
+ end
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default mount' do
+ expect(described_class.default)
+ .to eq({
+ source: 'source',
+ target: ''
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb
new file mode 100644
index 00000000000..0ae2ece9474
--- /dev/null
+++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mounts do
+ subject(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'with a valid config' do
+ let(:config) do
+ [
+ {
+ source: 'source',
+ target: ''
+ },
+ {
+ source: 'sub-site/source',
+ target: 'sub-site'
+ }
+ ]
+ end
+
+ it { is_expected.to be_valid }
+
+ describe '#value' do
+ it 'returns mounts configuration' do
+ expect(entry.value).to eq config
+ end
+ end
+ end
+
+ context 'with an invalid config' do
+ let(:config) { { not_an_array: true } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports errors about wrong type' do
+ expect(entry.errors)
+ .to include 'mounts config should be a array'
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default mounts' do
+ expect(described_class.default)
+ .to eq([{
+ source: 'source',
+ target: ''
+ }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb
new file mode 100644
index 00000000000..a9c730218cf
--- /dev/null
+++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator do
+ let(:static_site_generator) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when value is valid' do
+ let(:config) { 'middleman' }
+
+ describe '#value' do
+ it 'returns a static_site_generator key' do
+ expect(static_site_generator.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(static_site_generator).to be_valid
+ end
+ end
+ end
+
+ context 'when value is invalid' do
+ let(:config) { 'not-a-valid-generator' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(static_site_generator).not_to be_valid
+ end
+ end
+ end
+
+ context 'when value has a wrong type' do
+ let(:config) { { not_a_string: true } }
+
+ it 'reports errors about wrong type' do
+ expect(static_site_generator.errors)
+ .to include 'static site generator config should be a string'
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default static_site_generator' do
+ expect(described_class.default).to eq 'middleman'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
index 594425c2dab..d444d4f1df7 100644
--- a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
@@ -3,13 +3,85 @@
require 'spec_helper'
RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig do
- subject(:config) { described_class.new }
+ let(:config) do
+ described_class.new(yml)
+ end
+
+ context 'when config is valid' do
+ context 'when config has valid values' do
+ let(:yml) do
+ <<-EOS
+ static_site_generator: middleman
+ EOS
+ end
+
+ describe '#to_hash_with_defaults' do
+ it 'returns hash created from string' do
+ expect(config.to_hash_with_defaults.fetch(:static_site_generator)).to eq 'middleman'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when a config entry has an empty value' do
+ let(:yml) { 'static_site_generator: ' }
+
+ describe '#to_hash' do
+ it 'returns default value' do
+ expect(config.to_hash_with_defaults.fetch(:static_site_generator)).to eq 'middleman'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+ end
+
+ context 'when config is invalid' do
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(described_class::ConfigError, /Invalid configuration format/)
+ end
+ end
+ end
+
+ context 'when config value exists but is not a valid value' do
+ let(:yml) { 'static_site_generator: "unsupported-generator"' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(config).not_to be_valid
+ end
- describe '#data' do
- subject { config.data }
+ it 'has errors' do
+ expect(config.errors).not_to be_empty
+ end
+ end
- it 'returns hardcoded data for now' do
- is_expected.to match(static_site_generator: 'middleman')
+ describe '#errors' do
+ it 'returns an array of strings' do
+ expect(config.errors).to all(be_an_instance_of(String))
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
index 3433a54be9c..2f761b69e60 100644
--- a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
project: 'project',
project_id: project.id,
return_url: 'http://example.com',
- is_supported_content: 'true',
+ is_supported_content: true,
base_url: '/namespace/project/-/sse/master%2FREADME.md',
merge_requests_illustration_path: %r{illustrations/merge_requests}
})
@@ -65,7 +65,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
stub_feature_flags(sse_erb_support: project)
end
- it { is_expected.to include(is_supported_content: 'true') }
+ it { is_expected.to include(is_supported_content: true) }
end
context 'when feature flag is disabled' do
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
stub_feature_flags(sse_erb_support: false)
end
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
end
@@ -88,31 +88,31 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
context 'when branch is not master' do
let(:ref) { 'my-branch' }
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
context 'when file does not have a markdown extension' do
let(:path) { 'README.txt' }
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
context 'when file does not have an extension' do
let(:path) { 'README' }
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
context 'when file does not exist' do
let(:path) { 'UNKNOWN.md' }
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
context 'when repository is empty' do
let(:repository) { create(:project_empty_repo).repository }
- it { is_expected.to include(is_supported_content: 'false') }
+ it { is_expected.to include(is_supported_content: false) }
end
context 'when return_url is not a valid URL' do
@@ -132,5 +132,11 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
it { is_expected.to include(return_url: nil) }
end
+
+ context 'when a commit for the ref cannot be found' do
+ let(:ref) { 'nonexistent-ref' }
+
+ it { is_expected.to include(commit_id: nil) }
+ end
end
end
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
new file mode 100644
index 00000000000..351af3c07d2
--- /dev/null
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::SubscriptionPortal do
+ describe '.default_subscriptions_url' do
+ subject { described_class.default_subscriptions_url }
+
+ context 'on non test and non dev environments' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
+ end
+
+ it 'returns production subscriptions app URL' do
+ is_expected.to eq('https://customers.gitlab.com')
+ end
+ end
+
+ context 'on dev environment' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(true)
+ end
+
+ it 'returns staging subscriptions app url' do
+ is_expected.to eq('https://customers.stg.gitlab.com')
+ end
+ end
+
+ context 'on test environment' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(true)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
+ end
+
+ it 'returns staging subscriptions app url' do
+ is_expected.to eq('https://customers.stg.gitlab.com')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index 68ff28becfa..6d03cf496b8 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -47,4 +47,18 @@ RSpec.describe Gitlab::Themes, lib: true do
expect(ids).not_to be_empty
end
end
+
+ describe 'theme.css_filename' do
+ described_class.each do |theme|
+ next unless theme.css_filename
+
+ context "for #{theme.name}" do
+ it 'returns an existing CSS filename' do
+ css_file_path = Rails.root.join('app/assets/stylesheets/themes', theme.css_filename + '.scss')
+
+ expect(File.exist?(css_file_path)).to eq(true)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index f0bf7b9964f..6ddeaf98370 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do
end
around do |example|
- Timecop.freeze(timestamp) { example.run }
+ travel_to(timestamp) { example.run }
end
before do
diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
index 2a674557b76..f2c1d8718d7 100644
--- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
@@ -41,11 +41,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
context 'for web IDE edit actions' do
it_behaves_like 'tracks and counts action' do
def track_action(params)
- described_class.track_web_ide_edit_action(params)
+ described_class.track_web_ide_edit_action(**params)
end
def count_unique(params)
- described_class.count_web_ide_edit_actions(params)
+ described_class.count_web_ide_edit_actions(**params)
end
end
end
@@ -53,11 +53,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
context 'for SFE edit actions' do
it_behaves_like 'tracks and counts action' do
def track_action(params)
- described_class.track_sfe_edit_action(params)
+ described_class.track_sfe_edit_action(**params)
end
def count_unique(params)
- described_class.count_sfe_edit_actions(params)
+ described_class.count_sfe_edit_actions(**params)
end
end
end
@@ -65,11 +65,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
context 'for snippet editor edit actions' do
it_behaves_like 'tracks and counts action' do
def track_action(params)
- described_class.track_snippet_editor_edit_action(params)
+ described_class.track_snippet_editor_edit_action(**params)
end
def count_unique(params)
- described_class.count_snippet_editor_edit_actions(params)
+ described_class.count_snippet_editor_edit_actions(**params)
end
end
end
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 f881da71251..e84c3c17274 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
@@ -15,12 +15,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
# depending on which day of the week test is run.
# Monday 6th of June
reference_time = Time.utc(2020, 6, 1)
- Timecop.freeze(reference_time) { example.run }
+ travel_to(reference_time) { example.run }
end
describe '.categories' do
it 'gets all unique category names' do
- expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit')
+ expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit', 'testing')
end
end
@@ -238,16 +238,20 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'returns the number of unique events for all known events' do
results = {
- 'category1' => {
- 'event1_slot' => 1,
- 'event2_slot' => 1,
- 'category1_total_unique_counts_weekly' => 2,
- 'category1_total_unique_counts_monthly' => 3
- },
- 'category2' => {
- 'event3' => 1,
- 'event4' => 1
- }
+ "category1" => {
+ "event1_slot_weekly" => 1,
+ "event1_slot_monthly" => 1,
+ "event2_slot_weekly" => 1,
+ "event2_slot_monthly" => 2,
+ "category1_total_unique_counts_weekly" => 2,
+ "category1_total_unique_counts_monthly" => 3
+ },
+ "category2" => {
+ "event3_weekly" => 1,
+ "event3_monthly" => 1,
+ "event4_weekly" => 1,
+ "event4_monthly" => 1
+ }
}
expect(subject.unique_events_data).to eq(results)
diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
index 479fe36bcdd..e08dc41d0cc 100644
--- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:action) { described_class::ISSUE_TITLE_CHANGED }
def track_action(params)
- described_class.track_issue_title_changed_action(params)
+ described_class.track_issue_title_changed_action(**params)
end
end
end
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED }
def track_action(params)
- described_class.track_issue_description_changed_action(params)
+ described_class.track_issue_description_changed_action(**params)
end
end
end
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED }
def track_action(params)
- described_class.track_issue_assignee_changed_action(params)
+ described_class.track_issue_assignee_changed_action(**params)
end
end
end
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL }
def track_action(params)
- described_class.track_issue_made_confidential_action(params)
+ described_class.track_issue_made_confidential_action(**params)
end
end
end
@@ -87,7 +87,207 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
let(:action) { described_class::ISSUE_MADE_VISIBLE }
def track_action(params)
- described_class.track_issue_made_visible_action(params)
+ described_class.track_issue_made_visible_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue created actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_CREATED }
+
+ def track_action(params)
+ described_class.track_issue_created_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue closed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_CLOSED }
+
+ def track_action(params)
+ described_class.track_issue_closed_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue reopened actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_REOPENED }
+
+ def track_action(params)
+ described_class.track_issue_reopened_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue label changed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_LABEL_CHANGED }
+
+ def track_action(params)
+ described_class.track_issue_label_changed_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue cross-referenced actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_CROSS_REFERENCED }
+
+ def track_action(params)
+ described_class.track_issue_cross_referenced_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue moved actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_MOVED }
+
+ def track_action(params)
+ described_class.track_issue_moved_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue relate actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_RELATED }
+
+ def track_action(params)
+ described_class.track_issue_related_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue unrelate actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_UNRELATED }
+
+ def track_action(params)
+ described_class.track_issue_unrelated_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue marked as duplicate actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE }
+
+ def track_action(params)
+ described_class.track_issue_marked_as_duplicate_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue locked actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_LOCKED }
+
+ def track_action(params)
+ described_class.track_issue_locked_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue unlocked actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_UNLOCKED }
+
+ def track_action(params)
+ described_class.track_issue_unlocked_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue added to epic actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_ADDED_TO_EPIC}
+
+ def track_action(params)
+ described_class.track_issue_added_to_epic_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue removed from epic actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC}
+
+ def track_action(params)
+ described_class.track_issue_removed_from_epic_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue changed epic actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_CHANGED_EPIC}
+
+ def track_action(params)
+ described_class.track_issue_changed_epic_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue designs added actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_DESIGNS_ADDED }
+
+ def track_action(params)
+ described_class.track_issue_designs_added_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue designs modified actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_DESIGNS_MODIFIED }
+
+ def track_action(params)
+ described_class.track_issue_designs_modified_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue designs removed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_DESIGNS_REMOVED }
+
+ def track_action(params)
+ described_class.track_issue_designs_removed_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue due date changed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_DUE_DATE_CHANGED }
+
+ def track_action(params)
+ described_class.track_issue_due_date_changed_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue time estimate changed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED }
+
+ def track_action(params)
+ described_class.track_issue_time_estimate_changed_action(**params)
+ end
+ end
+ end
+
+ context 'for Issue time spent changed actions' do
+ it_behaves_like 'tracks and counts action' do
+ let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED }
+
+ def track_action(params)
+ described_class.track_issue_time_spent_changed_action(**params)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb
new file mode 100644
index 00000000000..aaa576865f6
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::StaticSiteEditorCounter do
+ it_behaves_like 'a redis usage counter', 'StaticSiteEditor', :views
+
+ it_behaves_like 'a redis usage counter with totals', :static_site_editor,
+ views: 3
+end
diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
index 8f5f1347ce8..d1144dd0bc5 100644
--- a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
@@ -8,11 +8,11 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueEvents, :clean_gitlab_redis
let(:time) { Time.zone.now }
def track_event(params)
- track_unique_events.track_event(params)
+ track_unique_events.track_event(**params)
end
def count_unique(params)
- track_unique_events.count_unique_events(params)
+ track_unique_events.count_unique_events(**params)
end
context 'tracking an event' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 6631a0d3cc6..f64fa2b868d 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
before do
stub_usage_data_connections
stub_object_store_settings
+ clear_memoized_values(described_class::CE_MEMOIZED_VALUES)
end
describe '.uncached_data' do
@@ -24,17 +25,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
it 'clears memoized values' do
- values = %i(issue_minimum_id issue_maximum_id
- project_minimum_id project_maximum_id
- user_minimum_id user_maximum_id unique_visit_service
- deployment_minimum_id deployment_maximum_id
- approval_merge_request_rule_minimum_id
- approval_merge_request_rule_maximum_id)
- values.each do |key|
- expect(described_class).to receive(:clear_memoization).with(key)
- end
+ allow(described_class).to receive(:clear_memoization)
subject
+
+ described_class::CE_MEMOIZED_VALUES.each do |key|
+ expect(described_class).to have_received(:clear_memoization).with(key)
+ end
end
it 'merge_requests_users is included only in montly counters' do
@@ -174,21 +171,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
for_defined_days_back do
user = create(:user)
+ user2 = create(:user)
create(:event, author: user)
create(:group_member, user: user)
+ create(:authentication_event, user: user, provider: :ldapmain, result: :success)
+ create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success)
+ create(:authentication_event, user: user2, provider: :group_saml, result: :success)
+ create(:authentication_event, user: user2, provider: :group_saml, result: :success)
+ create(:authentication_event, user: user, provider: :group_saml, result: :failed)
end
expect(described_class.usage_activity_by_stage_manage({})).to include(
events: 2,
groups: 2,
- users_created: 4,
- omniauth_providers: ['google_oauth2']
+ users_created: 6,
+ omniauth_providers: ['google_oauth2'],
+ user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4 }
)
expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
events: 1,
groups: 1,
- users_created: 2,
- omniauth_providers: ['google_oauth2']
+ users_created: 3,
+ omniauth_providers: ['google_oauth2'],
+ user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2 }
)
end
@@ -244,6 +249,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
)
end
+ it 'includes group imports usage data' do
+ for_defined_days_back do
+ user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+ create(:group_import_state, group: group, user: user)
+ end
+
+ expect(described_class.usage_activity_by_stage_manage({}))
+ .to include(groups_imported: 2)
+ expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period))
+ .to include(groups_imported: 1)
+ end
+
def omniauth_providers
[
OpenStruct.new(name: 'google_oauth2'),
@@ -260,17 +279,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
cluster = create(:cluster, user: user)
create(:project, creator: user)
create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:project_tracing_setting)
end
expect(described_class.usage_activity_by_stage_monitor({})).to include(
clusters: 2,
clusters_applications_prometheus: 2,
- operations_dashboard_default_dashboard: 2
+ operations_dashboard_default_dashboard: 2,
+ projects_with_tracing_enabled: 2
)
expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include(
clusters: 1,
clusters_applications_prometheus: 1,
- operations_dashboard_default_dashboard: 1
+ operations_dashboard_default_dashboard: 1,
+ projects_with_tracing_enabled: 1
)
end
end
@@ -415,11 +437,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_slack_slash_commands_active]).to eq(1)
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_instance_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)
expect(count_data[:projects_with_repositories_enabled]).to eq(3)
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
+ expect(count_data[:projects_with_tracing_enabled]).to eq(1)
expect(count_data[:projects_with_alerts_service_enabled]).to eq(1)
expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
expect(count_data[:projects_with_terraform_reports]).to eq(2)
@@ -472,8 +497,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:personal_snippets]).to eq(2)
expect(count_data[:project_snippets]).to eq(4)
+ expect(count_data[:projects_creating_incidents]).to eq(2)
expect(count_data[:projects_with_packages]).to eq(2)
expect(count_data[:packages]).to eq(4)
+ expect(count_data[:user_preferences_user_gitpod_enabled]).to eq(1)
end
it 'gathers object store usage correctly' do
@@ -549,8 +576,17 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
describe '.system_usage_data_monthly' do
+ let_it_be(:project) { create(:project) }
let!(:ud) { build(:usage_data) }
+ before do
+ stub_application_setting(self_monitoring_project: project)
+
+ for_defined_days_back do
+ create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote')
+ end
+ end
+
subject { described_class.system_usage_data_monthly }
it 'gathers monthly usage counts correctly' do
@@ -563,6 +599,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(counts_monthly[:personal_snippets]).to eq(1)
expect(counts_monthly[:project_snippets]).to eq(2)
expect(counts_monthly[:packages]).to eq(3)
+ expect(counts_monthly[:promoted_issues]).to eq(1)
end
end
@@ -570,6 +607,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.usage_counters }
it { is_expected.to include(:kubernetes_agent_gitops_sync) }
+ it { is_expected.to include(:static_site_editor_views) }
end
describe '.usage_data_counters' do
@@ -628,6 +666,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
expect(subject[:web_ide_clientside_preview_enabled]).to eq(Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?)
expect(subject[:grafana_link_enabled]).to eq(Gitlab::CurrentSettings.grafana_enabled?)
+ expect(subject[:gitpod_enabled]).to eq(Gitlab::CurrentSettings.gitpod_enabled?)
end
context 'with embedded Prometheus' do
@@ -657,6 +696,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:grafana_link_enabled]).to eq(false)
end
end
+
+ context 'with Gitpod' do
+ it 'returns true when is enabled' do
+ stub_application_setting(gitpod_enabled: true)
+
+ expect(subject[:gitpod_enabled]).to eq(true)
+ end
+
+ it 'returns false when is disabled' do
+ stub_application_setting(gitpod_enabled: false)
+
+ expect(subject[:gitpod_enabled]).to eq(false)
+ end
+ end
end
describe '.components_usage_data' do
@@ -670,6 +723,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
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[:mail][:smtp_server]).to eq(ActionMailer::Base.smtp_settings[:address])
expect(subject[:gitaly][:version]).to be_present
expect(subject[:gitaly][:servers]).to be >= 1
@@ -979,9 +1033,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- def for_defined_days_back(days: [29, 2])
+ def for_defined_days_back(days: [31, 3])
days.each do |n|
- Timecop.travel(n.days.ago) do
+ travel_to(n.days.ago) do
yield
end
end
@@ -1078,8 +1132,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.compliance_unique_visits_data }
before do
- described_class.clear_memoization(:unique_visit_service)
-
allow_next_instance_of(::Gitlab::Analytics::UniqueVisits) do |instance|
::Gitlab::Analytics::UniqueVisits.compliance_events.each do |target|
allow(instance).to receive(:unique_visits_for).with(targets: target).and_return(123)
@@ -1110,7 +1162,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.search_unique_visits_data }
before do
- described_class.clear_memoization(:unique_visit_service)
events = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('search')
events.each do |event|
allow(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:unique_events).with(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current).and_return(123)
@@ -1136,9 +1187,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.redis_hll_counters }
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
- let(:ineligible_total_categories) { ['source_code'] }
+ let(:ineligible_total_categories) { %w[source_code testing] }
- it 'has all know_events' do
+ it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)
expect(subject[:redis_hll_counters].keys).to match_array(categories)
@@ -1146,11 +1197,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
categories.each do |category|
keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category)
+ metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" }
+
if ineligible_total_categories.exclude?(category)
- keys.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
+ metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
end
- expect(subject[:redis_hll_counters][category].keys).to match_array(keys)
+ expect(subject[:redis_hll_counters][category].keys).to match_array(metrics)
end
end
end
@@ -1169,6 +1222,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
describe '.snowplow_event_counts' do
+ let_it_be(:time_period) { { collector_tstamp: 8.days.ago..1.day.ago } }
+
context 'when self-monitoring project exists' do
let_it_be(:project) { create(:project) }
@@ -1181,14 +1236,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
stub_feature_flags(product_analytics: project)
create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote')
- create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 28.days.ago)
+ create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 2.days.ago)
+ create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 9.days.ago)
+
+ create(:product_analytics_event, project: project, se_category: 'foo', se_action: 'bar', collector_tstamp: 2.days.ago)
end
it 'returns promoted_issues for the time period' do
- expect(described_class.snowplow_event_counts[:promoted_issues]).to eq(2)
- expect(described_class.snowplow_event_counts(
- time_period: described_class.last_28_days_time_period(column: :collector_tstamp)
- )[:promoted_issues]).to eq(1)
+ expect(described_class.snowplow_event_counts(time_period)[:promoted_issues]).to eq(1)
end
end
@@ -1198,14 +1253,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
it 'returns an empty hash' do
- expect(described_class.snowplow_event_counts).to eq({})
+ expect(described_class.snowplow_event_counts(time_period)).to eq({})
end
end
end
context 'when self-monitoring project does not exist' do
it 'returns an empty hash' do
- expect(described_class.snowplow_event_counts).to eq({})
+ expect(described_class.snowplow_event_counts(time_period)).to eq({})
end
end
end
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 362cbaa78e9..9c0dc69ccd1 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -212,33 +212,26 @@ RSpec.describe Gitlab::Utils::UsageData do
describe '#track_usage_event' do
let(:value) { '9f302fea-f828-4ca9-aef4-e10bd723c0b3' }
- let(:event_name) { 'my_event' }
+ let(:event_name) { 'incident_management_alert_status_changed' }
let(:unknown_event) { 'unknown' }
let(:feature) { "usage_data_#{event_name}" }
+ before do
+ skip_feature_flags_yaml_validation
+ end
+
context 'with feature enabled' do
before do
stub_feature_flags(feature => true)
end
it 'tracks redis hll event' do
- stub_application_setting(usage_ping_enabled: true)
-
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(value, event_name)
described_class.track_usage_event(event_name, value)
end
- it 'does not track event when usage ping is not enabled' do
- stub_application_setting(usage_ping_enabled: false)
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
-
- described_class.track_usage_event(event_name, value)
- end
-
it 'raise an error for unknown event' do
- stub_application_setting(usage_ping_enabled: true)
-
expect { described_class.track_usage_event(unknown_event, value) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
end
end
diff --git a/spec/lib/gitlab/visibility_level_checker_spec.rb b/spec/lib/gitlab/visibility_level_checker_spec.rb
index 833021a22ca..38a7d967c33 100644
--- a/spec/lib/gitlab/visibility_level_checker_spec.rb
+++ b/spec/lib/gitlab/visibility_level_checker_spec.rb
@@ -5,16 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::VisibilityLevelChecker do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:visibility_level_checker) { }
let(:override_params) { {} }
- subject { described_class.new(user, project, project_params: override_params) }
-
describe '#level_restricted?' do
+ subject(:result) { described_class.new(user, project, project_params: override_params).level_restricted? }
+
context 'when visibility level is allowed' do
it 'returns false with nil for visibility level' do
- result = subject.level_restricted?
-
expect(result.restricted?).to eq(false)
expect(result.visibility_level).to be_nil
end
@@ -25,12 +22,26 @@ RSpec.describe Gitlab::VisibilityLevelChecker do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- it 'returns true and visibility name' do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- result = subject.level_restricted?
+ context 'for public project' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ context 'for non-admin user' do
+ it 'returns true and visibility name' do
+ expect(result.restricted?).to eq(true)
+ expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context 'for admin user' do
+ let(:user) { create(:user, :admin) }
- expect(result.restricted?).to eq(true)
- expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ it 'returns false and a nil visibility level' do
+ expect(result.restricted?).to eq(false)
+ expect(result.visibility_level).to be_nil
+ end
+ end
end
context 'overridden visibility' do
@@ -50,8 +61,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do
let(:override_visibility) { 'public' }
it 'returns true and visibility name' do
- result = subject.level_restricted?
-
expect(result.restricted?).to eq(true)
expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
@@ -61,8 +70,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do
let(:override_visibility) { 'publik' }
it 'returns false with nil for visibility level' do
- result = subject.level_restricted?
-
expect(result.restricted?).to eq(false)
expect(result.visibility_level).to be_nil
end
@@ -72,8 +79,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do
let(:override_params) { {} }
it 'returns false with nil for visibility level' do
- result = subject.level_restricted?
-
expect(result.restricted?).to eq(false)
expect(result.visibility_level).to be_nil
end
diff --git a/spec/lib/gitlab/webpack/manifest_spec.rb b/spec/lib/gitlab/webpack/manifest_spec.rb
new file mode 100644
index 00000000000..1427bdd7d4f
--- /dev/null
+++ b/spec/lib/gitlab/webpack/manifest_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'json'
+
+RSpec.describe Gitlab::Webpack::Manifest do
+ let(:manifest) do
+ <<-EOF
+ {
+ "errors": [],
+ "assetsByChunkName": {
+ "entry1": [ "entry1.js", "entry1-a.js" ],
+ "entry2": "entry2.js"
+ }
+ }
+ EOF
+ end
+
+ around do |example|
+ Gitlab::Webpack::Manifest.clear_manifest!
+
+ example.run
+
+ Gitlab::Webpack::Manifest.clear_manifest!
+ end
+
+ shared_examples_for "a valid manifest" do
+ it "returns single entry asset paths from the manifest" do
+ expect(Gitlab::Webpack::Manifest.asset_paths("entry2")).to eq(["/public_path/entry2.js"])
+ end
+
+ it "returns multiple entry asset paths from the manifest" do
+ expect(Gitlab::Webpack::Manifest.asset_paths("entry1")).to eq(["/public_path/entry1.js", "/public_path/entry1-a.js"])
+ end
+
+ it "errors on a missing entry point" do
+ expect { Gitlab::Webpack::Manifest.asset_paths("herp") }.to raise_error(Gitlab::Webpack::Manifest::AssetMissingError)
+ end
+ end
+
+ before do
+ # Test that config variables work while we're here
+ allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false)
+ allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('my_manifest.json')
+ allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path')
+ allow(Gitlab.config.webpack).to receive(:output_dir).and_return('manifest_output')
+ end
+
+ context "with dev server enabled" do
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
+
+ stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: manifest, status: 200)
+ end
+
+ describe ".asset_paths" do
+ it_behaves_like "a valid manifest"
+
+ it "errors if we can't find the manifest" do
+ allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json')
+ stub_request(:get, "http://hostname:2000/public_path/broken.json").to_raise(SocketError)
+
+ expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
+ end
+
+ describe "webpack errors" do
+ context "when webpack has 'Module build failed' errors in its manifest" do
+ it "errors" do
+ error_manifest = Gitlab::Json.parse(manifest).merge("errors" => [
+ "somethingModule build failed something",
+ "I am an error"
+ ]).to_json
+ stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
+
+ expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::WebpackError)
+ end
+ end
+
+ context "when webpack does not have 'Module build failed' errors in its manifest" do
+ it "does not error" do
+ error_manifest = Gitlab::Json.parse(manifest).merge("errors" => ["something went wrong"]).to_json
+ stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
+
+ expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error
+ end
+ end
+
+ it "does not error if errors is present but empty" do
+ error_manifest = Gitlab::Json.parse(manifest).merge("errors" => []).to_json
+ stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
+ expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ context "with dev server disabled" do
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false)
+ allow(File).to receive(:read).with(::Rails.root.join("manifest_output/my_manifest.json")).and_return(manifest)
+ end
+
+ describe ".asset_paths" do
+ it_behaves_like "a valid manifest"
+
+ it "errors if we can't find the manifest" do
+ allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json')
+ allow(File).to receive(:read).with(::Rails.root.join("manifest_output/broken.json")).and_raise(Errno::ENOENT)
+ expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index e9733851590..9662ad13631 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -54,12 +54,44 @@ RSpec.describe Gitlab::Workhorse do
commit_id: metadata['CommitId'],
prefix: metadata['ArchivePrefix'],
format: Gitaly::GetArchiveRequest::Format::ZIP,
- path: path
+ path: path,
+ include_lfs_blobs: true
).to_proto
)
}.deep_stringify_keys)
end
+ context 'when include_lfs_blobs_in_archive is disabled' do
+ before do
+ stub_feature_flags(include_lfs_blobs_in_archive: false)
+ end
+
+ it 'sets include_lfs_blobs to false' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ features: { 'gitaly-feature-foobar' => 'true' },
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => Base64.encode64(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: Gitaly::GetArchiveRequest::Format::ZIP,
+ path: path,
+ include_lfs_blobs: false
+ ).to_proto
+ )
+ }.deep_stringify_keys)
+ end
+ end
+
context 'when archive caching is disabled' do
let(:cache_disabled) { true }