summaryrefslogtreecommitdiff
path: root/spec/lib
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib')
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb27
-rw-r--r--spec/lib/api/helpers/sse_helpers_spec.rb44
-rw-r--r--spec/lib/api/validations/validators/integer_or_custom_value_spec.rb46
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb165
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb52
-rw-r--r--spec/lib/backup/files_spec.rb18
-rw-r--r--spec/lib/backup/repositories_spec.rb4
-rw-r--r--spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb58
-rw-r--r--spec/lib/banzai/filter/kroki_filter_spec.rb41
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb6
-rw-r--r--spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb7
-rw-r--r--spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb88
-rw-r--r--spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb28
-rw-r--r--spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb72
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb11
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb5
-rw-r--r--spec/lib/bulk_imports/importers/group_importer_spec.rb30
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb169
-rw-r--r--spec/lib/bulk_imports/pipeline_spec.rb (renamed from spec/lib/bulk_imports/pipeline/attributes_spec.rb)10
-rw-r--r--spec/lib/feature/definition_spec.rb63
-rw-r--r--spec/lib/feature_spec.rb173
-rw-r--r--spec/lib/gitlab/anonymous_session_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc/html5_converter_spec.rb15
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb53
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb17
-rw-r--r--spec/lib/gitlab/auth/crowd/authentication_spec.rb48
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb17
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb17
-rw-r--r--spec/lib/gitlab/auth/otp/session_enforcer_spec.rb41
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb24
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb87
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb56
-rw-r--r--spec/lib/gitlab/auth_spec.rb23
-rw-r--r--spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb12
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb48
-rw-r--r--spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb48
-rw-r--r--spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb28
-rw-r--r--spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb44
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/reset_merge_status_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb74
-rw-r--r--spec/lib/gitlab/checks/diff_check_spec.rb21
-rw-r--r--spec/lib/gitlab/checks/push_check_spec.rb21
-rw-r--r--spec/lib/gitlab/checks/snippet_check_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/ansi2json/result_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb92
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config/entry/need_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/config/entry/needs_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/mask_secret_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb138
-rw-r--r--spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb749
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb109
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb95
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb75
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb153
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb308
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb136
-rw-r--r--spec/lib/gitlab/ci/reports/reports_comparer_spec.rb97
-rw-r--r--spec/lib/gitlab/ci/templates/npm_spec.rb92
-rw-r--r--spec/lib/gitlab/ci/trace/checksum_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb106
-rw-r--r--spec/lib/gitlab/cleanup/project_uploads_spec.rb18
-rw-r--r--spec/lib/gitlab/config/entry/configurable_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb3
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb4
-rw-r--r--spec/lib/gitlab/cycle_analytics/usage_data_spec.rb95
-rw-r--r--spec/lib/gitlab/danger/base_linter_spec.rb154
-rw-r--r--spec/lib/gitlab/danger/commit_linter_spec.rb135
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb36
-rw-r--r--spec/lib/gitlab/danger/merge_request_linter_spec.rb55
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb8
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb16
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb23
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb46
-rw-r--r--spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb132
-rw-r--r--spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb34
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb23
-rw-r--r--spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb43
-rw-r--r--spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb32
-rw-r--r--spec/lib/gitlab/database/reindexing/index_selection_spec.rb50
-rw-r--r--spec/lib/gitlab/database/reindexing/reindex_action_spec.rb8
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb6
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb4
-rw-r--r--spec/lib/gitlab/deploy_key_access_spec.rb66
-rw-r--r--spec/lib/gitlab/diff/file_collection/commit_spec.rb72
-rw-r--r--spec/lib/gitlab/diff/file_collection/compare_spec.rb39
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb31
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb7
-rw-r--r--spec/lib/gitlab/diff/file_collection_sorter_spec.rb51
-rw-r--r--spec/lib/gitlab/diff/lines_unfolder_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/email/smime/certificate_spec.rb14
-rw-r--r--spec/lib/gitlab/encrypted_configuration_spec.rb145
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb216
-rw-r--r--spec/lib/gitlab/experimentation/experiment_spec.rb55
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb152
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb66
-rw-r--r--spec/lib/gitlab/git_access_project_spec.rb17
-rw-r--r--spec/lib/gitlab/git_access_spec.rb117
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb27
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb24
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb69
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb202
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb1
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb47
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb46
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb72
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb1
-rw-r--r--spec/lib/gitlab/google_code_import/client_spec.rb38
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb88
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb32
-rw-r--r--spec/lib/gitlab/graphql/docs/renderer_spec.rb26
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/pagination/array_connection_spec.rb15
-rw-r--r--spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb8
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb11
-rw-r--r--spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb11
-rw-r--r--spec/lib/gitlab/graphql/timeout_spec.rb2
-rw-r--r--spec/lib/gitlab/hook_data/group_member_builder_spec.rb60
-rw-r--r--spec/lib/gitlab/i18n/po_linter_spec.rb55
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb112
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml10
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb74
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml6
-rw-r--r--spec/lib/gitlab/import_export/wiki_restorer_spec.rb47
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb3
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb9
-rw-r--r--spec/lib/gitlab/kubernetes/deployment_spec.rb190
-rw-r--r--spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb26
-rw-r--r--spec/lib/gitlab/kubernetes/ingress_spec.rb57
-rw-r--r--spec/lib/gitlab/kubernetes/rollout_instances_spec.rb128
-rw-r--r--spec/lib/gitlab/kubernetes/rollout_status_spec.rb271
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb56
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb66
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb119
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb6
-rw-r--r--spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb35
-rw-r--r--spec/lib/gitlab/pagination/offset_header_builder_spec.rb61
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb96
-rw-r--r--spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb64
-rw-r--r--spec/lib/gitlab/performance_bar/stats_spec.rb42
-rw-r--r--spec/lib/gitlab/rack_attack/user_allowlist_spec.rb33
-rw-r--r--spec/lib/gitlab/rack_attack_spec.rb96
-rw-r--r--spec/lib/gitlab/sample_data_template_spec.rb5
-rw-r--r--spec/lib/gitlab/setup_helper/workhorse_spec.rb25
-rw-r--r--spec/lib/gitlab/sidekiq_cluster_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_death_handler_spec.rb50
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb65
-rw-r--r--spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb84
-rw-r--r--spec/lib/gitlab/tracking/destinations/snowplow_spec.rb4
-rw-r--r--spec/lib/gitlab/tracking_spec.rb17
-rw-r--r--spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/base_counter_spec.rb34
-rw-r--r--spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb14
-rw-r--r--spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb31
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/usage_data_counters/search_counter_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_counters_spec.rb32
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb93
-rw-r--r--spec/lib/gitlab/user_access_spec.rb20
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb30
-rw-r--r--spec/lib/gitlab/uuid_spec.rb52
-rw-r--r--spec/lib/gitlab/webpack/manifest_spec.rb4
-rw-r--r--spec/lib/gitlab_danger_spec.rb4
-rw-r--r--spec/lib/gitlab_spec.rb31
-rw-r--r--spec/lib/microsoft_teams/notifier_spec.rb4
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb47
-rw-r--r--spec/lib/product_analytics/tracker_spec.rb49
-rw-r--r--spec/lib/quality/test_level_spec.rb8
212 files changed, 8175 insertions, 1868 deletions
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb
index 715fcf4bcdb..fe4c27b70ae 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -40,4 +40,31 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
expect(batch.count).to be_within(3 * query.count).of(control.count)
end
end
+
+ context 'reviewers' do
+ context "when merge_request_reviewers FF is enabled" do
+ before do
+ stub_feature_flags(merge_request_reviewers: true)
+ merge_request.reviewers = [user]
+ end
+
+ it 'includes assigned reviewers' do
+ result = Gitlab::Json.parse(present(merge_request).to_json)
+
+ expect(result['reviewers'][0]['username']).to eq user.username
+ end
+ end
+
+ context "when merge_request_reviewers FF is disabled" do
+ before do
+ stub_feature_flags(merge_request_reviewers: false)
+ end
+
+ it 'does not include reviewers' do
+ result = Gitlab::Json.parse(present(merge_request).to_json)
+
+ expect(result.keys).not_to include('reviewers')
+ end
+ end
+ end
end
diff --git a/spec/lib/api/helpers/sse_helpers_spec.rb b/spec/lib/api/helpers/sse_helpers_spec.rb
new file mode 100644
index 00000000000..397051d9142
--- /dev/null
+++ b/spec/lib/api/helpers/sse_helpers_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Helpers::SSEHelpers do
+ include Gitlab::Routing
+
+ let_it_be(:project) { create(:project) }
+
+ subject { Class.new.include(described_class).new }
+
+ describe '#request_from_sse?' do
+ before do
+ allow(subject).to receive(:request).and_return(request)
+ end
+
+ context 'when referer is nil' do
+ let(:request) { double(referer: nil)}
+
+ it 'returns false' do
+ expect(URI).not_to receive(:parse)
+ expect(subject.request_from_sse?(project)).to eq false
+ end
+ end
+
+ context 'when referer is not from SSE' do
+ let(:request) { double(referer: 'https://gitlab.com')}
+
+ it 'returns false' do
+ expect(URI).to receive(:parse).and_call_original
+ expect(subject.request_from_sse?(project)).to eq false
+ end
+ end
+
+ context 'when referer is from SSE' do
+ let(:request) { double(referer: project_show_sse_path(project, 'master/README.md'))}
+
+ it 'returns true' do
+ expect(URI).to receive(:parse).and_call_original
+ expect(subject.request_from_sse?(project)).to eq true
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb b/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb
new file mode 100644
index 00000000000..a04917736db
--- /dev/null
+++ b/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Validations::Validators::IntegerOrCustomValue do
+ include ApiValidatorsHelpers
+
+ let(:custom_values) { %w[None Any Started Current] }
+
+ subject { described_class.new(['test'], { values: custom_values }, false, scope.new) }
+
+ context 'valid parameters' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error('test' => 2)
+ expect_no_validation_error('test' => 100)
+ expect_no_validation_error('test' => 'None')
+ expect_no_validation_error('test' => 'Any')
+ expect_no_validation_error('test' => 'none')
+ expect_no_validation_error('test' => 'any')
+ expect_no_validation_error('test' => 'started')
+ expect_no_validation_error('test' => 'CURRENT')
+ end
+
+ context 'when custom values is empty and value is an integer' do
+ let(:custom_values) { [] }
+
+ it 'does not raise a validation error' do
+ expect_no_validation_error({ 'test' => 5 })
+ end
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'raises a validation error' do
+ expect_validation_error({ 'test' => 'Upcomming' })
+ end
+
+ context 'when custom values is empty and value is not an integer' do
+ let(:custom_values) { [] }
+
+ it 'raises a validation error' do
+ expect_validation_error({ 'test' => '5' })
+ end
+ end
+ end
+end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index cefd1fa3274..6a161854dfb 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -7,8 +7,10 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') }
+ let_it_be(:project) { create_default(:project, :repository) }
+
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
describe '.generate_update_sequence_id' do
@@ -19,41 +21,158 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- describe '#store_dev_info' do
- let_it_be(:project) { create_default(:project, :repository) }
- let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
+ describe '#send_info' do
+ it 'calls store_build_info and store_dev_info as appropriate' do
+ expect(subject).to receive(:store_build_info).with(
+ project: project,
+ update_sequence_id: :x,
+ pipelines: :y
+ ).and_return(:build_stored)
+
+ expect(subject).to receive(:store_dev_info).with(
+ project: project,
+ update_sequence_id: :x,
+ commits: :a,
+ branches: :b,
+ merge_requests: :c
+ ).and_return(:dev_stored)
+
+ args = {
+ project: project,
+ update_sequence_id: :x,
+ commits: :a,
+ branches: :b,
+ merge_requests: :c,
+ pipelines: :y
+ }
+
+ expect(subject.send_info(**args)).to contain_exactly(:dev_stored, :build_stored)
+ end
- let(:expected_jwt) do
- Atlassian::Jwt.encode(
- Atlassian::Jwt.build_claims(
- Atlassian::JiraConnect.app_key,
- '/rest/devinfo/0.10/bulk',
- 'POST'
- ),
- 'sample_secret'
- )
+ it 'only calls methods that we need to call' do
+ expect(subject).to receive(:store_dev_info).with(
+ project: project,
+ update_sequence_id: :x,
+ commits: :a
+ ).and_return(:dev_stored)
+
+ args = {
+ project: project,
+ update_sequence_id: :x,
+ commits: :a
+ }
+
+ expect(subject.send_info(**args)).to contain_exactly(:dev_stored)
+ end
+
+ it 'raises an argument error if there is nothing to send (probably a typo?)' do
+ expect { subject.send_info(project: project, builds: :x) }
+ .to raise_error(ArgumentError)
+ end
+ end
+
+ def expected_headers(path)
+ expected_jwt = Atlassian::Jwt.encode(
+ Atlassian::Jwt.build_claims(Atlassian::JiraConnect.app_key, path, 'POST'),
+ 'sample_secret'
+ )
+
+ {
+ 'Authorization' => "JWT #{expected_jwt}",
+ 'Content-Type' => 'application/json'
+ }
+ end
+
+ describe '#store_build_info' do
+ let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) }
+ let_it_be(:mrs_by_branch) { create_list(:merge_request, 2, :jira_branch) }
+ let_it_be(:red_herrings) { create_list(:merge_request, 1, :unique_branches) }
+
+ let_it_be(:pipelines) do
+ (red_herrings + mrs_by_branch + mrs_by_title).map do |mr|
+ create(:ci_pipeline, merge_request: mr)
+ end
+ end
+
+ let(:build_info_payload_schema) do
+ Atlassian::Schemata.build_info_payload
+ end
+
+ let(:body) do
+ matcher = be_valid_json.according_to_schema(build_info_payload_schema)
+
+ ->(text) { matcher.matches?(text) }
end
before do
- stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post)
- .with(
- headers: {
- 'Authorization' => "JWT #{expected_jwt}",
- 'Content-Type' => 'application/json'
- }
- )
+ path = '/rest/builds/0.1/bulk'
+ stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ .with(body: body, headers: expected_headers(path))
+ end
+
+ it "calls the API with auth headers" do
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end
+
+ it 'only sends information about relevant MRs' do
+ expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: have_attributes(size: 6) })
+
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end
+
+ it 'does not call the API if there is nothing to report' do
+ expect(subject).not_to receive(:post)
+
+ subject.send(:store_build_info, project: project, pipelines: pipelines.take(1))
+ end
+
+ it 'does not call the API if the feature flag is not enabled' do
+ stub_feature_flags(jira_sync_builds: false)
+
+ expect(subject).not_to receive(:post)
+
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end
+
+ it 'does call the API if the feature flag enabled for the project' do
+ stub_feature_flags(jira_sync_builds: project)
+
+ expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: Array })
+
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end
+
+ it 'avoids N+1 database queries' do
+ baseline = ActiveRecord::QueryRecorder.new do
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end
+
+ pipelines << create(:ci_pipeline, head_pipeline_of: create(:merge_request, :jira_branch))
+
+ expect { subject.send(:store_build_info, project: project, pipelines: pipelines) }.not_to exceed_query_limit(baseline)
+ end
+ end
+
+ describe '#store_dev_info' do
+ let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
+
+ before do
+ path = '/rest/devinfo/0.10/bulk'
+
+ stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ .with(headers: expected_headers(path))
end
it "calls the API with auth headers" do
- subject.store_dev_info(project: project)
+ subject.send(:store_dev_info, project: project)
end
it 'avoids N+1 database queries' do
- control_count = ActiveRecord::QueryRecorder.new { subject.store_dev_info(project: project, merge_requests: merge_requests) }.count
+ control_count = ActiveRecord::QueryRecorder.new { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.count
merge_requests << create(:merge_request, :unique_branches)
- expect { subject.store_dev_info(project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
+ expect { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
end
end
end
diff --git a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
new file mode 100644
index 00000000000..52e475d20ca
--- /dev/null
+++ b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:project) { create_default(:project) }
+
+ subject { described_class.represent(pipeline) }
+
+ context 'when the pipeline does not belong to any Jira issue' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ describe '#issue_keys' do
+ it 'is empty' do
+ expect(subject.issue_keys).to be_empty
+ end
+ end
+
+ describe '#to_json' do
+ it 'can encode the object' do
+ expect(subject.to_json).to be_valid_json
+ end
+
+ it 'is invalid, since it has no issue keys' do
+ expect(subject.to_json).not_to be_valid_json.according_to_schema(Atlassian::Schemata.build_info)
+ end
+ end
+ end
+
+ context 'when the pipeline does belong to a Jira issue' do
+ let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
+
+ %i[jira_branch jira_title].each do |trait|
+ context "because it belongs to an MR with a #{trait}" do
+ let(:merge_request) { create(:merge_request, trait) }
+
+ describe '#issue_keys' do
+ it 'is not empty' do
+ expect(subject.issue_keys).not_to be_empty
+ end
+ end
+
+ describe '#to_json' do
+ it 'is valid according to the build info schema' do
+ expect(subject.to_json).to be_valid_json.according_to_schema(Atlassian::Schemata.build_info)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index dbc04704fba..450e396a389 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -149,13 +149,27 @@ RSpec.describe Backup::Files do
end
it 'excludes tmp dirs from rsync' do
- expect(Gitlab::Popen).to receive(:popen).with(%w(rsync -a --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)).and_return(['', 0])
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .and_return(['', 0])
subject.dump
end
+ it 'retries if rsync fails due to vanishing files' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .and_return(['rsync failed', 24], ['', 0])
+
+ expect do
+ subject.dump
+ end.to output(/files vanished during rsync, retrying/).to_stdout
+ end
+
it 'raises an error and outputs an error message if rsync failed' do
- allow(Gitlab::Popen).to receive(:popen).with(%w(rsync -a --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)).and_return(['rsync failed', 1])
+ allow(Gitlab::Popen).to receive(:popen)
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .and_return(['rsync failed', 1])
expect do
subject.dump
diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb
index 9c139e9f954..492058c6a00 100644
--- a/spec/lib/backup/repositories_spec.rb
+++ b/spec/lib/backup/repositories_spec.rb
@@ -242,7 +242,9 @@ RSpec.describe Backup::Repositories do
# 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo
expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args|
- repository = method.call(*original_args)
+ full_path, container, kwargs = original_args
+
+ repository = method.call(full_path, container, **kwargs)
expect(repository).to receive(:remove)
diff --git a/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb
new file mode 100644
index 00000000000..272b4386ec8
--- /dev/null
+++ b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::AsciiDocSanitizationFilter do
+ include FilterSpecHelper
+
+ it 'preserves footnotes refs' do
+ result = filter('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>').to_html
+ expect(result).to eq('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>')
+ end
+
+ it 'preserves footnotes defs' do
+ result = filter('<div id="_footnotedef_1">
+<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>').to_html
+ expect(result).to eq(%(<div id="_footnotedef_1">
+<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>))
+ end
+
+ it 'preserves user-content- prefixed ids on anchors' do
+ result = filter('<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html
+ expect(result).to eq(%(<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>))
+ end
+
+ it 'preserves user-content- prefixed ids on div (blocks)' do
+ html_content = <<~HTML
+ <div id="user-content-open-block" class="openblock">
+ <div class="content">
+ <div class="paragraph">
+ <p>This is an open block</p>
+ </div>
+ </div>
+ </div>
+ HTML
+ output = <<~SANITIZED_HTML
+ <div id="user-content-open-block">
+ <div>
+ <div>
+ <p>This is an open block</p>
+ </div>
+ </div>
+ </div>
+ SANITIZED_HTML
+ expect(filter(html_content).to_html).to eq(output)
+ end
+
+ it 'preserves section anchor ids' do
+ result = filter(%(<h2 id="user-content-first-section">
+<a class="anchor" href="#user-content-first-section"></a>First section</h2>)).to_html
+ expect(result).to eq(%(<h2 id="user-content-first-section">
+<a class="anchor" href="#user-content-first-section"></a>First section</h2>))
+ end
+
+ it 'removes non prefixed ids' do
+ result = filter('<p><a id="cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html
+ expect(result).to eq(%(<p><a></a>A link to another location within an AsciiDoc document.</p>))
+ end
+end
diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb
new file mode 100644
index 00000000000..57caba1d4d7
--- /dev/null
+++ b/spec/lib/banzai/filter/kroki_filter_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::KrokiFilter do
+ include FilterSpecHelper
+
+ it 'replaces nomnoml pre tag with img tag if kroki is enabled' do
+ stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000")
+ doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>")
+
+ expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">'
+ end
+
+ it 'replaces nomnoml pre tag with img tag if both kroki and plantuml are enabled' do
+ stub_application_setting(kroki_enabled: true,
+ kroki_url: "http://localhost:8000",
+ plantuml_enabled: true,
+ plantuml_url: "http://localhost:8080")
+ doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>")
+
+ expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">'
+ end
+
+ it 'does not replace nomnoml pre tag with img tag if kroki is disabled' do
+ stub_application_setting(kroki_enabled: false)
+ doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>")
+
+ expect(doc.to_s).to eq "<pre lang=\"nomnoml\"><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:&gt;[foul mouth]\n]</code></pre>"
+ end
+
+ it 'does not replace plantuml pre tag with img tag if both kroki and plantuml are enabled' do
+ stub_application_setting(kroki_enabled: true,
+ kroki_url: "http://localhost:8000",
+ plantuml_enabled: true,
+ plantuml_url: "http://localhost:8080")
+ doc = filter("<pre lang='plantuml'><code>Bob->Alice : hello</code></pre>")
+
+ expect(doc.to_s).to eq '<pre lang="plantuml"><code>Bob-&gt;Alice : hello</code></pre>'
+ end
+end
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index 8d01a651651..c5e84a0c1e7 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -46,6 +46,12 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
expect(result).to start_with('<pre><code lang="日">')
end
+
+ it 'works with additional language parameters' do
+ result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true)
+
+ expect(result).to start_with('<pre><code lang="ruby:red gem">')
+ end
end
end
diff --git a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
index d8841a9753e..74005adf673 100644
--- a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline do
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
- it 'converts text in Atlassian Document Format ' do
+ it 'converts text in Atlassian Document Format' do
source = fixture_file(File.join(fixtures_path, 'paragraph.json'))
target = fixture_file(File.join(fixtures_path, 'paragraph.md'))
output = described_class.call(source, {})[:output]
diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
index cde8e2d5c18..a7a19fb73fc 100644
--- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
@@ -41,12 +41,11 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
end
context 'when variables are present' do
- let(:query) { { query: double(to_s: 'test', variables: { full_path: :source_full_path }) } }
+ let(:variables) { { foo: :bar } }
+ let(:query) { { query: double(to_s: 'test', variables: variables) } }
it 'builds graphql query variables for import entity' do
- expected_variables = { full_path: import_entity.source_full_path }
-
- expect(graphql_client).to receive(:execute).with(anything, expected_variables)
+ expect(graphql_client).to receive(:execute).with(anything, variables)
subject.extract(context).first
end
diff --git a/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb
deleted file mode 100644
index 8f39b6e7c93..00000000000
--- a/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Common::Transformers::GraphqlCleanerTransformer do
- describe '#transform' do
- let_it_be(:expected_output) do
- {
- 'name' => 'test',
- 'fullName' => 'test',
- 'description' => 'test',
- 'labels' => [
- { 'title' => 'label1' },
- { 'title' => 'label2' },
- { 'title' => 'label3' }
- ]
- }
- end
-
- it 'deep cleans hash from GraphQL keys' do
- data = {
- 'data' => {
- 'group' => {
- 'name' => 'test',
- 'fullName' => 'test',
- 'description' => 'test',
- 'labels' => {
- 'edges' => [
- { 'node' => { 'title' => 'label1' } },
- { 'node' => { 'title' => 'label2' } },
- { 'node' => { 'title' => 'label3' } }
- ]
- }
- }
- }
- }
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to eq(expected_output)
- end
-
- context 'when data does not have data/group nesting' do
- it 'deep cleans hash from GraphQL keys' do
- data = {
- 'name' => 'test',
- 'fullName' => 'test',
- 'description' => 'test',
- 'labels' => {
- 'edges' => [
- { 'node' => { 'title' => 'label1' } },
- { 'node' => { 'title' => 'label2' } },
- { 'node' => { 'title' => 'label3' } }
- ]
- }
- }
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to eq(expected_output)
- end
- end
-
- context 'when data is not a hash' do
- it 'does not perform transformation' do
- data = 'test'
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to eq(data)
- end
- end
-
- context 'when nested data is not an array or hash' do
- it 'only removes top level data/group keys' do
- data = {
- 'data' => {
- 'group' => 'test'
- }
- }
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to eq('test')
- end
- end
- end
-end
diff --git a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb
new file mode 100644
index 00000000000..2b33701653e
--- /dev/null
+++ b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Transformers::HashKeyDigger do
+ describe '#transform' do
+ it 'when the key_path is an array' do
+ data = { foo: { bar: :value } }
+ key_path = %i[foo bar]
+ transformed = described_class.new(key_path: key_path).transform(nil, data)
+
+ expect(transformed).to eq(:value)
+ end
+
+ it 'when the key_path is not an array' do
+ data = { foo: { bar: :value } }
+ key_path = :foo
+ transformed = described_class.new(key_path: key_path).transform(nil, data)
+
+ expect(transformed).to eq({ bar: :value })
+ end
+
+ it "when the data is not a hash" do
+ expect { described_class.new(key_path: nil).transform(nil, nil) }
+ .to raise_error(ArgumentError, "Given data must be a Hash")
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb
new file mode 100644
index 00000000000..03d138b227c
--- /dev/null
+++ b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Transformers::ProhibitedAttributesTransformer do
+ describe '#transform' do
+ let_it_be(:hash) do
+ {
+ 'id' => 101,
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99,
+ 'import_source' => 'test',
+ 'import_type' => 'test',
+ 'non_existent_attr' => 'test',
+ 'some_html' => '<p>dodgy html</p>',
+ 'legit_html' => '<p>legit html</p>',
+ '_html' => '<p>perfectly ordinary html</p>',
+ 'cached_markdown_version' => 12345,
+ 'custom_attributes' => 'test',
+ 'some_attributes_metadata' => 'test',
+ 'group_id' => 99,
+ 'commit_id' => 99,
+ 'issue_ids' => [1, 2, 3],
+ 'merge_request_ids' => [1, 2, 3],
+ 'note_ids' => [1, 2, 3],
+ 'remote_attachment_url' => 'http://something.dodgy',
+ 'remote_attachment_request_header' => 'bad value',
+ 'remote_attachment_urls' => %w(http://something.dodgy http://something.okay),
+ 'attributes' => {
+ 'issue_ids' => [1, 2, 3],
+ 'merge_request_ids' => [1, 2, 3],
+ 'note_ids' => [1, 2, 3]
+ },
+ 'variables_attributes' => {
+ 'id' => 1
+ },
+ 'attr_with_nested_attrs' => {
+ 'nested_id' => 1,
+ 'nested_attr' => 2
+ }
+ }
+ end
+
+ let(:expected_hash) do
+ {
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99,
+ 'import_source' => 'test',
+ 'import_type' => 'test',
+ 'non_existent_attr' => 'test',
+ 'attr_with_nested_attrs' => {
+ 'nested_attr' => 2
+ }
+ }
+ end
+
+ it 'removes prohibited attributes' do
+ transformed_hash = subject.transform(nil, hash)
+
+ expect(transformed_hash).to eq(expected_hash)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
index 3949dd23b49..c9b481388c3 100644
--- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
@@ -72,7 +72,6 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
describe 'pipeline parts' do
it { expect(described_class).to include_module(BulkImports::Pipeline) }
- it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) }
it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
it 'has extractors' do
@@ -90,13 +89,17 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
it 'has transformers' do
expect(described_class.transformers)
.to contain_exactly(
- { klass: BulkImports::Common::Transformers::GraphqlCleanerTransformer, options: nil },
+ { klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group] } },
{ klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil },
- { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil })
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
+ { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil }
+ )
end
it 'has loaders' do
- expect(described_class.loaders).to contain_exactly({ klass: BulkImports::Groups::Loaders::GroupLoader, options: nil })
+ expect(described_class.loaders).to contain_exactly({
+ klass: BulkImports::Groups::Loaders::GroupLoader, options: nil
+ })
end
end
end
diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
index 60a4a796682..788a6e98c45 100644
--- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
@@ -55,7 +55,6 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
describe 'pipeline parts' do
it { expect(described_class).to include_module(BulkImports::Pipeline) }
- it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) }
it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
it 'has extractors' do
@@ -67,8 +66,8 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
it 'has transformers' do
expect(described_class.transformers).to contain_exactly(
- klass: BulkImports::Groups::Transformers::SubgroupToEntityTransformer,
- options: nil
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
+ { klass: BulkImports::Groups::Transformers::SubgroupToEntityTransformer, options: nil }
)
end
diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb
index 95ac5925c97..95dca7fc486 100644
--- a/spec/lib/bulk_imports/importers/group_importer_spec.rb
+++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb
@@ -18,12 +18,12 @@ RSpec.describe BulkImports::Importers::GroupImporter do
subject { described_class.new(bulk_import_entity) }
before do
+ allow(Gitlab).to receive(:ee?).and_return(false)
allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context)
- stub_http_requests
end
describe '#execute' do
- it "starts the entity and run its pipelines" do
+ it 'starts the entity and run its pipelines' do
expect(bulk_import_entity).to receive(:start).and_call_original
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
@@ -32,6 +32,18 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect(bulk_import_entity.reload).to be_finished
end
+
+ context 'when failed' do
+ let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) }
+
+ it 'does not transition entity to finished state' do
+ allow(bulk_import_entity).to receive(:start!)
+
+ subject.execute
+
+ expect(bulk_import_entity.reload).to be_failed
+ end
+ end
end
def expect_to_run_pipeline(klass, context:)
@@ -39,18 +51,4 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect(pipeline).to receive(:run).with(context)
end
end
-
- def stub_http_requests
- double_response = double(
- code: 200,
- success?: true,
- parsed_response: {},
- headers: {}
- )
-
- allow_next_instance_of(BulkImports::Clients::Http) do |client|
- allow(client).to receive(:get).and_return(double_response)
- allow(client).to receive(:post).and_return(double_response)
- end
- end
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 8c882c799ec..60833e83dcc 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -3,26 +3,32 @@
require 'spec_helper'
RSpec.describe BulkImports::Pipeline::Runner do
- describe 'pipeline runner' do
- before do
- extractor = Class.new do
- def initialize(options = {}); end
+ let(:extractor) do
+ Class.new do
+ def initialize(options = {}); end
- def extract(context); end
- end
+ def extract(context); end
+ end
+ end
- transformer = Class.new do
- def initialize(options = {}); end
+ let(:transformer) do
+ Class.new do
+ def initialize(options = {}); end
- def transform(context, entry); end
- end
+ def transform(context); end
+ end
+ end
- loader = Class.new do
- def initialize(options = {}); end
+ let(:loader) do
+ Class.new do
+ def initialize(options = {}); end
- def load(context, entry); end
- end
+ def load(context); end
+ end
+ end
+ describe 'pipeline runner' do
+ before do
stub_const('BulkImports::Extractor', extractor)
stub_const('BulkImports::Transformer', transformer)
stub_const('BulkImports::Loader', loader)
@@ -38,37 +44,126 @@ RSpec.describe BulkImports::Pipeline::Runner do
stub_const('BulkImports::MyPipeline', pipeline)
end
- it 'runs pipeline extractor, transformer, loader' do
- context = instance_double(
- BulkImports::Pipeline::Context,
- entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group')
- )
- entries = [{ foo: :bar }]
-
- expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor).to receive(:extract).with(context).and_return(entries)
+ context 'when entity is not marked as failed' do
+ let(:context) do
+ instance_double(
+ BulkImports::Pipeline::Context,
+ entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: false)
+ )
end
- expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first)
+ it 'runs pipeline extractor, transformer, loader' do
+ entries = [{ foo: :bar }]
+
+ expect_next_instance_of(BulkImports::Extractor) do |extractor|
+ expect(extractor).to receive(:extract).with(context).and_return(entries)
+ end
+
+ expect_next_instance_of(BulkImports::Transformer) do |transformer|
+ expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first)
+ end
+
+ expect_next_instance_of(BulkImports::Loader) do |loader|
+ expect(loader).to receive(:load).with(context, entries.first)
+ end
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:info)
+ .with(
+ message: 'Pipeline started',
+ pipeline_class: 'BulkImports::MyPipeline',
+ bulk_import_entity_id: 1,
+ bulk_import_entity_type: 'group'
+ )
+ expect(logger).to receive(:info)
+ .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', extractor: 'BulkImports::Extractor')
+ expect(logger).to receive(:info)
+ .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', transformer: 'BulkImports::Transformer')
+ expect(logger).to receive(:info)
+ .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', loader: 'BulkImports::Loader')
+ end
+
+ BulkImports::MyPipeline.new.run(context)
end
- expect_next_instance_of(BulkImports::Loader) do |loader|
- expect(loader).to receive(:load).with(context, entries.first)
+ context 'when exception is raised' do
+ let(:entity) { create(:bulk_import_entity, :created) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity: entity) }
+
+ before do
+ allow_next_instance_of(BulkImports::Extractor) do |extractor|
+ allow(extractor).to receive(:extract).with(context).and_raise(StandardError, 'Error!')
+ end
+ end
+
+ it 'logs import failure' do
+ BulkImports::MyPipeline.new.run(context)
+
+ failure = entity.failures.first
+
+ expect(failure).to be_present
+ expect(failure.pipeline_class).to eq('BulkImports::MyPipeline')
+ expect(failure.exception_class).to eq('StandardError')
+ expect(failure.exception_message).to eq('Error!')
+ end
+
+ context 'when pipeline is marked to abort on failure' do
+ before do
+ BulkImports::MyPipeline.abort_on_failure!
+ end
+
+ it 'marks entity as failed' do
+ BulkImports::MyPipeline.new.run(context)
+
+ expect(entity.failed?).to eq(true)
+ end
+
+ it 'logs warn message' do
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:warn)
+ .with(
+ message: 'Pipeline failed',
+ pipeline_class: 'BulkImports::MyPipeline',
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: entity.source_type
+ )
+ end
+
+ BulkImports::MyPipeline.new.run(context)
+ end
+ end
+
+ context 'when pipeline is not marked to abort on failure' do
+ it 'marks entity as failed' do
+ BulkImports::MyPipeline.new.run(context)
+
+ expect(entity.failed?).to eq(false)
+ end
+ end
end
+ end
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger).to receive(:info)
- .with(message: "Pipeline started", pipeline: 'BulkImports::MyPipeline', entity: 1, entity_type: 'group')
- expect(logger).to receive(:info)
- .with(entity: 1, entity_type: 'group', extractor: 'BulkImports::Extractor')
- expect(logger).to receive(:info)
- .with(entity: 1, entity_type: 'group', transformer: 'BulkImports::Transformer')
- expect(logger).to receive(:info)
- .with(entity: 1, entity_type: 'group', loader: 'BulkImports::Loader')
+ context 'when entity is marked as failed' do
+ let(:context) do
+ instance_double(
+ BulkImports::Pipeline::Context,
+ entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: true)
+ )
end
- BulkImports::MyPipeline.new.run(context)
+ it 'logs and returns without execution' do
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:info)
+ .with(
+ message: 'Skipping due to failed pipeline status',
+ pipeline_class: 'BulkImports::MyPipeline',
+ bulk_import_entity_id: 1,
+ bulk_import_entity_type: 'group'
+ )
+ end
+
+ BulkImports::MyPipeline.new.run(context)
+ end
end
end
end
diff --git a/spec/lib/bulk_imports/pipeline/attributes_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb
index 54c5dbd4cae..94052be7df2 100644
--- a/spec/lib/bulk_imports/pipeline/attributes_spec.rb
+++ b/spec/lib/bulk_imports/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Pipeline::Attributes do
+RSpec.describe BulkImports::Pipeline do
describe 'pipeline attributes' do
before do
stub_const('BulkImports::Extractor', Class.new)
@@ -10,7 +10,9 @@ RSpec.describe BulkImports::Pipeline::Attributes do
stub_const('BulkImports::Loader', Class.new)
klass = Class.new do
- include BulkImports::Pipeline::Attributes
+ include BulkImports::Pipeline
+
+ abort_on_failure!
extractor BulkImports::Extractor, { foo: :bar }
transformer BulkImports::Transformer, { foo: :bar }
@@ -25,6 +27,7 @@ RSpec.describe BulkImports::Pipeline::Attributes do
expect(BulkImports::MyPipeline.extractors).to contain_exactly({ klass: BulkImports::Extractor, options: { foo: :bar } })
expect(BulkImports::MyPipeline.transformers).to contain_exactly({ klass: BulkImports::Transformer, options: { foo: :bar } })
expect(BulkImports::MyPipeline.loaders).to contain_exactly({ klass: BulkImports::Loader, options: { foo: :bar } })
+ expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true)
end
end
@@ -36,6 +39,7 @@ RSpec.describe BulkImports::Pipeline::Attributes do
BulkImports::MyPipeline.extractor(klass, options)
BulkImports::MyPipeline.transformer(klass, options)
BulkImports::MyPipeline.loader(klass, options)
+ BulkImports::MyPipeline.abort_on_failure!
expect(BulkImports::MyPipeline.extractors)
.to contain_exactly(
@@ -51,6 +55,8 @@ RSpec.describe BulkImports::Pipeline::Attributes do
.to contain_exactly(
{ klass: BulkImports::Loader, options: { foo: :bar } },
{ klass: klass, options: options })
+
+ expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true)
end
end
end
diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb
index fa0207d829a..21120012927 100644
--- a/spec/lib/feature/definition_spec.rb
+++ b/spec/lib/feature/definition_spec.rb
@@ -64,6 +64,11 @@ RSpec.describe Feature::Definition do
expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) }
.to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/)
end
+
+ it 'allows passing `default_enabled: :yaml`' do
+ expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: :yaml) }
+ .not_to raise_error
+ end
end
end
@@ -75,7 +80,7 @@ RSpec.describe Feature::Definition do
describe '.load_from_file' do
it 'properly loads a definition from file' do
- expect(File).to receive(:read).with(path) { yaml_content }
+ expect_file_read(path, content: yaml_content)
expect(described_class.send(:load_from_file, path).attributes)
.to eq(definition.attributes)
@@ -93,7 +98,7 @@ RSpec.describe Feature::Definition do
context 'for invalid definition' do
it 'raises exception' do
- expect(File).to receive(:read).with(path) { '{}' }
+ expect_file_read(path, content: '{}')
expect do
described_class.send(:load_from_file, path)
@@ -209,4 +214,58 @@ RSpec.describe Feature::Definition do
end
end
end
+
+ describe '.defaul_enabled?' do
+ subject { described_class.default_enabled?(key) }
+
+ context 'when feature flag exist' do
+ let(:key) { definition.key }
+
+ before do
+ allow(described_class).to receive(:definitions) do
+ { definition.key => definition }
+ end
+ end
+
+ context 'when default_enabled is true' do
+ it 'returns the value from the definition' do
+ expect(subject).to eq(true)
+ end
+ end
+
+ context 'when default_enabled is false' do
+ let(:attributes) do
+ { name: 'feature_flag',
+ type: 'development',
+ default_enabled: false }
+ end
+
+ it 'returns the value from the definition' do
+ expect(subject).to eq(false)
+ end
+ end
+ end
+
+ context 'when feature flag does not exist' do
+ let(:key) { :unknown_feature_flag }
+
+ context 'when on dev or test environment' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Feature::InvalidFeatureFlagError,
+ "The feature flag YAML definition for 'unknown_feature_flag' does not exist")
+ end
+ end
+
+ context 'when on production environment' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(subject).to eq(false)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 5dff9dbd995..1bcb2223012 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -249,10 +249,12 @@ RSpec.describe Feature, stub_feature_flags: false do
Feature::Definition.new('development/my_feature_flag.yml',
name: 'my_feature_flag',
type: 'development',
- default_enabled: false
+ default_enabled: default_enabled
).tap(&:validate!)
end
+ let(:default_enabled) { false }
+
before do
stub_env('LAZILY_CREATE_FEATURE_FLAG', '0')
@@ -275,6 +277,63 @@ RSpec.describe Feature, stub_feature_flags: false do
expect { described_class.enabled?(:my_feature_flag, default_enabled: true) }
.to raise_error(/The `default_enabled:` of/)
end
+
+ context 'when `default_enabled: :yaml` is used in code' do
+ it 'reads the default from the YAML definition' do
+ expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(false)
+ end
+
+ context 'when default_enabled is true in the YAML definition' do
+ let(:default_enabled) { true }
+
+ it 'reads the default from the YAML definition' do
+ expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(true)
+ end
+ end
+
+ context 'when YAML definition does not exist for an optional type' do
+ let(:optional_type) { described_class::Shared::TYPES.find { |name, attrs| attrs[:optional] }.first }
+
+ context 'when in dev or test environment' do
+ it 'raises an error for dev' do
+ expect { described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml) }
+ .to raise_error(
+ Feature::InvalidFeatureFlagError,
+ "The feature flag YAML definition for 'non_existent_flag' does not exist")
+ end
+ end
+
+ context 'when in production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ context 'when database exists' do
+ before do
+ allow(Gitlab::Database).to receive(:exists?).and_return(true)
+ end
+
+ it 'checks the persisted status and returns false' do
+ expect(described_class).to receive(:get).with(:non_existent_flag).and_call_original
+
+ expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
+ end
+ end
+
+ context 'when database does not exist' do
+ before do
+ allow(Gitlab::Database).to receive(:exists?).and_return(false)
+ end
+
+ it 'returns false without checking the status in the database' do
+ expect(described_class).not_to receive(:get)
+
+ expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
+ end
+ end
+ end
+ end
+ end
end
end
@@ -300,7 +359,119 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
+ shared_examples_for 'logging' do
+ let(:expected_action) { }
+ let(:expected_extra) { }
+
+ it 'logs the event' do
+ expect(Feature.logger).to receive(:info).with(key: key, action: expected_action, **expected_extra)
+
+ subject
+ end
+ end
+
+ describe '.enable' do
+ subject { described_class.enable(key, thing) }
+
+ let(:key) { :awesome_feature }
+ let(:thing) { true }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :enable }
+ let(:expected_extra) { { "extra.thing" => "true" } }
+ end
+
+ context 'when thing is an actor' do
+ let(:thing) { create(:project) }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :enable }
+ let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } }
+ end
+ end
+ end
+
+ describe '.disable' do
+ subject { described_class.disable(key, thing) }
+
+ let(:key) { :awesome_feature }
+ let(:thing) { false }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :disable }
+ let(:expected_extra) { { "extra.thing" => "false" } }
+ end
+
+ context 'when thing is an actor' do
+ let(:thing) { create(:project) }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :disable }
+ let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } }
+ end
+ end
+ end
+
+ describe '.enable_percentage_of_time' do
+ subject { described_class.enable_percentage_of_time(key, percentage) }
+
+ let(:key) { :awesome_feature }
+ let(:percentage) { 50 }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :enable_percentage_of_time }
+ let(:expected_extra) { { "extra.percentage" => "#{percentage}" } }
+ end
+ end
+
+ describe '.disable_percentage_of_time' do
+ subject { described_class.disable_percentage_of_time(key) }
+
+ let(:key) { :awesome_feature }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :disable_percentage_of_time }
+ let(:expected_extra) { {} }
+ end
+ end
+
+ describe '.enable_percentage_of_actors' do
+ subject { described_class.enable_percentage_of_actors(key, percentage) }
+
+ let(:key) { :awesome_feature }
+ let(:percentage) { 50 }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :enable_percentage_of_actors }
+ let(:expected_extra) { { "extra.percentage" => "#{percentage}" } }
+ end
+ end
+
+ describe '.disable_percentage_of_actors' do
+ subject { described_class.disable_percentage_of_actors(key) }
+
+ let(:key) { :awesome_feature }
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :disable_percentage_of_actors }
+ let(:expected_extra) { {} }
+ end
+ end
+
describe '.remove' do
+ subject { described_class.remove(key) }
+
+ let(:key) { :awesome_feature }
+
+ before do
+ described_class.enable(key)
+ end
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { :remove }
+ let(:expected_extra) { {} }
+ end
+
context 'for a non-persisted feature' do
it 'returns nil' do
expect(described_class.remove(:non_persisted_feature_flag)).to be_nil
diff --git a/spec/lib/gitlab/anonymous_session_spec.rb b/spec/lib/gitlab/anonymous_session_spec.rb
index 671d452ad13..245ca02e91a 100644
--- a/spec/lib/gitlab/anonymous_session_spec.rb
+++ b/spec/lib/gitlab/anonymous_session_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do
end
it 'adds expiration time to key' do
- Timecop.freeze do
+ freeze_time do
subject.count_session_ip
Gitlab::Redis::SharedState.with do |redis|
diff --git a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
new file mode 100644
index 00000000000..84c2cda496e
--- /dev/null
+++ b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Asciidoc::Html5Converter do
+ describe 'convert AsciiDoc to HTML5' do
+ it 'appends user-content- prefix on ref (anchor)' do
+ doc = Asciidoctor::Document.new('')
+ anchor = Asciidoctor::Inline.new(doc, :anchor, '', type: :ref, id: 'cross-references')
+ converter = Gitlab::Asciidoc::Html5Converter.new('gitlab_html5')
+ html = converter.convert_inline_anchor(anchor)
+ expect(html).to eq('<a id="user-content-cross-references"></a>')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 6b93634690c..36e4decdead 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -20,7 +20,7 @@ module Gitlab
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS,
+ attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
extensions: be_a(Proc)
}
@@ -35,7 +35,7 @@ module Gitlab
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS,
+ attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
extensions: be_a(Proc)
}
@@ -252,6 +252,27 @@ module Gitlab
end
end
+ context 'with xrefs' do
+ it 'preserves ids' do
+ input = <<~ADOC
+ Learn how to xref:cross-references[use cross references].
+
+ [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
+ </div>
+ <div>
+ <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+
context 'with checklist' do
it 'preserves classes' do
input = <<~ADOC
@@ -462,6 +483,34 @@ module Gitlab
expect(render(input, context)).to include(output.strip)
end
end
+
+ context 'with Kroki enabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
+ end
+
+ it 'converts a graphviz diagram to image' do
+ input = <<~ADOC
+ [graphviz]
+ ....
+ digraph G {
+ Hello->World
+ }
+ ....
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
end
context 'with project' do
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 3c19ef0bd1b..f927d5912bb 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -147,6 +147,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_user_from_feed_token(:rss)).to eq user
end
+ it 'returns nil if valid feed_token and disabled' do
+ allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
+ set_param(:feed_token, user.feed_token)
+
+ expect(find_user_from_feed_token(:rss)).to be_nil
+ end
+
it 'returns nil if feed_token is blank' do
expect(find_user_from_feed_token(:rss)).to be_nil
end
@@ -377,6 +384,16 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
+
+ context 'when using a non-prefixed access token' do
+ let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
+
+ it 'returns user' do
+ set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+
+ expect(find_user_from_access_token).to eq user
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/auth/crowd/authentication_spec.rb b/spec/lib/gitlab/auth/crowd/authentication_spec.rb
new file mode 100644
index 00000000000..71eb8036fdd
--- /dev/null
+++ b/spec/lib/gitlab/auth/crowd/authentication_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Crowd::Authentication do
+ let(:provider) { 'crowd' }
+ let(:login) { generate(:username) }
+ let(:password) { 'password' }
+ let(:crowd_auth) { described_class.new(provider) }
+ let(:user_info) { { user: login } }
+
+ describe 'login' do
+ before do
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).with(provider).and_return(true)
+ allow(crowd_auth).to receive(:user_info_from_authentication).and_return(user_info)
+ end
+
+ it "finds the user if authentication is successful" do
+ create(:omniauth_user, extern_uid: login, username: login, provider: provider)
+
+ expect(crowd_auth.login(login, password)).to be_truthy
+ end
+
+ it "is false if the user does not exist" do
+ expect(crowd_auth.login(login, password)).to be_falsey
+ end
+
+ it "is false if the authentication fails" do
+ allow(crowd_auth).to receive(:user_info_from_authentication).and_return(nil)
+
+ expect(crowd_auth.login(login, password)).to be_falsey
+ end
+
+ it "fails when crowd is disabled" do
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).with('crowd').and_return(false)
+
+ expect(crowd_auth.login(login, password)).to be_falsey
+ end
+
+ it "fails if no login is supplied" do
+ expect(crowd_auth.login('', password)).to be_falsey
+ end
+
+ it "fails if no password is supplied" do
+ expect(crowd_auth.login(login, '')).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index ccaed94b5c8..e910ac09448 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -49,23 +49,6 @@ RSpec.describe Gitlab::Auth::Ldap::User do
end
end
- describe '.find_by_uid_and_provider' do
- let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
-
- it 'retrieves the correct user' do
- special_info = {
- name: 'John Åström',
- email: 'john@example.com',
- nickname: 'jastrom'
- }
- special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
- special_chars_user = described_class.new(special_hash)
- user = special_chars_user.save
-
- expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
- end
- end
-
describe 'find or create' do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 243d0a4cb45..6c6cee9c273 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -25,6 +25,23 @@ RSpec.describe Gitlab::Auth::OAuth::User do
let(:ldap_user) { Gitlab::Auth::Ldap::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+ describe '.find_by_uid_and_provider' do
+ let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
+
+ it 'retrieves the correct user' do
+ special_info = {
+ name: 'John Åström',
+ email: 'john@example.com',
+ nickname: 'jastrom'
+ }
+ special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
+ special_chars_user = described_class.new(special_hash)
+ user = special_chars_user.save
+
+ expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
+ end
+ end
+
describe '#persisted?' do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
diff --git a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
new file mode 100644
index 00000000000..928aade4008
--- /dev/null
+++ b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do
+ let_it_be(:key) { create(:key)}
+
+ describe '#update_session' do
+ it 'registers a session in Redis' do
+ redis = double(:redis)
+ expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
+
+ expect(redis).to(
+ receive(:setex)
+ .with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}",
+ described_class::DEFAULT_EXPIRATION,
+ true)
+ .once)
+
+ described_class.new(key).update_session
+ end
+ end
+
+ describe '#access_restricted?' do
+ subject { described_class.new(key).access_restricted? }
+
+ context 'with existing session' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
+ end
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'without an existing session' do
+ it { is_expected.to be_truthy }
+ end
+ 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
index 18fd6d08057..88a245b6b10 100644
--- a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb
@@ -12,30 +12,32 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do
let(:api_token) { 's3cr3t' }
let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" }
+ let(:response_status) { 200 }
subject(:validate) { described_class.new(user).validate(otp_code) }
before do
- stub_feature_flags(forti_authenticator: true)
+ stub_feature_flags(forti_authenticator: user)
stub_forti_authenticator_config(
+ enabled: true,
host: host,
port: port,
username: api_username,
- token: api_token
+ access_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: {})
+ .with(body: JSON(request_body),
+ headers: { 'Content-Type': 'application/json' },
+ basic_auth: [api_username, api_token])
+ .to_return(status: response_status, body: '')
end
context 'successful validation' do
- let(:response_status) { 200 }
-
it 'returns success' do
expect(validate[:status]).to eq(:success)
end
@@ -49,6 +51,16 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do
end
end
+ context 'unexpected error' do
+ it 'returns error' do
+ error_message = 'boom!'
+ stub_request(:post, forti_authenticator_auth_url).to_raise(StandardError.new(error_message))
+
+ expect(validate[:status]).to eq(:error)
+ expect(validate[:message]).to eq(error_message)
+ end
+ end
+
def stub_forti_authenticator_config(forti_authenticator_settings)
allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings))
end
diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
new file mode 100644
index 00000000000..1580fc82279
--- /dev/null
+++ b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
+ let_it_be(:user) { create(:user) }
+ let(:otp_code) { 42 }
+
+ let(:url) { 'https://ftc.example.com:9696/api/v1' }
+ let(:client_id) { 'client_id' }
+ let(:client_secret) { 's3cr3t' }
+ let(:access_token_create_url) { url + '/login' }
+ let(:otp_verification_url) { url + '/auth' }
+ let(:access_token) { 'an_access_token' }
+ let(:access_token_create_response_body) { '' }
+
+ subject(:validate) { described_class.new(user).validate(otp_code) }
+
+ before do
+ stub_feature_flags(forti_token_cloud: user)
+
+ stub_const("#{described_class}::BASE_API_URL", url)
+
+ stub_forti_token_cloud_config(
+ enabled: true,
+ client_id: client_id,
+ client_secret: client_secret
+ )
+
+ access_token_request_body = { client_id: client_id,
+ client_secret: client_secret }
+
+ stub_request(:post, access_token_create_url)
+ .with(body: JSON(access_token_request_body), headers: { 'Content-Type' => 'application/json' })
+ .to_return(
+ status: access_token_create_response_status,
+ body: Gitlab::Json.generate(access_token_create_response_body),
+ headers: {}
+ )
+ end
+
+ context 'access token is created successfully' do
+ let(:access_token_create_response_body) { { access_token: access_token, expires_in: 3600 } }
+ let(:access_token_create_response_status) { 201 }
+
+ before do
+ otp_verification_request_body = { username: user.username,
+ token: otp_code }
+
+ stub_request(:post, otp_verification_url)
+ .with(body: JSON(otp_verification_request_body),
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{access_token}"
+ })
+ .to_return(status: otp_verification_response_status, body: '', headers: {})
+ end
+
+ context 'otp verification is successful' do
+ let(:otp_verification_response_status) { 200 }
+
+ it 'returns success' do
+ expect(validate[:status]).to eq(:success)
+ end
+ end
+
+ context 'otp verification is not successful' do
+ let(:otp_verification_response_status) { 401 }
+
+ it 'returns error' do
+ expect(validate[:status]).to eq(:error)
+ end
+ end
+ end
+
+ context 'access token creation fails' do
+ let(:access_token_create_response_status) { 400 }
+
+ it 'returns error' do
+ expect(validate[:status]).to eq(:error)
+ end
+ end
+
+ def stub_forti_token_cloud_config(forti_token_cloud_settings)
+ allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings))
+ end
+end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index b89ceb37076..ef6b1d72712 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -50,13 +50,13 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user)
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
- expect(subject.find_sessionless_user([:api])).to eq access_token_user
+ expect(subject.find_sessionless_user(:api)).to eq access_token_user
end
it 'returns feed_token user if no access_token user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
- expect(subject.find_sessionless_user([:api])).to eq feed_token_user
+ expect(subject.find_sessionless_user(:api)).to eq feed_token_user
end
it 'returns static_object_token user if no feed_token user found' do
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
.to receive(:find_user_from_static_object_token)
.and_return(static_object_token_user)
- expect(subject.find_sessionless_user([:api])).to eq static_object_token_user
+ expect(subject.find_sessionless_user(:api)).to eq static_object_token_user
end
it 'returns job_token user if no static_object_token user found' do
@@ -72,17 +72,61 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
.to receive(:find_user_from_job_token)
.and_return(job_token_user)
- expect(subject.find_sessionless_user([:api])).to eq job_token_user
+ expect(subject.find_sessionless_user(:api)).to eq job_token_user
end
it 'returns nil if no user found' do
- expect(subject.find_sessionless_user([:api])).to be_blank
+ expect(subject.find_sessionless_user(:api)).to be_blank
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
- expect(subject.find_sessionless_user([:api])).to be_blank
+ expect(subject.find_sessionless_user(:api)).to be_blank
+ end
+ end
+
+ describe '#find_personal_access_token_from_http_basic_auth' do
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:user) { personal_access_token.user }
+
+ before do
+ allow(subject).to receive(:has_basic_credentials?).and_return(true)
+ allow(subject).to receive(:user_name_and_password).and_return([user.username, personal_access_token.token])
+ end
+
+ context 'with API requests' do
+ before do
+ env['SCRIPT_NAME'] = '/api/endpoint'
+ end
+
+ it 'tries to find the user' do
+ expect(subject.user([:api])).to eq user
+ end
+
+ it 'returns nil if the token is revoked' do
+ personal_access_token.revoke!
+
+ expect(subject.user([:api])).to be_blank
+ end
+
+ it 'returns nil if the token does not have API scope' do
+ personal_access_token.update!(scopes: ['read_registry'])
+
+ expect(subject.user([:api])).to be_blank
+ end
+ end
+
+ context 'without API requests' do
+ before do
+ env['SCRIPT_NAME'] = '/web/endpoint'
+ end
+
+ it 'does not search for job users' do
+ expect(PersonalAccessToken).not_to receive(:find_by_token)
+
+ expect(subject.user([:api])).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 1768ab41a71..dfd21983682 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -364,20 +364,33 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
let_it_be(:project_access_token) { create(:personal_access_token, user: project_bot_user) }
context 'with valid project access token' do
- before_all do
+ before do
project.add_maintainer(project_bot_user)
end
- it 'succeeds' do
+ it 'successfully authenticates the project bot' do
expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
.to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities))
end
end
context 'with invalid project access token' do
- it 'fails' do
- expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
- .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ context 'when project bot is not a project member' do
+ it 'fails for a non-project member' do
+ expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ end
+ end
+
+ context 'when project bot user is blocked' do
+ before do
+ project_bot_user.block!
+ end
+
+ it 'fails for a blocked project bot' do
+ expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
+ end
end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
index 2be9c03e5bd..54c14e7a4b8 100644
--- a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
@@ -9,10 +9,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy
it 'backfills deployment_cluster for all deployments in the given range with a non-null cluster_id' do
deployment_clusters = table(:deployment_clusters)
- namespace = table(:namespaces).create(name: 'the-namespace', path: 'the-path')
- project = table(:projects).create(name: 'the-project', namespace_id: namespace.id)
- environment = table(:environments).create(name: 'the-environment', project_id: project.id, slug: 'slug')
- cluster = table(:clusters).create(name: 'the-cluster')
+ namespace = table(:namespaces).create!(name: 'the-namespace', path: 'the-path')
+ project = table(:projects).create!(name: 'the-project', namespace_id: namespace.id)
+ environment = table(:environments).create!(name: 'the-environment', project_id: project.id, slug: 'slug')
+ cluster = table(:clusters).create!(name: 'the-cluster')
deployment_data = { cluster_id: cluster.id, project_id: project.id, environment_id: environment.id, ref: 'abc', tag: false, sha: 'sha', status: 1 }
expected_deployment_1 = create_deployment(**deployment_data)
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy
out_of_range_deployment = create_deployment(**deployment_data, cluster_id: cluster.id) # expected to be out of range
# to test "ON CONFLICT DO NOTHING"
- existing_record_for_deployment_2 = deployment_clusters.create(
+ existing_record_for_deployment_2 = deployment_clusters.create!(
deployment_id: expected_deployment_2.id,
cluster_id: expected_deployment_2.cluster_id,
kubernetes_namespace: 'production'
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy
def create_deployment(**data)
@iid ||= 0
@iid += 1
- table(:deployments).create(iid: @iid, **data)
+ table(:deployments).create!(iid: @iid, **data)
end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb
index 8a8edc1af29..539dff86168 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectRepositories do
end
it 'returns the correct disk_path using the route entry' do
- project_legacy_storage_5.route.update(path: 'zoo/new-test')
+ project_legacy_storage_5.route.update!(path: 'zoo/new-test')
project = described_class.find(project_legacy_storage_5.id)
expect(project.disk_path).to eq('zoo/new-test')
@@ -93,8 +93,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectRepositories do
subgroup.update_column(:parent_id, non_existing_record_id)
project = described_class.find(project_orphaned_namespace.id)
- project.route.destroy
- subgroup.route.destroy
+ project.route.destroy!
+ subgroup.route.destroy!
expect { project.reload.disk_path }
.to raise_error(Gitlab::BackgroundMigration::BackfillProjectRepositories::OrphanedNamespaceError)
diff --git a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
index 4e7a3a33f7e..48c5674822a 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
@@ -5,16 +5,16 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillProjectSettings, schema: 20200114113341 do
let(:projects) { table(:projects) }
let(:project_settings) { table(:project_settings) }
- let(:namespace) { table(:namespaces).create(name: 'user', path: 'user') }
- let(:project) { projects.create(namespace_id: namespace.id) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
subject { described_class.new }
describe '#perform' do
it 'creates settings for all projects in range' do
- projects.create(id: 5, namespace_id: namespace.id)
- projects.create(id: 7, namespace_id: namespace.id)
- projects.create(id: 8, namespace_id: namespace.id)
+ projects.create!(id: 5, namespace_id: namespace.id)
+ projects.create!(id: 7, namespace_id: namespace.id)
+ projects.create!(id: 8, namespace_id: namespace.id)
subject.perform(5, 7)
diff --git a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
index 39b49d008d4..9ce6a3227b5 100644
--- a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
@@ -6,21 +6,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPushRulesIdInProjects, :migr
let(:push_rules) { table(:push_rules) }
let(:projects) { table(:projects) }
let(:project_settings) { table(:project_settings) }
- let(:namespace) { table(:namespaces).create(name: 'user', path: 'user') }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
subject { described_class.new }
describe '#perform' do
it 'creates new project push_rules for all push rules in the range' do
- project_1 = projects.create(id: 1, namespace_id: namespace.id)
- project_2 = projects.create(id: 2, namespace_id: namespace.id)
- project_3 = projects.create(id: 3, namespace_id: namespace.id)
- project_settings_1 = project_settings.create(project_id: project_1.id)
- project_settings_2 = project_settings.create(project_id: project_2.id)
- project_settings_3 = project_settings.create(project_id: project_3.id)
- push_rule_1 = push_rules.create(id: 5, is_sample: false, project_id: project_1.id)
- push_rule_2 = push_rules.create(id: 6, is_sample: false, project_id: project_2.id)
- push_rules.create(id: 8, is_sample: false, project_id: 3)
+ project_1 = projects.create!(id: 1, namespace_id: namespace.id)
+ project_2 = projects.create!(id: 2, namespace_id: namespace.id)
+ project_3 = projects.create!(id: 3, namespace_id: namespace.id)
+ project_settings_1 = project_settings.create!(project_id: project_1.id)
+ project_settings_2 = project_settings.create!(project_id: project_2.id)
+ project_settings_3 = project_settings.create!(project_id: project_3.id)
+ push_rule_1 = push_rules.create!(id: 5, is_sample: false, project_id: project_1.id)
+ push_rule_2 = push_rules.create!(id: 6, is_sample: false, project_id: project_2.id)
+ push_rules.create!(id: 8, is_sample: false, project_id: 3)
subject.perform(5, 7)
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index a23b74bcaca..50e799908c6 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
let(:user_name) { 'Test' }
let!(:user) do
- users.create(id: 1,
+ users.create!(id: 1,
email: 'user@example.com',
projects_limit: 10,
username: 'test',
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
end
let!(:migration_bot) do
- users.create(id: 100,
+ users.create!(id: 100,
email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}",
user_type: HasUserType::USER_TYPES[:migration_bot],
name: 'GitLab Migration Bot',
@@ -33,9 +33,9 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
username: 'bot')
end
- let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
- let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
- let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let!(:snippet_with_repo) { snippets.create!(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let!(:snippet_with_empty_repo) { snippets.create!(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let!(:snippet_without_repo) { snippets.create!(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let(:file_name) { 'file_name.rb' }
let(:content) { 'content' }
@@ -197,8 +197,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
end
with_them do
- let!(:snippet_with_invalid_path) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: invalid_file_name, content: content) }
- let!(:snippet_with_valid_path) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let!(:snippet_with_invalid_path) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: invalid_file_name, content: content) }
+ let!(:snippet_with_valid_path) { snippets.create!(id: 5, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let(:ids) { [4, 5] }
after do
@@ -241,7 +241,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
context 'when user name is invalid' do
let(:user_name) { '.' }
- let!(:snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let!(:snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let(:ids) { [4, 4] }
after do
@@ -254,7 +254,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
context 'when both user name and snippet file_name are invalid' do
let(:user_name) { '.' }
let!(:other_user) do
- users.create(id: 2,
+ users.create!(id: 2,
email: 'user2@example.com',
projects_limit: 10,
username: 'test2',
@@ -265,8 +265,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
confirmed_at: 1.day.ago)
end
- let!(:invalid_snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
- let!(:snippet) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) }
+ let!(:invalid_snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
+ let!(:snippet) { snippets.create!(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) }
let(:ids) { [4, 5] }
after do
diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb
index e2175c41513..d503824041b 100644
--- a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutProjectFeature, sc
let(:projects) { table(:projects) }
let(:project_features) { table(:project_features) }
- let(:namespace) { namespaces.create(name: 'foo', path: 'foo') }
+ let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let!(:project) { projects.create!(namespace_id: namespace.id) }
let(:private_project_without_feature) { projects.create!(namespace_id: namespace.id, visibility_level: 0) }
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutProjectFeature, sc
let!(:projects_without_feature) { [private_project_without_feature, public_project_without_feature] }
before do
- project_features.create({ project_id: project.id, pages_access_level: 20 })
+ project_features.create!({ project_id: project.id, pages_access_level: 20 })
end
subject { described_class.new.perform(Project.minimum(:id), Project.maximum(:id)) }
diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb
index fe2b206ea74..9a497a9e01a 100644
--- a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb
@@ -33,8 +33,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
let(:clusters) { table(:clusters) }
let(:cluster_groups) { table(:cluster_groups) }
let(:clusters_applications_prometheus) { table(:clusters_applications_prometheus) }
- let(:namespace) { namespaces.create(name: 'user', path: 'user') }
- let(:project) { projects.create(namespace_id: namespace.id) }
+ let(:namespace) { namespaces.create!(name: 'user', path: 'user') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
let(:application_statuses) do
{
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'non prometheus services' do
it 'does not change them' do
other_type = 'SomeOtherService'
- services.create(service_params_for(project.id, active: true, type: other_type))
+ services.create!(service_params_for(project.id, active: true, type: other_type))
expect { subject.perform(project.id, project.id + 1) }.not_to change { services.where(type: other_type).order(:id).map { |row| row.attributes } }
end
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'template is present for prometheus services' do
it 'creates missing services entries', :aggregate_failures do
- services.create(service_params_for(nil, template: true, properties: { 'from_template' => true }.to_json))
+ services.create!(service_params_for(nil, template: true, properties: { 'from_template' => true }.to_json))
expect { subject.perform(project.id, project.id + 1) }.to change { services.count }.by(1)
updated_rows = services.where(template: false).order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys }
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'prometheus integration services exist' do
context 'in active state' do
it 'does not change them' do
- services.create(service_params_for(project.id, active: true))
+ services.create!(service_params_for(project.id, active: true))
expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
end
@@ -105,7 +105,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'not in active state' do
it 'sets active attribute to true' do
- service = services.create(service_params_for(project.id, active: false))
+ service = services.create!(service_params_for(project.id, active: false))
expect { subject.perform(project.id, project.id + 1) }.to change { service.reload.active? }.from(false).to(true)
end
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'prometheus services are configured manually ' do
it 'does not change them' do
properties = '{"api_url":"http://test.dev","manual_configuration":"1"}'
- services.create(service_params_for(project.id, properties: properties, active: false))
+ services.create!(service_params_for(project.id, properties: properties, active: false))
expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
end
@@ -123,11 +123,11 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
end
context 'k8s cluster shared on instance level' do
- let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:instance_type]) }
+ let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:instance_type]) }
context 'with installed prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
end
it_behaves_like 'fix services entries state'
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'with updated prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
end
it_behaves_like 'fix services entries state'
@@ -143,7 +143,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'with errored prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
end
it 'does not change services entries' do
@@ -153,26 +153,26 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
end
context 'k8s cluster shared on group level' do
- let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:group_type]) }
+ let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:group_type]) }
before do
- cluster_groups.create(cluster_id: cluster.id, group_id: project.namespace_id)
+ cluster_groups.create!(cluster_id: cluster.id, group_id: project.namespace_id)
end
context 'with installed prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
end
it_behaves_like 'fix services entries state'
context 'second k8s cluster without application available' do
- let(:namespace_2) { namespaces.create(name: 'namespace2', path: 'namespace2') }
- let(:project_2) { projects.create(namespace_id: namespace_2.id) }
+ let(:namespace_2) { namespaces.create!(name: 'namespace2', path: 'namespace2') }
+ let(:project_2) { projects.create!(namespace_id: namespace_2.id) }
before do
- cluster_2 = clusters.create(name: 'cluster2', cluster_type: cluster_types[:group_type])
- cluster_groups.create(cluster_id: cluster_2.id, group_id: project_2.namespace_id)
+ cluster_2 = clusters.create!(name: 'cluster2', cluster_type: cluster_types[:group_type])
+ cluster_groups.create!(cluster_id: cluster_2.id, group_id: project_2.namespace_id)
end
it 'changed only affected services entries' do
@@ -184,7 +184,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'with updated prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
end
it_behaves_like 'fix services entries state'
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'with errored prometheus application' do
before do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
end
it 'does not change services entries' do
@@ -207,7 +207,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
context 'with inactive service' do
it 'does not change services entries' do
- services.create(service_params_for(project.id))
+ services.create!(service_params_for(project.id))
expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
end
@@ -216,13 +216,13 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService,
end
context 'k8s cluster for single project' do
- let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:project_type]) }
+ let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:project_type]) }
let(:cluster_projects) { table(:cluster_projects) }
context 'with installed prometheus application' do
before do
- cluster_projects.create(cluster_id: cluster.id, project_id: project.id)
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
+ cluster_projects.create!(cluster_id: cluster.id, project_id: project.id)
+ clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
end
it 'does not change services entries' do
diff --git a/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
index 7768411828c..0d0ad2cc39e 100644
--- a/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
@@ -5,18 +5,18 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190620112608 do
let(:namespaces) { table(:namespaces) }
let(:users) { table(:users) }
- let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
+ let(:user) { users.create!(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
context 'updating the namespace names' do
it 'updates a user namespace within range' do
- user2 = users.create(name: "Other user's full name", projects_limit: 10, username: 'also-not-null', email: '2')
- user_namespace1 = namespaces.create(
+ user2 = users.create!(name: "Other user's full name", projects_limit: 10, username: 'also-not-null', email: '2')
+ user_namespace1 = namespaces.create!(
id: 2,
owner_id: user.id,
name: "Should be the user's name",
path: user.username
)
- user_namespace2 = namespaces.create(
+ user_namespace2 = namespaces.create!(
id: 3,
owner_id: user2.id,
name: "Should also be the user's name",
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
end
it 'does not update namespaces out of range' do
- user_namespace = namespaces.create(
+ user_namespace = namespaces.create!(
id: 6,
owner_id: user.id,
name: "Should be the user's name",
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
end
it 'does not update groups owned by the users' do
- user_group = namespaces.create(
+ user_group = namespaces.create!(
id: 2,
owner_id: user.id,
name: 'A group name',
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
context 'namespace route names' do
let(:routes) { table(:routes) }
let(:namespace) do
- namespaces.create(
+ namespaces.create!(
id: 2,
owner_id: user.id,
name: "Will be updated to the user's name",
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
end
it "updates the route name if it didn't match the namespace" do
- route = routes.create(path: namespace.path, name: 'Incorrect name', source_type: 'Namespace', source_id: namespace.id)
+ route = routes.create!(path: namespace.path, name: 'Incorrect name', source_type: 'Namespace', source_id: namespace.id)
described_class.new.perform(1, 5)
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
end
it 'updates the route name if it was nil match the namespace' do
- route = routes.create(path: namespace.path, name: nil, source_type: 'Namespace', source_id: namespace.id)
+ route = routes.create!(path: namespace.path, name: nil, source_type: 'Namespace', source_id: namespace.id)
described_class.new.perform(1, 5)
@@ -83,14 +83,14 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190
end
it "doesn't update group routes" do
- route = routes.create(path: 'group-path', name: 'Group name', source_type: 'Group', source_id: namespace.id)
+ route = routes.create!(path: 'group-path', name: 'Group name', source_type: 'Group', source_id: namespace.id)
expect { described_class.new.perform(1, 5) }
.not_to change { route.reload.name }
end
it "doesn't touch routes for namespaces out of range" do
- user_namespace = namespaces.create(
+ user_namespace = namespaces.create!(
id: 6,
owner_id: user.id,
name: "Should be the user's name",
diff --git a/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb
index 4c04043ebd0..211693d917b 100644
--- a/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb
@@ -8,10 +8,10 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
let(:routes) { table(:routes) }
let(:projects) { table(:projects) }
- let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
+ let(:user) { users.create!(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
let(:namespace) do
- namespaces.create(
+ namespaces.create!(
owner_id: user.id,
name: "Should eventually be the user's name",
path: user.username
@@ -19,11 +19,11 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
end
let(:project) do
- projects.create(namespace_id: namespace.id, name: 'Project Name')
+ projects.create!(namespace_id: namespace.id, name: 'Project Name')
end
it "updates the route for a project if it did not match the user's name" do
- route = routes.create(
+ route = routes.create!(
id: 1,
path: "#{user.username}/#{project.path}",
source_id: project.id,
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
end
it 'updates the route for a project if the name was nil' do
- route = routes.create(
+ route = routes.create!(
id: 1,
path: "#{user.username}/#{project.path}",
source_id: project.id,
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
end
it 'does not update routes that were are out of the range' do
- route = routes.create(
+ route = routes.create!(
id: 6,
path: "#{user.username}/#{project.path}",
source_id: project.id,
@@ -64,14 +64,14 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
end
it 'does not update routes for projects in groups owned by the user' do
- group = namespaces.create(
+ group = namespaces.create!(
owner_id: user.id,
name: 'A group',
path: 'a-path',
type: ''
)
- project = projects.create(namespace_id: group.id, name: 'Project Name')
- route = routes.create(
+ project = projects.create!(namespace_id: group.id, name: 'Project Name')
+ route = routes.create!(
id: 1,
path: "#{group.path}/#{project.path}",
source_id: project.id,
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20
end
it 'does not update routes for namespaces' do
- route = routes.create(
+ route = routes.create!(
id: 1,
path: namespace.path,
source_id: namespace.id,
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index 934ab7e37f8..264faa4de3b 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
if with_file
upload = create(:upload, :with_file, :attachment_upload, params)
- model.update(attachment: upload.retrieve_uploader)
+ model.update!(attachment: upload.retrieve_uploader)
model.attachment.upload
else
create(:upload, :attachment_upload, params)
@@ -245,7 +245,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
end
let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
- let(:bucket) { connection.directories.create(key: 'uploads') }
+ let(:bucket) { connection.directories.create(key: 'uploads') } # rubocop:disable Rails/SaveBang
before do
stub_uploads_object_storage(FileUploader)
@@ -257,7 +257,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file belongs to a legacy project' do
before do
- bucket.files.create(remote_file)
+ bucket.files.create(remote_file) # rubocop:disable Rails/SaveBang
end
let(:project) { legacy_project }
@@ -267,7 +267,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when the file belongs to a hashed project' do
before do
- bucket.files.create(remote_file)
+ bucket.files.create(remote_file) # rubocop:disable Rails/SaveBang
end
let(:project) { hashed_project }
diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
index 66a1787b2cb..7227f80a062 100644
--- a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do
if with_file
upload = create(:upload, :with_file, :attachment_upload, params)
- model.update(attachment: upload.retrieve_uploader)
+ model.update!(attachment: upload.retrieve_uploader)
model.attachment.upload
else
create(:upload, :attachment_upload, params)
diff --git a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
index dda4f5a3a36..b7cf101dd8a 100644
--- a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
@@ -10,44 +10,44 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration,
let(:lfs_objects) { table(:lfs_objects) }
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
- let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') }
+ let(:namespace) { namespaces.create!(name: 'GitLab', path: 'gitlab') }
- let(:fork_network) { fork_networks.create(root_project_id: source_project.id) }
- let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) }
+ let(:fork_network) { fork_networks.create!(root_project_id: source_project.id) }
+ let(:another_fork_network) { fork_networks.create!(root_project_id: another_source_project.id) }
- let(:source_project) { projects.create(namespace_id: namespace.id) }
- let(:another_source_project) { projects.create(namespace_id: namespace.id) }
- let(:project) { projects.create(namespace_id: namespace.id) }
- let(:another_project) { projects.create(namespace_id: namespace.id) }
- let(:partially_linked_project) { projects.create(namespace_id: namespace.id) }
- let(:fully_linked_project) { projects.create(namespace_id: namespace.id) }
+ let(:source_project) { projects.create!(namespace_id: namespace.id) }
+ let(:another_source_project) { projects.create!(namespace_id: namespace.id) }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+ let(:another_project) { projects.create!(namespace_id: namespace.id) }
+ let(:partially_linked_project) { projects.create!(namespace_id: namespace.id) }
+ let(:fully_linked_project) { projects.create!(namespace_id: namespace.id) }
- let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) }
- let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) }
+ let(:lfs_object) { lfs_objects.create!(oid: 'abc123', size: 100) }
+ let(:another_lfs_object) { lfs_objects.create!(oid: 'def456', size: 200) }
let!(:source_project_lop_1) do
- lfs_objects_projects.create(
+ lfs_objects_projects.create!(
lfs_object_id: lfs_object.id,
project_id: source_project.id
)
end
let!(:source_project_lop_2) do
- lfs_objects_projects.create(
+ lfs_objects_projects.create!(
lfs_object_id: another_lfs_object.id,
project_id: source_project.id
)
end
let!(:another_source_project_lop_1) do
- lfs_objects_projects.create(
+ lfs_objects_projects.create!(
lfs_object_id: lfs_object.id,
project_id: another_source_project.id
)
end
let!(:another_source_project_lop_2) do
- lfs_objects_projects.create(
+ lfs_objects_projects.create!(
lfs_object_id: another_lfs_object.id,
project_id: another_source_project.id
)
@@ -57,23 +57,23 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration,
stub_const("#{described_class}::BATCH_SIZE", 2)
# Create links between projects
- fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
+ fork_network_members.create!(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
[project, partially_linked_project, fully_linked_project].each do |p|
- fork_network_members.create(
+ fork_network_members.create!(
fork_network_id: fork_network.id,
project_id: p.id,
forked_from_project_id: fork_network.root_project_id
)
end
- fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
- fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_project.id, forked_from_project_id: another_fork_network.root_project_id)
+ fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
+ fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_project.id, forked_from_project_id: another_fork_network.root_project_id)
# Links LFS objects to some projects
- lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: fully_linked_project.id)
- lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: fully_linked_project.id)
- lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: partially_linked_project.id)
+ lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: fully_linked_project.id)
+ lfs_objects_projects.create!(lfs_object_id: another_lfs_object.id, project_id: fully_linked_project.id)
+ lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: partially_linked_project.id)
end
context 'when there are LFS objects to be linked' do
@@ -96,8 +96,8 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration,
before do
# Links LFS objects to all projects
projects.all.each do |p|
- lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
- lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
+ lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: p.id)
+ lfs_objects_projects.create!(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
index d829fd5daf5..8668216d014 100644
--- a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with Jira service' do
let!(:service) do
- services.create(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker')
+ services.create!(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with bugzilla service' do
let!(:service) do
- services.create(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
+ services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -140,7 +140,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with youtrack service' do
let!(:service) do
- services.create(id: 12, type: 'YoutrackService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker')
+ services.create!(id: 12, type: 'YoutrackService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -161,7 +161,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with gitlab service with no properties' do
let!(:service) do
- services.create(id: 13, type: 'GitlabIssueTrackerService', title: nil, properties: {}, category: 'issue_tracker')
+ services.create!(id: 13, type: 'GitlabIssueTrackerService', title: nil, properties: {}, category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -173,7 +173,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with redmine service already with data fields' do
let!(:service) do
- services.create(id: 14, type: 'RedmineService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker').tap do |service|
+ services.create!(id: 14, type: 'RedmineService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker').tap do |service|
IssueTrackerData.create!(service_id: service.id, project_url: url, new_issue_url: new_issue_url, issues_url: issues_url)
end
end
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with custom issue tracker which has data fields record inconsistent with properties field' do
let!(:service) do
- services.create(id: 15, type: 'CustomIssueTrackerService', title: 'Existing title', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
+ services.create!(id: 15, type: 'CustomIssueTrackerService', title: 'Existing title', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
IssueTrackerData.create!(service_id: service.id, project_url: 'http://other_url', new_issue_url: 'http://other_url/new_issue', issues_url: 'http://other_url/issues')
end
end
@@ -209,7 +209,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with Jira service which has data fields record inconsistent with properties field' do
let!(:service) do
- services.create(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
+ services.create!(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
JiraTrackerData.create!(service_id: service.id, url: 'http://other_jira_url')
end
end
@@ -232,7 +232,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'non issue tracker service' do
let!(:service) do
- services.create(id: 17, title: nil, description: nil, type: 'OtherService', properties: tracker_properties.to_json)
+ services.create!(id: 17, title: nil, description: nil, type: 'OtherService', properties: tracker_properties.to_json)
end
it_behaves_like 'handle properties'
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'Jira service with empty properties' do
let!(:service) do
- services.create(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker')
+ services.create!(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -260,7 +260,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'Jira service with nil properties' do
let!(:service) do
- services.create(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker')
+ services.create!(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -272,7 +272,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'Jira service with invalid properties' do
let!(:service) do
- services.create(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
+ services.create!(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
end
it_behaves_like 'handle properties'
@@ -284,15 +284,15 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
context 'with Jira service with invalid properties, valid Jira service and valid bugzilla service' do
let!(:jira_service_invalid) do
- services.create(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
+ services.create!(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
end
let!(:jira_service_valid) do
- services.create(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker')
+ services.create!(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker')
end
let!(:bugzilla_service_valid) do
- services.create(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
+ services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
end
it 'migrates data for the valid service' do
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 3cec5cb4c35..d90a5d30954 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
@@ -11,17 +11,17 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateUsersBioToUserDetails, :migra
klass
end
- let!(:user_needs_migration) { users.create(name: 'user1', email: 'test1@test.com', projects_limit: 1, bio: 'bio') }
- let!(:user_needs_no_migration) { users.create(name: 'user2', email: 'test2@test.com', projects_limit: 1) }
- let!(:user_also_needs_no_migration) { users.create(name: 'user3', email: 'test3@test.com', projects_limit: 1, bio: '') }
- let!(:user_with_long_bio) { users.create(name: 'user4', email: 'test4@test.com', projects_limit: 1, bio: 'a' * 256) } # 255 is the max
+ let!(:user_needs_migration) { users.create!(name: 'user1', email: 'test1@test.com', projects_limit: 1, bio: 'bio') }
+ let!(:user_needs_no_migration) { users.create!(name: 'user2', email: 'test2@test.com', projects_limit: 1) }
+ let!(:user_also_needs_no_migration) { users.create!(name: 'user3', email: 'test3@test.com', projects_limit: 1, bio: '') }
+ let!(:user_with_long_bio) { users.create!(name: 'user4', email: 'test4@test.com', projects_limit: 1, bio: 'a' * 256) } # 255 is the max
- let!(:user_already_has_details) { users.create(name: 'user5', email: 'test5@test.com', projects_limit: 1, bio: 'my bio') }
- let!(:existing_user_details) { user_details.find_or_create_by(user_id: user_already_has_details.id).update(bio: 'my bio') }
+ let!(:user_already_has_details) { users.create!(name: 'user5', email: 'test5@test.com', projects_limit: 1, bio: 'my bio') }
+ let!(:existing_user_details) { user_details.find_or_create_by!(user_id: user_already_has_details.id).update!(bio: 'my bio') }
# unlikely scenario since we have triggers
- let!(:user_has_different_details) { users.create(name: 'user6', email: 'test6@test.com', projects_limit: 1, bio: 'different') }
- let!(:different_existing_user_details) { user_details.find_or_create_by(user_id: user_has_different_details.id).update(bio: 'bio') }
+ let!(:user_has_different_details) { users.create!(name: 'user6', email: 'test6@test.com', projects_limit: 1, bio: 'different') }
+ let!(:different_existing_user_details) { user_details.find_or_create_by!(user_id: user_has_different_details.id).update!(bio: 'bio') }
let(:user_ids) do
[
diff --git a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
index ee0024e8526..36000dc3ffd 100644
--- a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration,
describe 'gracefully handles existing records, some of which may have an already-existing identical canonical_email field' do
let_it_be(:user_one) { create_user(email: "example.user@gmail.com", id: 1) }
let_it_be(:user_two) { create_user(email: "exampleuser@gmail.com", id: 2) }
- let_it_be(:user_email_one) { user_canonical_emails.create(canonical_email: "exampleuser@gmail.com", user_id: user_one.id) }
+ let_it_be(:user_email_one) { user_canonical_emails.create!(canonical_email: "exampleuser@gmail.com", user_id: user_one.id) }
subject { migration.perform(1, 2) }
@@ -79,7 +79,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration,
projects_limit: 0
}
- users.create(default_attributes.merge!(attributes))
+ users.create!(default_attributes.merge!(attributes))
end
def canonical_emails(user_id: nil)
diff --git a/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb
new file mode 100644
index 00000000000..bc55f240a58
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 2020_11_30_103926 do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+
+ let!(:namespace) { namespaces.create!(name: "foo", path: "bar") }
+ let!(:user) { users.create!(name: 'John Doe', email: 'test@example.com', projects_limit: 5) }
+ let!(:project) { projects.create!(namespace_id: namespace.id) }
+ let!(:vulnerability_params) do
+ {
+ project_id: project.id,
+ author_id: user.id,
+ title: 'Vulnerability',
+ severity: 5,
+ confidence: 5,
+ report_type: 5
+ }
+ end
+
+ let!(:vulnerability_1) { vulnerabilities.create!(vulnerability_params.merge(state: 1)) }
+ let!(:vulnerability_2) { vulnerabilities.create!(vulnerability_params.merge(state: 3)) }
+
+ describe '#perform' do
+ it 'changes state of vulnerability to dismissed' do
+ subject.perform(vulnerability_1.id, vulnerability_2.id)
+
+ expect(vulnerability_1.reload.state).to eq(2)
+ expect(vulnerability_2.reload.state).to eq(2)
+ end
+
+ it 'populates missing dismissal information' do
+ expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migration|
+ expect(migration).to receive(:perform).with(vulnerability_1.id, vulnerability_2.id)
+ end
+
+ subject.perform(vulnerability_1.id, vulnerability_2.id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb
index 1e5773ee16b..4e7872a9a1b 100644
--- a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable,
let(:user_2) { users.create!(email: 'test2@example.com', projects_limit: 100, username: 'test') }
let(:user_3) { users.create!(email: 'test3@example.com', projects_limit: 100, username: 'test') }
- let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
let(:merge_requests) { table(:merge_requests) }
let(:merge_request_assignees) { table(:merge_request_assignees) }
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable,
source_branch: 'mr name',
title: "mr name#{id}")
- merge_requests.create(params)
+ merge_requests.create!(params)
end
before do
diff --git a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb b/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb
index f0b0f77280e..b3cacc60cdc 100644
--- a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem
projects_limit: 0
}.merge(params)
- users.create(user_params)
+ users.create!(user_params)
end
def create_member(id, access_level, params = {})
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem
notification_level: 0
}.merge(params)
- members.create(params)
+ members.create!(params)
end
before do
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem
create_member(7, 30)
create_member(8, 20, requested_at: Time.current)
- user_highest_roles.create(user_id: 1, highest_access_level: 50)
+ user_highest_roles.create!(user_id: 1, highest_access_level: 50)
end
describe '#perform' do
diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb
index 33e1f31d1f1..1c55b50ea3f 100644
--- a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc
projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0,
namespace_id: shared_group.id)
- group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id,
+ group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id,
group_access: 20)
end
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc
shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project',
visibility_level: 0, namespace_id: another_group.id)
- project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20)
+ project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20)
end
it 'creates correct authorization' do
@@ -174,7 +174,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc
projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0,
namespace_id: shared_group.id)
- group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id,
+ group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id,
group_access: 20)
end
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc
shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project',
visibility_level: 0, namespace_id: another_group.id)
- project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20)
+ project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20)
end
it 'does not create authorization' do
diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
index 43fc0fb3691..2f5074649c4 100644
--- a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
+++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::ResetMergeStatus do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
- let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
let(:merge_requests) { table(:merge_requests) }
def create_merge_request(id, extra_params = {})
diff --git a/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb
new file mode 100644
index 00000000000..bebb398413b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::UpdateExistingUsersThatRequireTwoFactorAuth, schema: 20201030121314 do
+ include MigrationHelpers::NamespacesHelpers
+
+ let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE) }
+ let(:group_with_2fa_child) { create_namespace('child', Gitlab::VisibilityLevel::PRIVATE, parent_id: group_with_2fa_parent.id) }
+ let(:members_table) { table(:members) }
+ let(:users_table) { table(:users) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'with group members' do
+ let(:user_1) { create_user('user@example.com') }
+ let!(:member) { create_group_member(user_1, group_with_2fa_parent) }
+ let!(:user_without_group) { create_user('user_without@example.com') }
+ let(:user_other) { create_user('user_other@example.com') }
+ let!(:member_other) { create_group_member(user_other, group_with_2fa_parent) }
+
+ it 'updates user when user should not be required to establish two factor authentication' do
+ subject.perform(user_1.id, user_without_group.id)
+
+ expect(user_1.reload.require_two_factor_authentication_from_group).to eq(false)
+ end
+
+ it 'does not update user when user is member of group that requires two factor authentication' do
+ group = create_namespace('other', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true)
+ create_group_member(user_1, group)
+
+ subject.perform(user_1.id, user_without_group.id)
+
+ expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
+ end
+
+ it 'does not update user who is not in current batch' do
+ subject.perform(user_1.id, user_without_group.id)
+
+ expect(user_other.reload.require_two_factor_authentication_from_group).to eq(true)
+ end
+
+ it 'updates all users in current batch' do
+ subject.perform(user_1.id, user_other.id)
+
+ expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false)
+ end
+
+ it 'does not update user when user is member of group which parent group requires two factor authentication' do
+ group_with_2fa_parent.update!(require_two_factor_authentication: true)
+ subject.perform(user_1.id, user_other.id)
+
+ expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
+ end
+
+ it 'does not update user when user is member of group which has subgroup that requires two factor authentication' do
+ create_namespace('subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true, parent_id: group_with_2fa_child.id)
+
+ subject.perform(user_1.id, user_other.id)
+
+ expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
+ end
+ end
+ end
+
+ def create_user(email, require_2fa: true)
+ users_table.create!(email: email, projects_limit: 10, require_two_factor_authentication_from_group: require_2fa)
+ end
+
+ def create_group_member(user, group)
+ members_table.create!(user_id: user.id, source_id: group.id, access_level: GroupMember::MAINTAINER, source_type: "Namespace", type: "GroupMember", notification_level: 3)
+ end
+end
diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb
index 2cca0aed9c6..f4daafb1d0e 100644
--- a/spec/lib/gitlab/checks/diff_check_spec.rb
+++ b/spec/lib/gitlab/checks/diff_check_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe Gitlab::Checks::DiffCheck do
describe '#validate!' do
let(:owner) { create(:user) }
- let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do
allow(project.repository).to receive(:new_commits).and_return(
@@ -28,13 +27,27 @@ RSpec.describe Gitlab::Checks::DiffCheck do
end
context 'with LFS enabled' do
+ let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
+
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
context 'when change is sent by a different user' do
- it 'raises an error if the user is not allowed to update the file' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
+ context 'when diff check with paths rpc feature flag is true' do
+ it 'raises an error if the user is not allowed to update the file' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
+ end
+ end
+
+ context 'when diff check with paths rpc feature flag is false' do
+ before do
+ stub_feature_flags(diff_check_with_paths_changed_rpc: false)
+ end
+
+ it 'raises an error if the user is not allowed to update the file' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
+ end
end
end
@@ -53,6 +66,8 @@ RSpec.describe Gitlab::Checks::DiffCheck do
expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original
+ stub_feature_flags(diff_check_with_paths_changed_rpc: false)
+
subject.validate!
end
diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb
index 45ab13cf0cf..262438256b4 100644
--- a/spec/lib/gitlab/checks/push_check_spec.rb
+++ b/spec/lib/gitlab/checks/push_check_spec.rb
@@ -18,5 +18,26 @@ RSpec.describe Gitlab::Checks::PushCheck do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.')
end
end
+
+ context 'when using a DeployKeyAccess instance' do
+ let(:deploy_key) { create(:deploy_key) }
+ let(:user_access) { Gitlab::DeployKeyAccess.new(deploy_key, container: project) }
+
+ context 'when the deploy key cannot push to the targetted branch' do
+ it 'raises an error' do
+ allow(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.')
+ end
+ end
+
+ context 'when the deploy key can push to the targetted branch' do
+ it 'is valid' do
+ allow(user_access).to receive(:can_push_to_branch?).and_return(true)
+
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/snippet_check_spec.rb b/spec/lib/gitlab/checks/snippet_check_spec.rb
index 037de8e9369..89417aaca4d 100644
--- a/spec/lib/gitlab/checks/snippet_check_spec.rb
+++ b/spec/lib/gitlab/checks/snippet_check_spec.rb
@@ -9,19 +9,30 @@ RSpec.describe Gitlab::Checks::SnippetCheck do
let(:user_access) { Gitlab::UserAccessSnippet.new(user, snippet: snippet) }
let(:default_branch) { snippet.default_branch }
+ let(:branch_name) { default_branch }
+ let(:creation) { false }
+ let(:deletion) { false }
- subject { Gitlab::Checks::SnippetCheck.new(changes, default_branch: default_branch, logger: logger) }
+ subject { Gitlab::Checks::SnippetCheck.new(changes, default_branch: default_branch, root_ref: snippet.repository.root_ref, logger: logger) }
describe '#validate!' do
it 'does not raise any error' do
expect { subject.validate! }.not_to raise_error
end
+ shared_examples 'raises and logs error' do
+ specify do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Gitlab::GitAccess::ForbiddenError), default_branch: default_branch, branch_name: branch_name, creation: creation, deletion: deletion)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
+ end
+ end
+
context 'trying to delete the branch' do
let(:newrev) { '0000000000000000000000000000000000000000' }
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
+ it_behaves_like 'raises and logs error' do
+ let(:deletion) { true }
end
end
@@ -29,14 +40,23 @@ RSpec.describe Gitlab::Checks::SnippetCheck do
let(:oldrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/heads/feature' }
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
+ it_behaves_like 'raises and logs error' do
+ let(:creation) { true }
+ let(:branch_name) { 'feature' }
end
- context "when branch is 'master'" do
- let(:ref) { 'refs/heads/master' }
+ context 'when branch is the same as the default branch' do
+ let(:ref) { "refs/heads/#{default_branch}" }
- it "allows the operation" do
+ it 'allows the operation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ context 'when snippet has an empty repo' do
+ let_it_be(:snippet) { create(:personal_snippet, :empty_repo) }
+
+ it 'allows the operation' do
expect { subject.validate! }.not_to raise_error
end
end
@@ -45,8 +65,8 @@ RSpec.describe Gitlab::Checks::SnippetCheck do
context 'when default_branch is nil' do
let(:default_branch) { nil }
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
+ it_behaves_like 'raises and logs error' do
+ let(:branch_name) { 'master' }
end
end
end
diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
index 31c0da95f0a..b7b4d6de8b9 100644
--- a/spec/lib/gitlab/ci/ansi2json/result_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Result do
{ lines: [], state: state, append: false, truncated: false, offset: offset, stream: stream }
end
- subject { described_class.new(params) }
+ subject { described_class.new(**params) }
describe '#size' do
before do
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
index d27a642ecf3..ff70ff69aaa 100644
--- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Ansi2json::Style do
describe '#set?' do
- subject { described_class.new(params).set? }
+ subject { described_class.new(**params).set? }
context 'when fg color is set' do
let(:params) { { fg: 'term-fg-black' } }
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Style do
end
describe 'update formats to mimic terminals' do
- subject { described_class.new(params) }
+ subject { described_class.new(**params) }
context 'when fg color present' do
let(:params) { { fg: 'term-fg-black', mask: mask } }
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index 77b8aa1d591..efe99cd276c 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata do
it 'reads expected number of entries' do
stream = File.open(tmpfile.path)
- metadata = described_class.new(stream, 'public', { recursive: true })
+ metadata = described_class.new(stream, 'public', recursive: true)
expect(metadata.find_entries!.count).to eq entry_count
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb
new file mode 100644
index 00000000000..faede7a361f
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause do
+ describe '.fabricate' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:value) { 'some value' }
+
+ subject { described_class.fabricate(type, value) }
+
+ context 'when type is valid' do
+ where(:type, :result) do
+ 'changes' | Gitlab::Ci::Build::Rules::Rule::Clause::Changes
+ 'exists' | Gitlab::Ci::Build::Rules::Rule::Clause::Exists
+ 'if' | Gitlab::Ci::Build::Rules::Rule::Clause::If
+ end
+
+ with_them do
+ it { is_expected.to be_instance_of(result) }
+ end
+ end
+
+ context 'when type is invalid' do
+ let(:type) { 'when' }
+
+ it { is_expected.to be_nil }
+
+ context "when type is 'variables'" do
+ let(:type) { 'variables' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index cbeae33fbcf..a1af5b75f87 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do
context 'with one rule without any clauses' do
let(:rule_list) { [{ when: 'manual', allow_failure: true }] }
- it { is_expected.to eq(described_class::Result.new('manual', nil, true)) }
+ it { is_expected.to eq(described_class::Result.new('manual', nil, true, nil)) }
end
context 'with one matching rule' do
@@ -171,7 +171,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do
context 'with matching rule' do
let(:rule_list) { [{ if: '$VAR == null', allow_failure: true }] }
- it { is_expected.to eq(described_class::Result.new('on_success', nil, true)) }
+ it { is_expected.to eq(described_class::Result.new('on_success', nil, true, nil)) }
end
context 'with non-matching rule' do
@@ -180,18 +180,60 @@ RSpec.describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) }
end
end
+
+ context 'with variables' do
+ context 'with matching rule' do
+ let(:rule_list) { [{ if: '$VAR == null', variables: { MY_VAR: 'my var' } }] }
+
+ it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, { MY_VAR: 'my var' })) }
+ end
+ end
end
describe 'Gitlab::Ci::Build::Rules::Result' do
let(:when_value) { 'on_success' }
let(:start_in) { nil }
let(:allow_failure) { nil }
+ let(:variables) { nil }
- subject { Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) }
+ subject(:result) do
+ Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables)
+ end
describe '#build_attributes' do
+ let(:seed_attributes) { {} }
+
+ subject(:build_attributes) do
+ result.build_attributes(seed_attributes)
+ end
+
it 'compacts nil values' do
- expect(subject.build_attributes).to eq(options: {}, when: 'on_success')
+ is_expected.to eq(options: {}, when: 'on_success')
+ end
+
+ context 'when there are variables in rules' do
+ let(:variables) { { VAR1: 'new var 1', VAR3: 'var 3' } }
+
+ context 'when there are seed variables' do
+ let(:seed_attributes) do
+ { yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true },
+ { key: 'VAR2', value: 'var 2', public: true }] }
+ end
+
+ it 'returns yaml_variables with override' do
+ is_expected.to include(
+ yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true },
+ { key: 'VAR2', value: 'var 2', public: true },
+ { key: 'VAR3', value: 'var 3', public: true }]
+ )
+ end
+ end
+
+ context 'when there is not seed variables' do
+ it 'does not return yaml_variables' do
+ is_expected.not_to have_key(:yaml_variables)
+ end
+ end
end
end
@@ -200,7 +242,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do
let!(:when_value) { 'never' }
it 'returns false' do
- expect(subject.pass?).to eq(false)
+ expect(result.pass?).to eq(false)
end
end
@@ -208,7 +250,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do
let!(:when_value) { 'on_success' }
it 'returns true' do
- expect(subject.pass?).to eq(true)
+ expect(result.pass?).to eq(true)
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb
new file mode 100644
index 00000000000..7aaad57f5cd
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::AllowFailure do
+ let(:entry) { described_class.new(config.deep_dup) }
+ let(:expected_config) { config }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ shared_examples 'valid entry' do
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(expected_config)
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'with boolean values' do
+ it_behaves_like 'valid entry' do
+ let(:config) { true }
+ end
+
+ it_behaves_like 'valid entry' do
+ let(:config) { false }
+ end
+ end
+
+ context 'with hash values' do
+ it_behaves_like 'valid entry' do
+ let(:config) { { exit_codes: 137 } }
+ let(:expected_config) { { exit_codes: [137] } }
+ end
+
+ it_behaves_like 'valid entry' do
+ let(:config) { { exit_codes: [42, 137] } }
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ shared_examples 'invalid entry' do
+ describe '#valid?' do
+ it { expect(entry).not_to be_valid }
+ it { expect(entry.errors).to include(error_message) }
+ end
+ end
+
+ context 'when it has a wrong type' do
+ let(:config) { [1] }
+ let(:error_message) do
+ 'allow failure config should be a hash or a boolean value'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'with string exit codes' do
+ let(:config) { { exit_codes: 'string' } }
+ let(:error_message) do
+ 'allow failure exit codes should be an array of integers or an integer'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'with array of strings as exit codes' do
+ let(:config) { { exit_codes: ['string 1', 'string 2'] } }
+ let(:error_message) do
+ 'allow failure exit codes should be an array of integers or an integer'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'when it has an extra keys' do
+ let(:config) { { extra: true } }
+ let(:error_message) do
+ 'allow failure config contains unknown keys: extra'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 8b2e0410474..b3b7901074a 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -227,6 +227,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ context 'when bridge config contains exit_codes' do
+ let(:config) do
+ { script: 'rspec', allow_failure: { exit_codes: [42] } }
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns an error message' do
+ expect(subject.errors)
+ .to include(/allow failure should be a boolean value/)
+ end
+ end
+ end
end
describe '#manual_action?' do
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index c3d91057328..e810d65d560 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { { name: 'ruby:2.7', entrypoint: %w(/bin/sh run), ports: ports } }
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index e0e8bc93770..7834a1a94f2 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -670,6 +670,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
end
describe '#ignored?' do
+ before do
+ entry.compose!
+ end
+
context 'when job is a manual action' do
context 'when it is not specified if job is allowed to fail' do
let(:config) do
@@ -700,6 +704,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
expect(entry).not_to be_ignored
end
end
+
+ context 'when job is dynamically allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: { exit_codes: 42 } }
+ end
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
end
context 'when job is not a manual action' do
@@ -709,6 +723,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is not an ignored job' do
expect(entry).not_to be_ignored
end
+
+ it 'does not return allow_failure' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
end
context 'when job is allowed to fail' do
@@ -717,6 +735,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is an ignored job' do
expect(entry).to be_ignored
end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
end
context 'when job is not allowed to fail' do
@@ -725,6 +747,32 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is not an ignored job' do
expect(entry).not_to be_ignored
end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
+
+ context 'when job is dynamically allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: { exit_codes: 42 } } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+
+ it 'returns allow_failure_criteria' do
+ expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42])
+ end
+
+ context 'with ci_allow_failure_with_exit_codes disabled' do
+ before do
+ stub_feature_flags(ci_allow_failure_with_exit_codes: false)
+ end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb
index 5a826bf8282..983e95fae42 100644
--- a/spec/lib/gitlab/ci/config/entry/need_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb
@@ -165,6 +165,45 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
end
end
+ context 'with cross pipeline artifacts needs' do
+ context 'when pipeline is provided' do
+ context 'when job is provided' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID' } }
+
+ it { is_expected.to be_valid }
+
+ it 'sets artifacts:true by default' do
+ expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: true)
+ end
+
+ it 'sets the type as cross_dependency' do
+ expect(need.type).to eq(:cross_dependency)
+ end
+ end
+
+ context 'when artifacts is provided' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false } }
+
+ it { is_expected.to be_valid }
+
+ it 'returns the correct value' do
+ expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false)
+ end
+ end
+ end
+
+ context 'when config contains not allowed keys' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', something: 'else' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error' do
+ expect(need.errors)
+ .to contain_exactly('cross pipeline dependency config contains unknown keys: something')
+ end
+ end
+ end
+
context 'when need config is not a string or a hash' do
let(:config) { :job_name }
diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
index f3b9d0c3c84..f11f2a56f5f 100644
--- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) }
before do
- needs.metadata[:allowed_needs] = %i[job]
+ needs.metadata[:allowed_needs] = %i[job cross_dependency]
end
describe 'validations' do
@@ -66,6 +66,27 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
end
end
end
+
+ context 'with too many cross pipeline dependencies' do
+ let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT }
+
+ let(:config) do
+ Array.new(limit.next) do |index|
+ { pipeline: "$UPSTREAM_PIPELINE_#{index}", job: 'job-1' }
+ end
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(needs.errors).to contain_exactly(
+ "needs config must be less than or equal to #{limit}")
+ end
+ end
+ end
end
describe '.compose!' do
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index ac8dd2a3267..aadf94365c6 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -361,7 +361,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
context 'when root yaml variables are used' do
let(:variables) do
Gitlab::Ci::Config::Entry::Variables.new(
- A: 'root', C: 'root', D: 'root'
+ { A: 'root', C: 'root', D: 'root' }
).value
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 79716df6b60..54c7a5c3602 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: 'ruby:2.7',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
- variables: { VAR: 'root' },
+ variables: { VAR: 'root', VAR2: { value: 'val 2', description: 'this is var 2' } },
after_script: ['make clean'],
stages: %w(build pages release),
cache: { key: 'k', untracked: true, paths: ['public/'] },
@@ -80,6 +80,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
.to eq 'List of external YAML files to include.'
end
+ it 'sets correct variables value' do
+ expect(root.variables_value).to eq('VAR' => 'root', 'VAR2' => 'val 2')
+ end
+
describe '#leaf?' do
it 'is not leaf' do
expect(root).not_to be_leaf
@@ -128,7 +132,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
- variables: { 'VAR' => 'root' },
+ variables: { 'VAR' => 'root', 'VAR2' => 'val 2' },
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] },
@@ -142,7 +146,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' },
- variables: { 'VAR' => 'root' },
+ variables: { 'VAR' => 'root', 'VAR2' => 'val 2' },
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] },
@@ -158,7 +162,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' },
only: { refs: %w(branches tags) },
- variables: { 'VAR' => 'job' },
+ variables: { 'VAR' => 'job', 'VAR2' => 'val 2' },
after_script: [],
ignore: false,
scheduling_type: :stage }
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index 4a43e6c9a86..d1bd22e5573 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -339,6 +339,22 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do
end
end
end
+
+ context 'with an invalid variables' do
+ let(:config) do
+ { if: '$THIS == "that"', variables: 'hello' }
+ end
+
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid variables:' do
+ expect(subject.errors).to include(/variables config should be a hash of key value pairs/)
+ end
+ end
end
context 'allow_failure: validation' do
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index ec137ef2ae4..2795cc9dddf 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
{ name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
end
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
index e4f8a348d21..85e7f297b03 100644
--- a/spec/lib/gitlab/ci/config/entry/services_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Services do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] }
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index ac33f858f43..426a38e2ef7 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Variables do
- subject { described_class.new(config) }
+ let(:metadata) { {} }
+
+ subject { described_class.new(config, metadata) }
shared_examples 'valid config' do
describe '#value' do
@@ -71,7 +73,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
end
- it_behaves_like 'valid config'
+ it_behaves_like 'invalid config'
+
+ context 'when metadata has use_value_data' do
+ let(:metadata) { { use_value_data: true } }
+
+ it_behaves_like 'valid config'
+ end
end
context 'when entry value is an array' do
@@ -80,32 +88,36 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
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
+ context 'when metadata has use_value_data' do
+ let(:metadata) { { use_value_data: true } }
- 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
- context 'when entry config value has hash with nil description' do
- let(:config) do
- { 'VARIABLE_1' => { value: 'value 1', description: nil } }
+ it_behaves_like 'invalid config'
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
- context 'when entry config value has hash without description' do
- let(:config) do
- { 'VARIABLE_1' => { value: 'value 1' } }
+ it_behaves_like 'invalid config'
end
- let(:result) do
- { 'VARIABLE_1' => 'value 1' }
- end
+ context 'when entry config value has hash without description' do
+ let(:config) do
+ { 'VARIABLE_1' => { value: 'value 1' } }
+ end
- it_behaves_like 'valid config'
+ let(:result) do
+ { 'VARIABLE_1' => 'value 1' }
+ end
+
+ it_behaves_like 'valid config'
+ end
end
end
diff --git a/spec/lib/gitlab/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb
index 7b2d6b58518..7d950c86700 100644
--- a/spec/lib/gitlab/ci/mask_secret_spec.rb
+++ b/spec/lib/gitlab/ci/mask_secret_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe Gitlab::Ci::MaskSecret do
expect(mask('token', nil)).to eq('token')
end
+ it 'does not change a bytesize of a value' do
+ expect(mask('token-ü/unicode', 'token-ü').bytesize).to eq 16
+ end
+
def mask(value, token)
subject.mask!(value.dup, token)
end
diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb
new file mode 100644
index 00000000000..c6b8cf2a985
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
+ describe '#parse!' do
+ subject(:parse) { described_class.new.parse!(code_climate, codequality_report) }
+
+ let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:code_climate) do
+ [
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }
+ ].to_json
+ end
+
+ context "when data is code_climate style JSON" do
+ context "when there are no degradations" do
+ let(:code_climate) { [].to_json }
+
+ it "returns a codequality report" do
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.degradations_count).to eq(0)
+ end
+ end
+
+ context "when there are degradations" do
+ it "returns a codequality report" do
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.degradations_count).to eq(1)
+ end
+ end
+ end
+
+ context "when data is not a valid JSON string" do
+ let(:code_climate) do
+ [
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }
+ ]
+ end
+
+ it "sets error_message" do
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.error_message).to include('JSON parsing failed')
+ end
+ end
+
+ context 'when degradations contain an invalid one' do
+ let(:code_climate) do
+ [
+ {
+ "type": "Issue",
+ "check_name": "Rubocop/Metrics/ParameterLists",
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
+ "severity": "minor"
+ },
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }
+ ].to_json
+ end
+
+ it 'stops parsing the report' do
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.degradations_count).to eq(0)
+ expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
index 45e87466532..2313378d1e9 100644
--- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
@@ -4,207 +4,690 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
describe '#parse!' do
- subject { described_class.new.parse!(cobertura, coverage_report) }
+ subject(:parse_report) { described_class.new.parse!(cobertura, coverage_report, project_path: project_path, worktree_paths: paths) }
let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new }
+ let(:project_path) { 'foo/bar' }
+ let(:paths) { ['app/user.rb'] }
+
+ let(:cobertura) do
+ <<~EOF
+ <coverage>
+ #{sources_xml}
+ #{classes_xml}
+ </coverage>
+ EOF
+ end
context 'when data is Cobertura style XML' do
- context 'when there is no <class>' do
- let(:cobertura) { '' }
+ shared_examples_for 'ignoring sources, project_path, and worktree_paths' do
+ context 'when there is no <class>' do
+ let(:classes_xml) { '' }
- it 'parses XML and returns empty coverage' do
- expect { subject }.not_to raise_error
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
- expect(coverage_report.files).to eq({})
+ expect(coverage_report.files).to eq({})
+ end
end
- end
- context 'when there is a <sources>' do
- shared_examples_for 'ignoring sources' do
- it 'parses XML without errors' do
- expect { subject }.not_to raise_error
+ context 'when there is a single <class>' do
+ context 'with no lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
+ end
- expect(coverage_report.files).to eq({})
+ context 'with a single line' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
+ end
+ end
+
+ context 'without a package parent' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages>
+ <class filename="app.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
+ end
+ end
+
+ context 'with multiple lines and methods info' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
+ end
end
end
- context 'and has a single source' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
+ context 'when there are multiple <class>' do
+ context 'without a package parent' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ <class filename="foo.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ </lines></class>
+ </packages>
+ EOF
+ end
+
+ it 'parses XML and returns coverage information per class' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 }, 'foo.rb' => { 6 => 1 } })
+ end
+ end
+
+ context 'with the same filename and different lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="app.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with merged coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
+ end
+ end
+
+ context 'with the same filename and lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="1"/>
+ <line number="2" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with summed-up coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } })
+ end
+ end
+
+ context 'with missing filename' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and ignores class with missing name' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
+ end
+ end
+
+ context 'with invalid line information' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="app.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="app.rb"><methods/><lines>
+ <line null="test" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'raises an error' do
+ expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
+ end
+ end
+ end
+ end
+
+ context 'when there is no <sources>' do
+ let(:sources_xml) { '' }
+
+ it_behaves_like 'ignoring sources, project_path, and worktree_paths'
+ end
+
+ context 'when there is a <sources>' do
+ context 'and has a single source with a pattern for Go projects' do
+ let(:project_path) { 'local/go' } # Make sure we're not making false positives
+ let(:sources_xml) do
+ <<~EOF
<sources>
- <source>project/src</source>
+ <source>/usr/local/go/src</source>
</sources>
EOF
end
- it_behaves_like 'ignoring sources'
+ it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
- context 'and has multiple sources' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
+ context 'and has multiple sources with a pattern for Go projects' do
+ let(:project_path) { 'local/go' } # Make sure we're not making false positives
+ let(:sources_xml) do
+ <<~EOF
<sources>
- <source>project/src/foo</source>
- <source>project/src/bar</source>
+ <source>/usr/local/go/src</source>
+ <source>/go/src</source>
</sources>
EOF
end
- it_behaves_like 'ignoring sources'
+ it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
- end
- context 'when there is a single <class>' do
- context 'with no lines' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes><class filename="app.rb"></class></classes>
+ context 'and has a single source but already is at the project root path' do
+ let(:sources_xml) do
+ <<~EOF
+ <sources>
+ <source>builds/#{project_path}</source>
+ </sources>
EOF
end
- it 'parses XML and returns empty coverage' do
- expect { subject }.not_to raise_error
+ it_behaves_like 'ignoring sources, project_path, and worktree_paths'
+ end
- expect(coverage_report.files).to eq({})
+ context 'and has multiple sources but already are at the project root path' do
+ let(:sources_xml) do
+ <<~EOF
+ <sources>
+ <source>builds/#{project_path}/</source>
+ <source>builds/somewhere/#{project_path}</source>
+ </sources>
+ EOF
end
+
+ it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
- context 'with a single line' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes>
- <class filename="app.rb"><lines>
- <line number="1" hits="2"/>
- </lines></class>
- </classes>
+ context 'and has a single source that is not at the project root path' do
+ let(:sources_xml) do
+ <<~EOF
+ <sources>
+ <source>builds/#{project_path}/app</source>
+ </sources>
EOF
end
- it 'parses XML and returns a single file with coverage' do
- expect { subject }.not_to raise_error
+ context 'when there is no <class>' do
+ let(:classes_xml) { '' }
- expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
- end
- end
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
- context 'with multipe lines and methods info' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="2"/>
- <line number="2" hits="0"/>
- </lines></class>
- </classes>
- EOF
+ expect(coverage_report.files).to eq({})
+ end
end
- it 'parses XML and returns a single file with coverage' do
- expect { subject }.not_to raise_error
+ context 'when there is a single <class>' do
+ context 'with no lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
+ end
+
+ context 'with a single line but the filename cannot be determined based on extracted source and worktree paths' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="member.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
+ end
+
+ context 'with a single line' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } })
+ end
+ end
+
+ context 'with multiple lines and methods info' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
+ end
+ end
+ end
- expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
+ context 'when there are multiple <class>' do
+ context 'with the same filename but the filename cannot be determined based on extracted source and worktree paths' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="member.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="member.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
+ end
+
+ context 'without a parent package' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="user.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </packages>
+ EOF
+ end
+
+ it 'parses XML and returns coverage information with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
+ end
+ end
+
+ context 'with the same filename and different lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="user.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with merged coverage, and with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
+ end
+ end
+
+ context 'with the same filename and lines' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="1"/>
+ <line number="2" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with summed-up coverage, and with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 3, 2 => 1 } })
+ end
+ end
+
+ context 'with missing filename' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and ignores class with missing name' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
+ end
+ end
+
+ context 'with filename that cannot be determined based on extracted source and worktree paths' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="member.rb"><methods/><lines>
+ <line number="6" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and ignores class with undetermined filename' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
+ end
+ end
+
+ context 'with invalid line information' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><methods/><lines>
+ <line number="1" hits="2"/>
+ <line number="2" hits="0"/>
+ </lines></class>
+ <class filename="user.rb"><methods/><lines>
+ <line null="test" hits="1"/>
+ <line number="7" hits="1"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'raises an error' do
+ expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
+ end
+ end
end
end
- end
- context 'when there are multipe <class>' do
- context 'with the same filename and different lines' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="2"/>
- <line number="2" hits="0"/>
- </lines></class>
- <class filename="app.rb"><methods/><lines>
- <line number="6" hits="1"/>
- <line number="7" hits="1"/>
- </lines></class>
- </classes>
+ context 'and has multiple sources that are not at the project root path' do
+ let(:sources_xml) do
+ <<~EOF
+ <sources>
+ <source>builds/#{project_path}/app1/</source>
+ <source>builds/#{project_path}/app2/</source>
+ </sources>
EOF
end
- it 'parses XML and returns a single file with merged coverage' do
- expect { subject }.not_to raise_error
-
- expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
+ context 'and a class filename is available under multiple extracted sources' do
+ let(:paths) { ['app1/user.rb', 'app2/user.rb'] }
+
+ let(:classes_xml) do
+ <<~EOF
+ <package name="app1">
+ <classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes>
+ </package>
+ <package name="app2">
+ <classes>
+ <class filename="user.rb"><lines>
+ <line number="2" hits="3"/>
+ </lines></class>
+ </classes>
+ </package>
+ EOF
+ end
+
+ it 'parses XML and returns the files with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({
+ 'app1/user.rb' => { 1 => 2 },
+ 'app2/user.rb' => { 2 => 3 }
+ })
+ end
end
- end
- context 'with the same filename and lines' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <packages><package><classes>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="2"/>
- <line number="2" hits="0"/>
- </lines></class>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="1"/>
- <line number="2" hits="1"/>
- </lines></class>
- </classes></package></packages>
- EOF
+ context 'and a class filename is available under one of the extracted sources' do
+ let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] }
+
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with the filename relative to project root using the extracted source where it is first found under' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app2/user.rb' => { 1 => 2 } })
+ end
end
- it 'parses XML and returns a single file with summed-up coverage' do
- expect { subject }.not_to raise_error
+ context 'and a class filename is not found under any of the extracted sources' do
+ let(:paths) { ['app1/member.rb', 'app2/pet.rb'] }
- expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } })
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
end
- end
- context 'with missing filename' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="2"/>
- <line number="2" hits="0"/>
- </lines></class>
- <class><methods/><lines>
- <line number="6" hits="1"/>
- <line number="7" hits="1"/>
- </lines></class>
- </classes>
- EOF
+ context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do
+ let(:paths) { ['app2/user.rb'] }
+
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="record.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ before do
+ stub_const("#{described_class}::MAX_SOURCES", 1)
+ end
+
+ it 'parses XML and returns empty coverage' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
end
+ end
+ end
- it 'parses XML and ignores class with missing name' do
- expect { subject }.not_to raise_error
+ shared_examples_for 'non-smart parsing' do
+ let(:sources_xml) do
+ <<~EOF
+ <sources>
+ <source>builds/foo/bar/app</source>
+ </sources>
+ EOF
+ end
- expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
- end
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
end
- context 'with invalid line information' do
- let(:cobertura) do
- <<-EOF.strip_heredoc
- <classes>
- <class filename="app.rb"><methods/><lines>
- <line number="1" hits="2"/>
- <line number="2" hits="0"/>
- </lines></class>
- <class filename="app.rb"><methods/><lines>
- <line null="test" hits="1"/>
- <line number="7" hits="1"/>
- </lines></class>
- </classes>
- EOF
- end
+ it 'parses XML and returns filenames unchanged just as how they are found in the class node' do
+ expect { parse_report }.not_to raise_error
- it 'raises an error' do
- expect { subject }.to raise_error(described_class::CoberturaParserError)
- end
+ expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } })
end
end
+
+ context 'when project_path is not present' do
+ let(:project_path) { nil }
+ let(:paths) { ['app/user.rb'] }
+
+ it_behaves_like 'non-smart parsing'
+ end
+
+ context 'when worktree_paths is not present' do
+ let(:project_path) { 'foo/bar' }
+ let(:paths) { nil }
+
+ it_behaves_like 'non-smart parsing'
+ end
end
context 'when data is not Cobertura style XML' do
let(:cobertura) { { coverage: '12%' }.to_json }
it 'raises an error' do
- expect { subject }.to raise_error(described_class::CoberturaParserError)
+ expect { parse_report }.to raise_error(described_class::InvalidXMLError)
end
end
end
diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index db9a5775d9f..b932cd81272 100644
--- a/spec/lib/gitlab/ci/parsers_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -30,6 +30,14 @@ RSpec.describe Gitlab::Ci::Parsers do
end
end
+ context 'when file_type is codequality' do
+ let(:file_type) { 'codequality' }
+
+ it 'fabricates the class' do
+ is_expected.to be_a(described_class::Codequality::CodeClimate)
+ end
+ end
+
context 'when file_type is terraform' do
let(:file_type) { 'terraform' }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
new file mode 100644
index 00000000000..78363be7f36
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project, reload: true) { create(:project, namespace: namespace) }
+ let_it_be(:plan_limits, reload: true) { create(:plan_limits, :default_plan) }
+
+ let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2) }
+ let(:save_incompleted) { false }
+
+ let(:command) do
+ double(:command,
+ project: project,
+ pipeline_seed: pipeline_seed,
+ save_incompleted: save_incompleted
+ )
+ end
+
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+ let(:step) { described_class.new(pipeline, command) }
+
+ subject(:perform) { step.perform! }
+
+ context 'when pipeline deployments limit is exceeded' do
+ before do
+ plan_limits.update!(ci_pipeline_deployments: 1)
+ end
+
+ context 'when saving incompleted pipelines' do
+ let(:save_incompleted) { true }
+
+ it 'drops the pipeline' do
+ perform
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.reload).to be_failed
+ end
+
+ it 'breaks the chain' do
+ perform
+
+ expect(step.break?).to be true
+ end
+
+ it 'sets a valid failure reason' do
+ perform
+
+ expect(pipeline.deployments_limit_exceeded?).to be true
+ end
+ end
+
+ context 'when not saving incomplete pipelines' do
+ let(:save_incompleted) { false }
+
+ it 'does not persist the pipeline' do
+ perform
+
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'breaks the chain' do
+ perform
+
+ expect(step.break?).to be true
+ end
+
+ it 'adds an informative error to the pipeline' do
+ perform
+
+ expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.'])
+ end
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Gitlab::Ci::Limit::LimitExceededError),
+ project_id: project.id, plan: namespace.actual_plan_name
+ )
+
+ perform
+ end
+ end
+
+ context 'when pipeline deployments limit is not exceeded' do
+ before do
+ plan_limits.update!(ci_pipeline_deployments: 100)
+ end
+
+ it 'does not break the chain' do
+ perform
+
+ expect(step.break?).to be false
+ end
+
+ it 'does not invalidate the pipeline' do
+ perform
+
+ expect(pipeline.errors).to be_empty
+ end
+
+ it 'does not log any error' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ perform
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
index d849c768a3c..0ce8b80902e 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
@@ -50,8 +50,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'sets the seeds in the command object' do
run_chain
- expect(command.stage_seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
- expect(command.stage_seeds.count).to eq 1
+ expect(command.pipeline_seed).to be_a(Gitlab::Ci::Pipeline::Seed::Pipeline)
+ expect(command.pipeline_seed.size).to eq 1
end
context 'when no ref policy is specified' do
@@ -63,16 +63,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
- it 'correctly fabricates a stage seeds object' do
+ it 'correctly fabricates stages and builds' do
run_chain
- seeds = command.stage_seeds
- expect(seeds.size).to eq 2
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.second.attributes[:name]).to eq 'deploy'
- expect(seeds.dig(0, 0, :name)).to eq 'rspec'
- expect(seeds.dig(0, 1, :name)).to eq 'spinach'
- expect(seeds.dig(1, 0, :name)).to eq 'production'
+ seed = command.pipeline_seed
+
+ expect(seed.stages.size).to eq 2
+ expect(seed.size).to eq 3
+ expect(seed.stages.first.name).to eq 'test'
+ expect(seed.stages.second.name).to eq 'deploy'
+ expect(seed.stages[0].statuses[0].name).to eq 'rspec'
+ expect(seed.stages[0].statuses[1].name).to eq 'spinach'
+ expect(seed.stages[1].statuses[0].name).to eq 'production'
end
end
@@ -88,14 +90,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
- it 'returns stage seeds only assigned to master' do
+ it 'returns pipeline seed with jobs only assigned to master' do
run_chain
- seeds = command.stage_seeds
+ seed = command.pipeline_seed
- expect(seeds.size).to eq 1
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seed.size).to eq 1
+ expect(seed.stages.first.name).to eq 'test'
+ expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
@@ -109,14 +111,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
- it 'returns stage seeds only assigned to schedules' do
+ it 'returns pipeline seed with jobs only assigned to schedules' do
run_chain
- seeds = command.stage_seeds
+ seed = command.pipeline_seed
- expect(seeds.size).to eq 1
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seed.size).to eq 1
+ expect(seed.stages.first.name).to eq 'test'
+ expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
@@ -141,11 +143,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'returns seeds for kubernetes dependent job' do
run_chain
- seeds = command.stage_seeds
+ seed = command.pipeline_seed
- expect(seeds.size).to eq 2
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
- expect(seeds.dig(1, 0, :name)).to eq 'production'
+ expect(seed.size).to eq 2
+ expect(seed.stages[0].statuses[0].name).to eq 'spinach'
+ expect(seed.stages[1].statuses[0].name).to eq 'production'
end
end
end
@@ -154,10 +156,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'does not return seeds for kubernetes dependent job' do
run_chain
- seeds = command.stage_seeds
+ seed = command.pipeline_seed
- expect(seeds.size).to eq 1
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seed.size).to eq 1
+ expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
end
@@ -173,10 +175,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'returns stage seeds only when variables expression is truthy' do
run_chain
- seeds = command.stage_seeds
+ seed = command.pipeline_seed
- expect(seeds.size).to eq 1
- expect(seeds.dig(0, 0, :name)).to eq 'unit'
+ expect(seed.size).to eq 1
+ expect(seed.stages[0].statuses[0].name).to eq 'unit'
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
new file mode 100644
index 00000000000..c52994fc6a2
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:default_plan, reload: true) { create(:default_plan) }
+ let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) }
+
+ let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
+
+ let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2)}
+
+ let(:command) do
+ double(:command,
+ project: project,
+ pipeline_seed: pipeline_seed,
+ save_incompleted: true
+ )
+ end
+
+ let(:ci_pipeline_deployments_limit) { 0 }
+
+ before do
+ plan_limits.update!(ci_pipeline_deployments: ci_pipeline_deployments_limit)
+ end
+
+ subject(:quota) { described_class.new(namespace, pipeline, command) }
+
+ shared_context 'limit exceeded' do
+ let(:ci_pipeline_deployments_limit) { 1 }
+ end
+
+ shared_context 'limit not exceeded' do
+ let(:ci_pipeline_deployments_limit) { 2 }
+ end
+
+ describe '#enabled?' do
+ context 'when limit is enabled in plan' do
+ let(:ci_pipeline_deployments_limit) { 10 }
+
+ it 'is enabled' do
+ expect(quota).to be_enabled
+ end
+ end
+
+ context 'when limit is not enabled' do
+ let(:ci_pipeline_deployments_limit) { 0 }
+
+ it 'is not enabled' do
+ expect(quota).not_to be_enabled
+ end
+ end
+
+ context 'when limit does not exist' do
+ before do
+ allow(namespace).to receive(:actual_plan) { create(:default_plan) }
+ end
+
+ it 'is enabled by default' do
+ expect(quota).to be_enabled
+ end
+ end
+ end
+
+ describe '#exceeded?' do
+ context 'when limit is exceeded' do
+ include_context 'limit exceeded'
+
+ it 'is exceeded' do
+ expect(quota).to be_exceeded
+ end
+ end
+
+ context 'when limit is not exceeded' do
+ include_context 'limit not exceeded'
+
+ it 'is not exceeded' do
+ expect(quota).not_to be_exceeded
+ end
+ end
+ end
+
+ describe '#message' do
+ context 'when limit is exceeded' do
+ include_context 'limit exceeded'
+
+ it 'returns info about pipeline deployment limit exceeded' do
+ expect(quota.message)
+ .to eq "Pipeline has too many deployments! Requested 2, but the limit is 1."
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 0b961336f3f..bc10e94c81d 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -71,6 +71,33 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
+ context 'with job:rules:[variables:]' do
+ let(:attributes) do
+ { name: 'rspec',
+ ref: 'master',
+ yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true },
+ { key: 'VAR2', value: 'var 2', public: true }],
+ rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] }
+ end
+
+ it do
+ is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true },
+ { key: 'VAR2', value: 'var 2', public: true },
+ { key: 'VAR3', value: 'var 3', public: true }])
+ end
+
+ context 'when FF ci_rules_variables is disabled' do
+ before do
+ stub_feature_flags(ci_rules_variables: false)
+ end
+
+ it do
+ is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true },
+ { key: 'VAR2', value: 'var 2', public: true }])
+ end
+ end
+ end
+
context 'with cache:key' do
let(:attributes) do
{
@@ -165,6 +192,45 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to include(options: {}) }
end
+
+ context 'with allow_failure' do
+ let(:options) do
+ { allow_failure_criteria: { exit_codes: [42] } }
+ end
+
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always' }]
+ end
+
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ options: options,
+ rules: rules
+ }
+ end
+
+ context 'when rules does not override allow_failure' do
+ it { is_expected.to match a_hash_including(options: options) }
+ end
+
+ context 'when rules set allow_failure to true' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: true }]
+ end
+
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ end
+
+ context 'when rules set allow_failure to false' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: false }]
+ end
+
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ end
+ end
end
describe '#bridge?' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
index e62bf042fba..664aaaedf7b 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -85,16 +85,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
end
it_behaves_like 'returning a correct environment'
-
- context 'but the environment auto_stop_in on create flag is disabled' do
- let(:expected_auto_stop_in) { nil }
-
- before do
- stub_feature_flags(environment_auto_stop_start_on_create: false)
- end
-
- it_behaves_like 'returning a correct environment'
- end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
new file mode 100644
index 00000000000..1790388da03
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:stages_attributes) do
+ [
+ {
+ name: 'build',
+ index: 0,
+ builds: [
+ { name: 'init', scheduling_type: :stage },
+ { name: 'build', scheduling_type: :stage }
+ ]
+ },
+ {
+ name: 'test',
+ index: 1,
+ builds: [
+ { name: 'rspec', scheduling_type: :stage },
+ { name: 'staging', scheduling_type: :stage, environment: 'staging' },
+ { name: 'deploy', scheduling_type: :stage, environment: 'production' }
+ ]
+ }
+ ]
+ end
+
+ subject(:seed) do
+ described_class.new(pipeline, stages_attributes)
+ end
+
+ describe '#stages' do
+ it 'returns the stage resources' do
+ stages = seed.stages
+
+ expect(stages).to all(be_a(Ci::Stage))
+ expect(stages.map(&:name)).to contain_exactly('build', 'test')
+ end
+ end
+
+ describe '#size' do
+ it 'returns the number of jobs' do
+ expect(seed.size).to eq(5)
+ end
+ end
+
+ describe '#errors' do
+ context 'when attributes are valid' do
+ it 'returns nil' do
+ expect(seed.errors).to be_nil
+ end
+ end
+
+ context 'when attributes are not valid' do
+ it 'returns the errors' do
+ stages_attributes[0][:builds] << {
+ name: 'invalid_job',
+ scheduling_type: :dag,
+ needs_attributes: [{ name: 'non-existent', artifacts: true }]
+ }
+
+ expect(seed.errors).to contain_exactly("invalid_job: needs 'non-existent'")
+ end
+ end
+ end
+
+ describe '#deployments_count' do
+ it 'counts the jobs having an environment associated' do
+ expect(seed.deployments_count).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
index 650ae41320b..ade0e36cf1e 100644
--- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
- let(:comparer) { described_class.new(base_reports, head_reports) }
- let(:base_reports) { Gitlab::Ci::Reports::AccessibilityReports.new }
- let(:head_reports) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:comparer) { described_class.new(base_report, head_report) }
+ let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
let(:url) { "https://gitlab.com" }
let(:single_error) do
[
@@ -38,233 +38,254 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
end
describe '#status' do
- subject { comparer.status }
+ subject(:status) { comparer.status }
context 'when head report has an error' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns status failed' do
- expect(subject).to eq(described_class::STATUS_FAILED)
+ expect(status).to eq(described_class::STATUS_FAILED)
end
end
context 'when head reports does not have errors' do
before do
- head_reports.add_url(url, [])
+ head_report.add_url(url, [])
end
it 'returns status success' do
- expect(subject).to eq(described_class::STATUS_SUCCESS)
+ expect(status).to eq(described_class::STATUS_SUCCESS)
end
end
end
describe '#errors_count' do
- subject { comparer.errors_count }
+ subject(:errors_count) { comparer.errors_count }
context 'when head report has an error' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns the number of new errors' do
- expect(subject).to eq(1)
+ expect(errors_count).to eq(1)
end
end
context 'when head reports does not have an error' do
before do
- head_reports.add_url(url, [])
+ head_report.add_url(url, [])
end
it 'returns the number new errors' do
- expect(subject).to eq(0)
+ expect(errors_count).to eq(0)
end
end
end
describe '#resolved_count' do
- subject { comparer.resolved_count }
+ subject(:resolved_count) { comparer.resolved_count }
context 'when base reports has an error and head has a different error' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
end
it 'returns the resolved count' do
- expect(subject).to eq(1)
+ expect(resolved_count).to eq(1)
end
end
context 'when base reports has errors head has no errors' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, [])
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, [])
end
it 'returns the resolved count' do
- expect(subject).to eq(1)
+ expect(resolved_count).to eq(1)
end
end
context 'when base reports has errors and head has the same error' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, single_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns zero' do
- expect(subject).to eq(0)
+ expect(resolved_count).to eq(0)
end
end
context 'when base reports does not have errors and head has errors' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns the number of resolved errors' do
- expect(subject).to eq(0)
+ expect(resolved_count).to eq(0)
end
end
end
describe '#total_count' do
- subject { comparer.total_count }
+ subject(:total_count) { comparer.total_count }
context 'when base reports has an error' do
before do
- base_reports.add_url(url, single_error)
+ base_report.add_url(url, single_error)
end
- it 'returns the error count' do
- expect(subject).to eq(1)
+ it 'returns zero' do
+ expect(total_count).to be_zero
end
end
context 'when head report has an error' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
- it 'returns the error count' do
- expect(subject).to eq(1)
+ it 'returns the total count' do
+ expect(total_count).to eq(1)
end
end
context 'when base report has errors and head report has errors' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
+ end
+
+ it 'returns the total count' do
+ expect(total_count).to eq(1)
+ end
+ end
+
+ context 'when base report has errors and head report has the same error' do
+ before do
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error + different_error)
end
- it 'returns the error count' do
- expect(subject).to eq(2)
+ it 'returns the total count' do
+ expect(total_count).to eq(2)
end
end
end
describe '#existing_errors' do
- subject { comparer.existing_errors }
+ subject(:existing_errors) { comparer.existing_errors }
context 'when base report has errors and head has a different error' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
end
- it 'returns the existing errors' do
- expect(subject.size).to eq(1)
- expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ it 'returns an empty array' do
+ expect(existing_errors).to be_empty
end
end
context 'when base report does not have errors and head has errors' do
before do
- base_reports.add_url(url, [])
- head_reports.add_url(url, single_error)
+ base_report.add_url(url, [])
+ head_report.add_url(url, single_error)
end
it 'returns an empty array' do
- expect(subject).to be_empty
+ expect(existing_errors).to be_empty
+ end
+ end
+
+ context 'when base report has errors and head report has the same error' do
+ before do
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error + different_error)
+ end
+
+ it 'returns the existing error' do
+ expect(existing_errors).to eq(single_error)
end
end
end
describe '#new_errors' do
- subject { comparer.new_errors }
+ subject(:new_errors) { comparer.new_errors }
context 'when base reports has errors and head has more errors' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, single_error + different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error + different_error)
end
it 'returns new errors between base and head reports' do
- expect(subject.size).to eq(1)
- expect(subject.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail")
+ expect(new_errors.size).to eq(1)
+ expect(new_errors.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail")
end
end
context 'when base reports has an error and head has no errors' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, [])
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, [])
end
it 'returns an empty array' do
- expect(subject).to be_empty
+ expect(new_errors).to be_empty
end
end
context 'when base reports does not have errors and head has errors' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns the new error' do
- expect(subject.size).to eq(1)
- expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ expect(new_errors.size).to eq(1)
+ expect(new_errors.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
end
end
end
describe '#resolved_errors' do
- subject { comparer.resolved_errors }
+ subject(:resolved_errors) { comparer.resolved_errors }
context 'when base report has errors and head has more errors' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, single_error + different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error + different_error)
end
it 'returns an empty array' do
- expect(subject).to be_empty
+ expect(resolved_errors).to be_empty
end
end
context 'when base reports has errors and head has a different error' do
before do
- base_reports.add_url(url, single_error)
- head_reports.add_url(url, different_error)
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
end
it 'returns the resolved errors' do
- expect(subject.size).to eq(1)
- expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ expect(resolved_errors.size).to eq(1)
+ expect(resolved_errors.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
end
end
context 'when base reports does not have errors and head has errors' do
before do
- head_reports.add_url(url, single_error)
+ head_report.add_url(url, single_error)
end
it 'returns an empty array' do
- expect(subject).to be_empty
+ expect(resolved_errors).to be_empty
end
end
end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
new file mode 100644
index 00000000000..7053d54381b
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
@@ -0,0 +1,308 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do
+ let(:comparer) { described_class.new(base_report, head_report) }
+ let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:degradation_1) do
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }.with_indifferent_access
+ end
+
+ let(:degradation_2) do
+ {
+ "type": "Issue",
+ "check_name": "Rubocop/Metrics/ParameterLists",
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "categories": [
+ "Complexity"
+ ],
+ "remediation_points": 550000,
+ "location": {
+ "path": "foo.rb",
+ "positions": {
+ "begin": {
+ "column": 14,
+ "line": 10
+ },
+ "end": {
+ "column": 39,
+ "line": 10
+ }
+ }
+ },
+ "content": {
+ "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
+ },
+ "engine_name": "rubocop",
+ "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
+ "severity": "minor"
+ }.with_indifferent_access
+ end
+
+ describe '#status' do
+ subject(:report_status) { comparer.status }
+
+ context 'when head report has an error' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns status failed' do
+ expect(report_status).to eq(described_class::STATUS_FAILED)
+ end
+ end
+
+ context 'when head report does not have errors' do
+ it 'returns status success' do
+ expect(report_status).to eq(described_class::STATUS_SUCCESS)
+ end
+ end
+ end
+
+ describe '#errors_count' do
+ subject(:errors_count) { comparer.errors_count }
+
+ context 'when head report has an error' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns the number of new errors' do
+ expect(errors_count).to eq(1)
+ end
+ end
+
+ context 'when head report does not have an error' do
+ it 'returns zero' do
+ expect(errors_count).to be_zero
+ end
+ end
+ end
+
+ describe '#resolved_count' do
+ subject(:resolved_count) { comparer.resolved_count }
+
+ context 'when base report has an error and head has a different error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'counts the base report error as resolved' do
+ expect(resolved_count).to eq(1)
+ end
+ end
+
+ context 'when base report has errors head has no errors' do
+ before do
+ base_report.add_degradation(degradation_1)
+ end
+
+ it 'counts the base report errors as resolved' do
+ expect(resolved_count).to eq(1)
+ end
+ end
+
+ context 'when base report has errors and head has the same error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns zero' do
+ expect(resolved_count).to eq(0)
+ end
+ end
+
+ context 'when base report does not have errors and head has errors' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns zero' do
+ expect(resolved_count).to be_zero
+ end
+ end
+ end
+
+ describe '#total_count' do
+ subject(:total_count) { comparer.total_count }
+
+ context 'when base report has an error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ end
+
+ it 'returns zero' do
+ expect(total_count).to be_zero
+ end
+ end
+
+ context 'when head report has an error' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'includes the head report error in the count' do
+ expect(total_count).to eq(1)
+ end
+ end
+
+ context 'when base report has errors and head report has errors' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'includes errors in the count' do
+ expect(total_count).to eq(1)
+ end
+ end
+
+ context 'when base report has errors and head report has the same error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'includes errors in the count' do
+ expect(total_count).to eq(2)
+ end
+ end
+ end
+
+ describe '#existing_errors' do
+ subject(:existing_errors) { comparer.existing_errors }
+
+ context 'when base report has errors and head has the same error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'includes the base report errors' do
+ expect(existing_errors).to contain_exactly(degradation_1)
+ end
+ end
+
+ context 'when base report has errors and head has a different error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'returns an empty array' do
+ expect(existing_errors).to be_empty
+ end
+ end
+
+ context 'when base report does not have errors and head has errors' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns an empty array' do
+ expect(existing_errors).to be_empty
+ end
+ end
+ end
+
+ describe '#new_errors' do
+ subject(:new_errors) { comparer.new_errors }
+
+ context 'when base report has errors and head has more errors' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'includes errors not found in the base report' do
+ expect(new_errors).to eq([degradation_2])
+ end
+ end
+
+ context 'when base report has an error and head has no errors' do
+ before do
+ base_report.add_degradation(degradation_1)
+ end
+
+ it 'returns an empty array' do
+ expect(new_errors).to be_empty
+ end
+ end
+
+ context 'when base report does not have errors and head has errors' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns the head report error' do
+ expect(new_errors).to eq([degradation_1])
+ end
+ end
+ end
+
+ describe '#resolved_errors' do
+ subject(:resolved_errors) { comparer.resolved_errors }
+
+ context 'when base report errors are still found in the head report' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'returns an empty array' do
+ expect(resolved_errors).to be_empty
+ end
+ end
+
+ context 'when base report has errors and head has a different error' do
+ before do
+ base_report.add_degradation(degradation_1)
+ head_report.add_degradation(degradation_2)
+ end
+
+ it 'returns the base report error' do
+ expect(resolved_errors).to eq([degradation_1])
+ end
+ end
+
+ context 'when base report does not have errors and head has errors' do
+ before do
+ head_report.add_degradation(degradation_1)
+ end
+
+ it 'returns an empty array' do
+ expect(resolved_errors).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
new file mode 100644
index 00000000000..44e67259369
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
+ let(:codequality_report) { described_class.new }
+ let(:degradation_1) do
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }.with_indifferent_access
+ end
+
+ let(:degradation_2) do
+ {
+ "type": "Issue",
+ "check_name": "Rubocop/Metrics/ParameterLists",
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "categories": [
+ "Complexity"
+ ],
+ "remediation_points": 550000,
+ "location": {
+ "path": "foo.rb",
+ "positions": {
+ "begin": {
+ "column": 14,
+ "line": 10
+ },
+ "end": {
+ "column": 39,
+ "line": 10
+ }
+ }
+ },
+ "content": {
+ "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
+ },
+ "engine_name": "rubocop",
+ "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
+ "severity": "minor"
+ }.with_indifferent_access
+ end
+
+ it { expect(codequality_report.degradations).to eq({}) }
+
+ describe '#add_degradation' do
+ context 'when there is a degradation' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ end
+
+ it 'adds degradation to codequality report' do
+ expect(codequality_report.degradations.keys).to eq([degradation_1[:fingerprint]])
+ expect(codequality_report.degradations.values.size).to eq(1)
+ end
+ end
+
+ context 'when a required property is missing in the degradation' do
+ let(:invalid_degradation) do
+ {
+ "type": "Issue",
+ "check_name": "Rubocop/Metrics/ParameterLists",
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
+ "severity": "minor"
+ }.with_indifferent_access
+ end
+
+ it 'sets location as an error' do
+ codequality_report.add_degradation(invalid_degradation)
+
+ expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'")
+ end
+ end
+ end
+
+ describe '#set_error_message' do
+ context 'when there is an error' do
+ it 'sets errors' do
+ codequality_report.set_error_message("error")
+
+ expect(codequality_report.error_message).to eq("error")
+ end
+ end
+ end
+
+ describe '#degradations_count' do
+ subject(:degradations_count) { codequality_report.degradations_count }
+
+ context 'when there are many degradations' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'returns the number of degradations' do
+ expect(degradations_count).to eq(2)
+ end
+ end
+ end
+
+ describe '#all_degradations' do
+ subject(:all_degradations) { codequality_report.all_degradations }
+
+ context 'when there are many degradations' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'returns all degradations' do
+ expect(all_degradations).to contain_exactly(degradation_1, degradation_2)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb
new file mode 100644
index 00000000000..1e5e4766583
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::ReportsComparer do
+ let(:comparer) { described_class.new(base_report, head_report) }
+ let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+
+ describe '#initialize' do
+ context 'sets getter for the report comparer' do
+ it 'return base report' do
+ expect(comparer.base_report).to be_an_instance_of(Gitlab::Ci::Reports::CodequalityReports)
+ end
+
+ it 'return head report' do
+ expect(comparer.head_report).to be_an_instance_of(Gitlab::Ci::Reports::CodequalityReports)
+ end
+ end
+ end
+
+ describe '#status' do
+ subject(:status) { comparer.status }
+
+ it 'returns not implemented error' do
+ expect { status }.to raise_error(NotImplementedError)
+ end
+
+ context 'when success? is true' do
+ before do
+ allow(comparer).to receive(:success?).and_return(true)
+ end
+
+ it 'returns status success' do
+ expect(status).to eq('success')
+ end
+ end
+
+ context 'when success? is false' do
+ before do
+ allow(comparer).to receive(:success?).and_return(false)
+ end
+
+ it 'returns status failed' do
+ expect(status).to eq('failed')
+ end
+ end
+ end
+
+ describe '#success?' do
+ subject(:success?) { comparer.success? }
+
+ it 'returns not implemented error' do
+ expect { success? }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#existing_errors' do
+ subject(:existing_errors) { comparer.existing_errors }
+
+ it 'returns not implemented error' do
+ expect { existing_errors }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#resolved_errors' do
+ subject(:resolved_errors) { comparer.resolved_errors }
+
+ it 'returns not implemented error' do
+ expect { resolved_errors }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#errors_count' do
+ subject(:errors_count) { comparer.errors_count }
+
+ it 'returns not implemented error' do
+ expect { errors_count }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#resolved_count' do
+ subject(:resolved_count) { comparer.resolved_count }
+
+ it 'returns not implemented error' do
+ expect { resolved_count }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#total_count' do
+ subject(:total_count) { comparer.total_count }
+
+ it 'returns not implemented error' do
+ expect { total_count }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb
new file mode 100644
index 00000000000..1f8e32ce019
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/npm_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'npm.latest.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('npm.latest') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+
+ let(:repo_files) { { 'package.json' => '{}', 'README.md' => '' } }
+ let(:modified_files) { %w[package.json] }
+ let(:project) { create(:project, :custom_repo, files: repo_files) }
+ let(:pipeline_branch) { project.default_branch }
+ let(:pipeline_tag) { 'v1.2.1' }
+ let(:pipeline_ref) { pipeline_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref ) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ def create_branch(name:)
+ ::Branches::CreateService.new(project, user).execute(name, project.default_branch)
+ end
+
+ def create_tag(name:)
+ ::Tags::CreateService.new(project, user).execute(name, project.default_branch, nil)
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+
+ create_branch(name: pipeline_branch)
+ create_tag(name: pipeline_tag)
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:modified_paths).and_return(modified_files)
+ end
+
+ shared_examples 'publish job created' do
+ it 'creates a pipeline with a single job: publish' do
+ expect(build_names).to eq(%w[publish])
+ end
+ end
+
+ shared_examples 'no pipeline created' do
+ it 'does not create a pipeline because the only job (publish) is not created' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.')
+ end
+ end
+
+ context 'on default branch' do
+ context 'when package.json has been changed' do
+ it_behaves_like 'publish job created'
+ end
+
+ context 'when package.json does not exist or has not been changed' do
+ let(:modified_files) { %w[README.md] }
+
+ it_behaves_like 'no pipeline created'
+ end
+ end
+
+ %w[v1.0.0 v2.1.0-alpha].each do |valid_version|
+ context "when the branch name is #{valid_version}" do
+ let(:pipeline_branch) { valid_version }
+
+ it_behaves_like 'publish job created'
+ end
+
+ context "when the tag name is #{valid_version}" do
+ let(:pipeline_tag) { valid_version }
+ let(:pipeline_ref) { pipeline_tag }
+
+ it_behaves_like 'publish job created'
+ end
+ end
+
+ %w[patch-1 my-feature-branch v1 v1.0 2.1.0].each do |invalid_version|
+ context "when the branch name is #{invalid_version}" do
+ let(:pipeline_branch) { invalid_version }
+
+ it_behaves_like 'no pipeline created'
+ end
+
+ context "when the tag name is #{invalid_version}" do
+ let(:pipeline_tag) { invalid_version }
+ let(:pipeline_ref) { pipeline_tag }
+
+ it_behaves_like 'no pipeline created'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/checksum_spec.rb b/spec/lib/gitlab/ci/trace/checksum_spec.rb
index 794794c3f69..a343d74f755 100644
--- a/spec/lib/gitlab/ci/trace/checksum_spec.rb
+++ b/spec/lib/gitlab/ci/trace/checksum_spec.rb
@@ -8,8 +8,12 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
subject { described_class.new(build) }
context 'when build pending state exists' do
+ let(:trace_details) do
+ { trace_checksum: 'crc32:d4777540', trace_bytesize: 262161 }
+ end
+
before do
- create(:ci_build_pending_state, build: build, trace_checksum: 'crc32:d4777540')
+ create(:ci_build_pending_state, build: build, **trace_details)
end
context 'when matching persisted trace chunks exist' do
@@ -22,6 +26,7 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
it 'calculates combined trace chunks CRC32 correctly' do
expect(subject.chunks_crc32).to eq 3564598592
expect(subject).to be_valid
+ expect(subject).not_to be_corrupted
end
end
@@ -32,8 +37,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
create_chunk(index: 2, data: 'ccccccccccccccccc')
end
- it 'makes trace checksum invalid' do
+ it 'makes trace checksum invalid but not corrupted' do
expect(subject).not_to be_valid
+ expect(subject).not_to be_corrupted
end
end
@@ -43,8 +49,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
create_chunk(index: 2, data: 'ccccccccccccccccc')
end
- it 'makes trace checksum invalid' do
+ it 'makes trace checksum invalid and corrupted' do
expect(subject).not_to be_valid
+ expect(subject).to be_corrupted
end
end
@@ -55,8 +62,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
create_chunk(index: 2, data: 'ccccccccccccccccc')
end
- it 'makes trace checksum invalid' do
+ it 'makes trace checksum invalid but not corrupted' do
expect(subject).not_to be_valid
+ expect(subject).not_to be_corrupted
end
end
@@ -99,6 +107,14 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
it 'returns nil' do
expect(subject.last_chunk).to be_nil
end
+
+ it 'is not a valid trace' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'is not a corrupted trace' do
+ expect(subject).not_to be_corrupted
+ end
end
context 'when there are multiple chunks' do
@@ -110,6 +126,26 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do
it 'returns chunk with the highest index' do
expect(subject.last_chunk.chunk_index).to eq 1
end
+
+ it 'is not a valid trace' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'is not a corrupted trace' do
+ expect(subject).not_to be_corrupted
+ end
+ end
+ end
+
+ describe '#trace_size' do
+ before do
+ create_chunk(index: 0, data: 'a' * 128.kilobytes)
+ create_chunk(index: 1, data: 'b' * 128.kilobytes)
+ create_chunk(index: 2, data: 'abcdefg-ü')
+ end
+
+ it 'returns total trace size in bytes' do
+ expect(subject.trace_size).to eq 262154
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index fb6395e888a..5ad1b3dd241 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -231,6 +231,23 @@ module Gitlab
expect(subject[:allow_failure]).to be true
end
end
+
+ context 'when allow_failure has exit_codes' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: { exit_codes: 1 } })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+
+ it 'saves allow_failure_criteria into options' do
+ expect(subject[:options]).to match(
+ a_hash_including(allow_failure_criteria: { exit_codes: [1] }))
+ end
+ end
end
context 'when job is not a manual action' do
@@ -254,6 +271,22 @@ module Gitlab
expect(subject[:allow_failure]).to be false
end
end
+
+ context 'when allow_failure is dynamically specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: { exit_codes: 1 } })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+
+ it 'saves allow_failure_criteria into options' do
+ expect(subject[:options]).to match(
+ a_hash_including(allow_failure_criteria: { exit_codes: [1] }))
+ end
+ end
end
end
@@ -2111,6 +2144,71 @@ module Gitlab
end
end
+ describe 'cross pipeline needs' do
+ context 'when configuration is valid' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: $THE_PIPELINE_ID
+ job: dependency-job
+ YAML
+ end
+
+ it 'returns a valid configuration and sets artifacts: true by default' do
+ expect(subject).to be_valid
+
+ rspec = subject.build_attributes(:rspec)
+ expect(rspec.dig(:options, :cross_dependencies)).to eq(
+ [{ pipeline: '$THE_PIPELINE_ID', job: 'dependency-job', artifacts: true }]
+ )
+ end
+
+ context 'when pipeline ID is hard-coded' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: "123"
+ job: dependency-job
+ YAML
+ end
+
+ it 'returns a valid configuration and sets artifacts: true by default' do
+ expect(subject).to be_valid
+
+ rspec = subject.build_attributes(:rspec)
+ expect(rspec.dig(:options, :cross_dependencies)).to eq(
+ [{ pipeline: '123', job: 'dependency-job', artifacts: true }]
+ )
+ end
+ end
+ end
+
+ context 'when configuration is not valid' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: $THE_PIPELINE_ID
+ job: dependency-job
+ something: else
+ YAML
+ end
+
+ it 'returns an error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors).to include(/:need config contains unknown keys: something/)
+ end
+ end
+ end
+
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
@@ -2429,7 +2527,13 @@ module Gitlab
context 'returns errors if job allow_failure parameter is not an boolean' do
let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) }
- it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a boolean value'
+ it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a hash or a boolean value'
+ end
+
+ context 'returns errors if job exit_code parameter from allow_failure is not an integer' do
+ let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: { exit_codes: 'string' } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:allow_failure exit codes should be an array of integers or an integer'
end
context 'returns errors if job stage is not a string' do
diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
index 05d744d95e2..a99bdcc9a0f 100644
--- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb
+++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
@@ -15,10 +15,10 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do
describe '#run!' do
shared_examples_for 'moves the file' do
shared_examples_for 'a real run' do
- let(:args) { [dry_run: false] }
+ let(:args) { { dry_run: false } }
it 'moves the file to its proper location' do
- subject.run!(*args)
+ subject.run!(**args)
expect(File.exist?(path)).to be_falsey
expect(File.exist?(new_path)).to be_truthy
@@ -28,13 +28,13 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...")
expect(logger).to receive(:info).with("Did #{action}")
- subject.run!(*args)
+ subject.run!(**args)
end
end
shared_examples_for 'a dry run' do
it 'does not move the file' do
- subject.run!(*args)
+ subject.run!(**args)
expect(File.exist?(path)).to be_truthy
expect(File.exist?(new_path)).to be_falsey
@@ -44,30 +44,30 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...")
expect(logger).to receive(:info).with("Can #{action}")
- subject.run!(*args)
+ subject.run!(**args)
end
end
context 'when dry_run is false' do
- let(:args) { [dry_run: false] }
+ let(:args) { { dry_run: false } }
it_behaves_like 'a real run'
end
context 'when dry_run is nil' do
- let(:args) { [dry_run: nil] }
+ let(:args) { { dry_run: nil } }
it_behaves_like 'a real run'
end
context 'when dry_run is true' do
- let(:args) { [dry_run: true] }
+ let(:args) { { dry_run: true } }
it_behaves_like 'a dry run'
end
context 'with dry_run not specified' do
- let(:args) { [] }
+ let(:args) { {} }
it_behaves_like 'a dry run'
end
diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb
index c72efa66024..0153cfbf091 100644
--- a/spec/lib/gitlab/config/entry/configurable_spec.rb
+++ b/spec/lib/gitlab/config/entry/configurable_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Config::Entry::Configurable do
describe 'validations' do
context 'when entry is a hash' do
- let(:instance) { entry.new(key: 'value') }
+ let(:instance) { entry.new({ key: 'value' }) }
it 'correctly validates an instance' do
expect(instance).to be_valid
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 2c5988f06b2..553f33a66c4 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -40,6 +40,9 @@ RSpec.describe 'value stream analytics events', :aggregate_failures do
before do
create_commit_referencing_issue(context)
+
+ # Adding extra duration because the new VSA backend filters out 0 durations between these columns
+ context.metrics.update!(first_mentioned_in_commit_at: context.metrics.first_associated_with_milestone_at + 1.day)
end
it 'has correct attributes' do
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 719d4a69985..21503dc1501 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
project.add_maintainer(user)
end
- let(:stage_summary) { described_class.new(project, options).data }
+ let(:stage_summary) { described_class.new(project, **options).data }
describe "#new_issues" do
subject { stage_summary.first }
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
it 'does not include commit stats' do
- data = described_class.new(project, options).data
+ data = described_class.new(project, **options).data
expect(includes_commits?(data)).to be_falsy
end
diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
deleted file mode 100644
index 9ebdacb16de..00000000000
--- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::CycleAnalytics::UsageData do
- describe '#to_json' do
- before do
- # Since git commits only have second precision, round up to the
- # nearest second to ensure we have accurate median and standard
- # deviation calculations.
- current_time = Time.at(Time.now.to_i)
-
- Timecop.freeze(current_time) do
- user = create(:user, :admin)
- projects = create_list(:project, 2, :repository)
-
- projects.each_with_index do |project, time|
- issue = create(:issue, project: project, created_at: (time + 1).hour.ago)
-
- allow_next_instance_of(Gitlab::ReferenceExtractor) do |instance|
- allow(instance).to receive(:issues).and_return([issue])
- end
-
- milestone = create(:milestone, project: project)
- mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
- pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
-
- create_cycle(user, project, issue, mr, milestone, pipeline)
- deploy_master(user, project, environment: 'staging')
- deploy_master(user, project)
- end
- end
- end
-
- context 'a valid usage data result' do
- let(:expect_values_per_stage) do
- {
- issue: {
- average: 5400,
- sd: 2545,
- missing: 0
- },
- plan: {
- average: 1,
- sd: 0,
- missing: 0
- },
- code: {
- average: nil,
- sd: 0,
- missing: 2
- },
- test: {
- average: nil,
- sd: 0,
- missing: 2
- },
- review: {
- average: 0,
- sd: 0,
- missing: 0
- },
- staging: {
- average: 0,
- sd: 0,
- missing: 0
- },
- production: {
- average: 5400,
- sd: 2545,
- missing: 0
- }
- }
- end
-
- it 'returns the aggregated usage data of every selected project', :sidekiq_might_not_need_inline do
- result = subject.to_json
-
- expect(result).to have_key(:avg_cycle_analytics)
-
- CycleAnalytics::LevelBase::STAGES.each do |stage|
- expect(result[:avg_cycle_analytics]).to have_key(stage)
-
- stage_values = result[:avg_cycle_analytics][stage]
- expected_values = expect_values_per_stage[stage]
-
- expected_values.each_pair do |op, value|
- expect(stage_values).to have_key(op)
- expect(stage_values[op]).to eq(value)
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/lib/gitlab/danger/base_linter_spec.rb
new file mode 100644
index 00000000000..bd0ceb5a125
--- /dev/null
+++ b/spec/lib/gitlab/danger/base_linter_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative 'danger_spec_helper'
+
+require 'gitlab/danger/base_linter'
+
+RSpec.describe Gitlab::Danger::BaseLinter do
+ let(:commit_class) do
+ Struct.new(:message, :sha, :diff_parent)
+ end
+
+ let(:commit_message) { 'A commit message' }
+ let(:commit) { commit_class.new(commit_message, anything, anything) }
+
+ subject(:commit_linter) { described_class.new(commit) }
+
+ describe '#failed?' do
+ context 'with no failures' do
+ it { expect(commit_linter).not_to be_failed }
+ end
+
+ context 'with failures' do
+ before do
+ commit_linter.add_problem(:subject_too_long, described_class.subject_description)
+ end
+
+ it { expect(commit_linter).to be_failed }
+ end
+ end
+
+ describe '#add_problem' do
+ it 'stores messages in #failures' do
+ commit_linter.add_problem(:subject_too_long, '%s')
+
+ expect(commit_linter.problems).to eq({ subject_too_long: described_class.problems_mapping[:subject_too_long] })
+ end
+ end
+
+ shared_examples 'a valid commit' do
+ it 'does not have any problem' do
+ commit_linter.lint_subject
+
+ expect(commit_linter.problems).to be_empty
+ end
+ end
+
+ describe '#lint_subject' do
+ context 'when subject valid' do
+ it_behaves_like 'a valid commit'
+ end
+
+ context 'when subject is too short' do
+ let(:commit_message) { 'A B' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+
+ context 'when subject is too long' do
+ let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+
+ context 'when subject is a WIP' do
+ let(:final_message) { 'A B C' }
+ # commit message with prefix will be over max length. commit message without prefix will be of maximum size
+ let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) }
+
+ it 'does not have any problems' do
+ commit_linter.lint_subject
+
+ expect(commit_linter.problems).to be_empty
+ end
+ end
+
+ context 'when subject is too short and too long' do
+ let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description)
+ expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+
+ context 'when subject starts with lowercase' do
+ let(:commit_message) { 'a B C' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+
+ [
+ '[ci skip] A commit message',
+ '[Ci skip] A commit message',
+ '[API] A commit message',
+ 'api: A commit message',
+ 'API: A commit message',
+ 'API: a commit message',
+ 'API: a commit message'
+ ].each do |message|
+ context "when subject is '#{message}'" do
+ let(:commit_message) { message }
+
+ it 'does not add a problem' do
+ expect(commit_linter).not_to receive(:add_problem)
+
+ commit_linter.lint_subject
+ end
+ end
+ end
+
+ [
+ '[ci skip]A commit message',
+ '[Ci skip] A commit message',
+ '[ci skip] a commit message',
+ 'api: a commit message',
+ '! A commit message'
+ ].each do |message|
+ context "when subject is '#{message}'" do
+ let(:commit_message) { message }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+ end
+
+ context 'when subject ends with a period' do
+ let(:commit_message) { 'A B C.' }
+
+ it 'adds a problem' do
+ expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class.subject_description)
+
+ commit_linter.lint_subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb
index ebfeedba700..d3d86037a53 100644
--- a/spec/lib/gitlab/danger/commit_linter_spec.rb
+++ b/spec/lib/gitlab/danger/commit_linter_spec.rb
@@ -98,28 +98,6 @@ RSpec.describe Gitlab::Danger::CommitLinter do
end
end
- describe '#failed?' do
- context 'with no failures' do
- it { expect(commit_linter).not_to be_failed }
- end
-
- context 'with failures' do
- before do
- commit_linter.add_problem(:details_line_too_long)
- end
-
- it { expect(commit_linter).to be_failed }
- end
- end
-
- describe '#add_problem' do
- it 'stores messages in #failures' do
- commit_linter.add_problem(:details_line_too_long)
-
- expect(commit_linter.problems).to eq({ details_line_too_long: described_class::PROBLEMS[:details_line_too_long] })
- end
- end
-
shared_examples 'a valid commit' do
it 'does not have any problem' do
commit_linter.lint
@@ -129,113 +107,6 @@ RSpec.describe Gitlab::Danger::CommitLinter do
end
describe '#lint' do
- describe 'subject' do
- context 'when subject valid' do
- it_behaves_like 'a valid commit'
- end
-
- context 'when subject is too short' do
- let(:commit_message) { 'A B' }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
-
- context 'when subject is too long' do
- let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
-
- context 'when subject is a WIP' do
- let(:final_message) { 'A B C' }
- # commit message with prefix will be over max length. commit message without prefix will be of maximum size
- let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) }
-
- it 'does not have any problems' do
- commit_linter.lint
-
- expect(commit_linter.problems).to be_empty
- end
- end
-
- context 'when subject is too short and too long' do
- let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION)
- expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
-
- context 'when subject starts with lowercase' do
- let(:commit_message) { 'a B C' }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
-
- [
- '[ci skip] A commit message',
- '[Ci skip] A commit message',
- '[API] A commit message',
- 'api: A commit message',
- 'API: A commit message'
- ].each do |message|
- context "when subject is '#{message}'" do
- let(:commit_message) { message }
-
- it 'does not add a problem' do
- expect(commit_linter).not_to receive(:add_problem)
-
- commit_linter.lint
- end
- end
- end
-
- [
- '[ci skip]A commit message',
- '[Ci skip] A commit message',
- '[ci skip] a commit message',
- 'API: a commit message',
- 'API: a commit message',
- 'api: a commit message',
- '! A commit message'
- ].each do |message|
- context "when subject is '#{message}'" do
- let(:commit_message) { message }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
- end
-
- context 'when subject ends with a period' do
- let(:commit_message) { 'A B C.' }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class::DEFAULT_SUBJECT_DESCRIPTION)
-
- commit_linter.lint
- end
- end
- end
-
describe 'separator' do
context 'when separator is missing' do
let(:commit_message) { "A B C\n" }
@@ -300,8 +171,10 @@ RSpec.describe Gitlab::Danger::CommitLinter do
end
end
- context 'when details exceeds the max line length including a URL' do
- let(:commit_message) { "A B C\n\nhttps://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH }
+ context 'when details exceeds the max line length including URLs' do
+ let(:commit_message) do
+ "A B C\n\nsome message with https://example.com and https://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH
+ end
it_behaves_like 'a valid commit'
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index f400641706d..a8f113a8cd1 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -33,6 +33,16 @@ RSpec.describe Gitlab::Danger::Helper do
expect(helper.gitlab_helper).to eq(fake_gitlab)
end
end
+
+ context 'when danger gitlab plugin is not available' do
+ it 'returns nil' do
+ invalid_danger = Class.new do
+ include Gitlab::Danger::Helper
+ end.new
+
+ expect(invalid_danger.gitlab_helper).to be_nil
+ end
+ end
end
describe '#release_automation?' do
@@ -591,4 +601,30 @@ RSpec.describe Gitlab::Danger::Helper do
expect(helper.prepare_labels_for_mr([])).to eq('')
end
end
+
+ describe '#has_ci_changes?' do
+ context 'when .gitlab/ci is changed' do
+ it 'returns true' do
+ expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab/ci/test.yml])
+
+ expect(helper.has_ci_changes?).to be_truthy
+ end
+ end
+
+ context 'when .gitlab-ci.yml is changed' do
+ it 'returns true' do
+ expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab-ci.yml])
+
+ expect(helper.has_ci_changes?).to be_truthy
+ end
+ end
+
+ context 'when neither .gitlab/ci/ or .gitlab-ci.yml is changed' do
+ it 'returns false' do
+ expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb nested/.gitlab-ci.yml])
+
+ expect(helper.has_ci_changes?).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/danger/merge_request_linter_spec.rb b/spec/lib/gitlab/danger/merge_request_linter_spec.rb
new file mode 100644
index 00000000000..29facc9fdd6
--- /dev/null
+++ b/spec/lib/gitlab/danger/merge_request_linter_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require_relative 'danger_spec_helper'
+
+require 'gitlab/danger/merge_request_linter'
+
+RSpec.describe Gitlab::Danger::MergeRequestLinter do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:mr_class) do
+ Struct.new(:message, :sha, :diff_parent)
+ end
+
+ let(:mr_title) { 'A B ' + 'C' }
+ let(:merge_request) { mr_class.new(mr_title, anything, anything) }
+
+ describe '#lint_subject' do
+ subject(:mr_linter) { described_class.new(merge_request) }
+
+ shared_examples 'a valid mr title' do
+ it 'does not have any problem' do
+ mr_linter.lint
+
+ expect(mr_linter.problems).to be_empty
+ end
+ end
+
+ context 'when subject valid' do
+ it_behaves_like 'a valid mr title'
+ end
+
+ context 'when it is too long' do
+ let(:mr_title) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH }
+
+ it 'adds a problem' do
+ expect(mr_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description)
+
+ mr_linter.lint
+ end
+ end
+
+ describe 'using magic mr run options' do
+ where(run_option: described_class.mr_run_options_regex.split('|') +
+ described_class.mr_run_options_regex.split('|').map! { |x| "[#{x}]" })
+
+ with_them do
+ let(:mr_title) { run_option + ' A B ' + 'C' * (described_class::MAX_LINE_LENGTH - 5) }
+
+ it_behaves_like 'a valid mr title'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 1a900dfba22..561e108bf31 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -165,6 +165,14 @@ RSpec.describe Gitlab::Danger::Roulette do
end
end
+ context 'when change contains many categories' do
+ let(:categories) { [:frontend, :test, :qa, :engineering_productivity, :ci_template, :backend] }
+
+ it 'has a deterministic sorting order' do
+ expect(spins.map(&:category)).to eq categories.sort
+ end
+ end
+
context 'when change contains QA category' do
let(:categories) { [:qa] }
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 4e0cc8a1fa9..e5dfff33a2a 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -21,9 +21,10 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
let(:data) { described_class.build(pipeline) }
let(:attributes) { data[:object_attributes] }
let(:build_data) { data[:builds].first }
+ let(:runner_data) { build_data[:runner] }
let(:project_data) { data[:project] }
- it 'has correct attributes' do
+ it 'has correct attributes', :aggregate_failures do
expect(attributes).to be_a(Hash)
expect(attributes[:ref]).to eq(pipeline.ref)
expect(attributes[:sha]).to eq(pipeline.sha)
@@ -36,6 +37,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
expect(build_data[:id]).to eq(build.id)
expect(build_data[:status]).to eq(build.status)
expect(build_data[:allow_failure]).to eq(build.allow_failure)
+ expect(runner_data).to eq(nil)
expect(project_data).to eq(project.hook_attrs(backward: false))
expect(data[:merge_request]).to be_nil
expect(data[:user]).to eq({
@@ -46,6 +48,18 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
})
end
+ context 'build with runner' do
+ let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
+ let(:ci_runner) { create(:ci_runner) }
+
+ it 'has runner attributes', :aggregate_failures do
+ expect(runner_data[:id]).to eq(ci_runner.id)
+ expect(runner_data[:description]).to eq(ci_runner.description)
+ expect(runner_data[:active]).to eq(ci_runner.active)
+ expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?)
+ end
+ end
+
context 'pipeline without variables' do
it 'has empty variables hash' do
expect(attributes[:variables]).to be_a(Array)
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index a1cc759e011..29688b18e94 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -130,6 +130,16 @@ RSpec.describe Gitlab::Database::BatchCount do
expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(5)
end
+ it 'stops counting when finish value is reached' do
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ expect(described_class.batch_count(model,
+ start: model.minimum(:id),
+ finish: model.maximum(:id) - 1, # Do not count the last record
+ batch_size: model.count - 2 # Ensure there are multiple batches
+ )).to eq(model.count - 1)
+ end
+
it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE}" do
min_id = model.minimum(:id)
relation = instance_double(ActiveRecord::Relation)
@@ -242,6 +252,19 @@ RSpec.describe Gitlab::Database::BatchCount do
expect(described_class.batch_distinct_count(model, column, start: model.minimum(column), finish: model.maximum(column))).to eq(2)
end
+ it 'stops counting when finish value is reached' do
+ # Create a new unique author that should not be counted
+ create(:issue)
+
+ stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ expect(described_class.batch_distinct_count(model, column,
+ start: User.minimum(:id),
+ finish: User.maximum(:id) - 1, # Do not count the newly created issue
+ batch_size: model.count - 2 # Ensure there are multiple batches
+ )).to eq(2)
+ end
+
it 'counts with User min and max as start and finish' do
expect(described_class.batch_distinct_count(model, column, start: User.minimum(:id), finish: User.maximum(:id))).to eq(2)
end
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index 48132d68031..3e8563376ce 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -189,7 +189,51 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
end
end
- context "when the model doesn't have an ID column" do
+ context 'when the model specifies a primary_column_name' do
+ let!(:id1) { create(:container_expiration_policy).id }
+ let!(:id2) { create(:container_expiration_policy).id }
+ let!(:id3) { create(:container_expiration_policy).id }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ ContainerExpirationPolicy.class_eval do
+ include EachBatch
+ end
+ end
+
+ it 'returns the final expected delay', :aggregate_failures do
+ Sidekiq::Testing.fake! do
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, batch_size: 2, primary_column_name: :project_id)
+
+ expect(final_delay.to_f).to eq(20.minutes.to_f)
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
+ end
+ end
+
+ context "when the primary_column_name is not an integer" do
+ it 'raises error' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :enabled)
+ end.to raise_error(StandardError, /is not an integer column/)
+ end
+ end
+
+ context "when the primary_column_name does not exist" do
+ it 'raises error' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :foo)
+ end.to raise_error(StandardError, /does not have an ID column of foo/)
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID or primary_column_name column" do
it 'raises error (for now)' do
expect do
model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
diff --git a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb
new file mode 100644
index 00000000000..934e2274358
--- /dev/null
+++ b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresHll::BatchDistinctCounter do
+ let_it_be(:error_rate) { described_class::ERROR_RATE } # HyperLogLog is a probabilistic algorithm, which provides estimated data, with given error margin
+ let_it_be(:fallback) { ::Gitlab::Database::BatchCounter::FALLBACK }
+ let_it_be(:small_batch_size) { calculate_batch_size(described_class::MIN_REQUIRED_BATCH_SIZE) }
+ let(:model) { Issue }
+ let(:column) { :author_id }
+
+ let(:in_transaction) { false }
+
+ let_it_be(:user) { create(:user, email: 'email1@domain.com') }
+ let_it_be(:another_user) { create(:user, email: 'email2@domain.com') }
+
+ def calculate_batch_size(batch_size)
+ zero_offset_modifier = -1
+
+ batch_size + zero_offset_modifier
+ end
+
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction)
+ end
+
+ context 'different distribution of relation records' do
+ [10, 100, 100_000].each do |spread|
+ context "records are spread within #{spread}" do
+ before do
+ ids = (1..spread).to_a.sample(10)
+ create_list(:issue, 10).each_with_index do |issue, i|
+ issue.id = ids[i]
+ end
+ end
+
+ it 'counts table' do
+ expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(10)
+ end
+ end
+ end
+ end
+
+ context 'unit test for different counting parameters' do
+ before_all do
+ create_list(:issue, 3, author: user)
+ create_list(:issue, 2, author: another_user)
+ end
+
+ describe '#estimate_distinct_count' do
+ it 'counts table' do
+ expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(5)
+ end
+
+ it 'counts with column field' do
+ expect(described_class.new(model, column).estimate_distinct_count).to be_within(error_rate).percent_of(2)
+ end
+
+ it 'counts with :id field' do
+ expect(described_class.new(model, :id).estimate_distinct_count).to be_within(error_rate).percent_of(5)
+ end
+
+ it 'counts with "id" field' do
+ expect(described_class.new(model, "id").estimate_distinct_count).to be_within(error_rate).percent_of(5)
+ end
+
+ it 'counts with table.column field' do
+ expect(described_class.new(model, "#{model.table_name}.#{column}").estimate_distinct_count).to be_within(error_rate).percent_of(2)
+ end
+
+ it 'counts with Arel column' do
+ expect(described_class.new(model, model.arel_table[column]).estimate_distinct_count).to be_within(error_rate).percent_of(2)
+ end
+
+ it 'counts over joined relations' do
+ expect(described_class.new(model.joins(:author), "users.email").estimate_distinct_count).to be_within(error_rate).percent_of(2)
+ end
+
+ it 'counts with :column field with batch_size of 50K' do
+ expect(described_class.new(model, column).estimate_distinct_count(batch_size: 50_000)).to be_within(error_rate).percent_of(2)
+ end
+
+ it 'will not count table with a batch size less than allowed' do
+ expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback)
+ end
+
+ it 'counts with different number of batches and aggregates total result' do
+ stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::MIN_REQUIRED_BATCH_SIZE', 0)
+
+ [1, 2, 4, 5, 6].each { |i| expect(described_class.new(model).estimate_distinct_count(batch_size: i)).to be_within(error_rate).percent_of(5) }
+ end
+
+ it 'counts with a start and finish' do
+ expect(described_class.new(model, column).estimate_distinct_count(start: model.minimum(:id), finish: model.maximum(:id))).to be_within(error_rate).percent_of(2)
+ end
+
+ it "defaults the batch size to #{Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE}" do
+ min_id = model.minimum(:id)
+ batch_end_id = min_id + calculate_batch_size(Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE)
+
+ expect(model).to receive(:where).with("id" => min_id..batch_end_id).and_call_original
+
+ described_class.new(model).estimate_distinct_count
+ end
+
+ context 'when a transaction is open' do
+ let(:in_transaction) { true }
+
+ it 'raises an error' do
+ expect { described_class.new(model, column).estimate_distinct_count }.to raise_error('BatchCount can not be run inside a transaction')
+ end
+ end
+
+ context 'disallowed configurations' do
+ let(:default_batch_size) { Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE }
+
+ it 'returns fallback if start is bigger than finish' do
+ expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: 0)).to eq(fallback)
+ end
+
+ it 'returns fallback if data volume exceeds upper limit' do
+ large_finish = Gitlab::Database::PostgresHll::BatchDistinctCounter::MAX_DATA_VOLUME + 1
+ expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: large_finish)).to eq(fallback)
+ end
+
+ it 'returns fallback if batch size is less than min required' do
+ expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb
new file mode 100644
index 00000000000..da4422bd442
--- /dev/null
+++ b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresIndexBloatEstimate do
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ ANALYZE schema_migrations
+ SQL
+ end
+
+ subject { described_class.find(identifier) }
+
+ let(:identifier) { 'public.schema_migrations_pkey' }
+
+ describe '#bloat_size' do
+ it 'returns the bloat size in bytes' do
+ # We cannot reach much more about the bloat size estimate here
+ expect(subject.bloat_size).to be >= 0
+ end
+ end
+
+ describe '#bloat_size_bytes' do
+ it 'is an alias of #bloat_size' do
+ expect(subject.bloat_size_bytes).to eq(subject.bloat_size)
+ end
+ end
+
+ describe '#index' do
+ it 'belongs to a PostgresIndex' do
+ expect(subject.index.identifier).to eq(identifier)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index d65b638f7bc..2fda9b85c5a 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
expect(described_class.regular).to all(have_attributes(unique: false))
end
- it 'only non partitioned indexes ' do
+ it 'only non partitioned indexes' do
expect(described_class.regular).to all(have_attributes(partitioned: false))
end
@@ -46,9 +46,24 @@ RSpec.describe Gitlab::Database::PostgresIndex do
end
end
- describe '.random_few' do
- it 'limits to two records by default' do
- expect(described_class.random_few(2).size).to eq(2)
+ describe '#bloat_size' do
+ subject { build(:postgres_index, bloat_estimate: bloat_estimate) }
+
+ let(:bloat_estimate) { build(:postgres_index_bloat_estimate) }
+ let(:bloat_size) { double }
+
+ it 'returns the bloat size from the estimate' do
+ expect(bloat_estimate).to receive(:bloat_size).and_return(bloat_size)
+
+ expect(subject.bloat_size).to eq(bloat_size)
+ end
+
+ context 'without a bloat estimate available' do
+ let(:bloat_estimate) { nil }
+
+ it 'returns 0' do
+ expect(subject.bloat_size).to eq(0)
+ end
end
end
diff --git a/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb
new file mode 100644
index 00000000000..6e1e53e0e41
--- /dev/null
+++ b/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresqlAdapter::EmptyQueryPing do
+ describe '#active?' do
+ let(:adapter_class) do
+ Class.new do
+ include Gitlab::Database::PostgresqlAdapter::EmptyQueryPing
+
+ def initialize(connection, lock)
+ @connection = connection
+ @lock = lock
+ end
+ end
+ end
+
+ subject { adapter_class.new(connection, lock).active? }
+
+ let(:connection) { double(query: nil) }
+ let(:lock) { double }
+
+ before do
+ allow(lock).to receive(:synchronize).and_yield
+ end
+
+ it 'uses an empty query to check liveness' do
+ expect(connection).to receive(:query).with(';')
+
+ subject
+ end
+
+ it 'returns true if no error was signaled' do
+ expect(subject).to be_truthy
+ end
+
+ it 'returns false when an error occurs' do
+ expect(lock).to receive(:synchronize).and_raise(PG::Error)
+
+ expect(subject).to be_falsey
+ 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
index 2d6765aac2e..51fc7c6620b 100644
--- a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
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(:index) { instance_double(Gitlab::Database::PostgresIndex, indexrelid: 42, name: index_name, schema: 'public', tablename: table_name, partitioned?: false, unique?: false, exclusion?: false, expression?: 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 }
@@ -130,6 +130,36 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
check_index_exists
end
+ context 'for expression indexes' do
+ before do
+ allow(index).to receive(:expression?).and_return(true)
+ end
+
+ it 'rebuilds table statistics before dropping the original index' do
+ expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice
+
+ expect_to_execute_concurrently_in_order(create_index)
+
+ expect_to_execute_concurrently_in_order(<<~SQL)
+ ANALYZE "#{index.schema}"."#{index.tablename}"
+ SQL
+
+ 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 a dangling index is left from a previous run' do
before do
connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
diff --git a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb
new file mode 100644
index 00000000000..a5e2f368f40
--- /dev/null
+++ b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reindexing::IndexSelection do
+ include DatabaseHelpers
+
+ subject { described_class.new(Gitlab::Database::PostgresIndex.all).to_a }
+
+ before do
+ swapout_view_for_table(:postgres_index_bloat_estimates)
+ swapout_view_for_table(:postgres_indexes)
+ end
+
+ def execute(sql)
+ ActiveRecord::Base.connection.execute(sql)
+ end
+
+ it 'orders by highest bloat first' do
+ create_list(:postgres_index, 10).each_with_index do |index, i|
+ create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i)
+ end
+
+ expected = Gitlab::Database::PostgresIndexBloatEstimate.order(bloat_size_bytes: :desc).map(&:index)
+
+ expect(subject).to eq(expected)
+ end
+
+ context 'with time frozen' do
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ it 'does not return indexes with reindex action in the last 7 days' do
+ not_recently_reindexed = create_list(:postgres_index, 2).each_with_index do |index, i|
+ create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i)
+ create(:reindex_action, index: index, action_end: Time.zone.now - 7.days - 1.minute)
+ end
+
+ create_list(:postgres_index, 2).each_with_index do |index, i|
+ create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i)
+ create(:reindex_action, index: index, action_end: Time.zone.now)
+ end
+
+ expected = Gitlab::Database::PostgresIndexBloatEstimate.where(identifier: not_recently_reindexed.map(&:identifier)).map(&:index).map(&:identifier).sort
+
+ expect(subject.map(&:identifier).sort).to eq(expected)
+ 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
index efb5b8463a1..225f23d2135 100644
--- a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
@@ -3,7 +3,7 @@
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(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil, bloat_size: 42) }
let(:size_after) { 512 }
it 'yields to the caller' do
@@ -47,6 +47,12 @@ RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do
expect(find_record.ondisk_size_bytes_end).to eq(size_after)
end
+ it 'creates the record with the indexes bloat estimate' do
+ described_class.keep_track_of(index) do
+ expect(find_record.bloat_estimate_bytes_start).to eq(index.bloat_size)
+ end
+ end
+
context 'in case of errors' do
it 'sets the state to failed' do
expect do
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 359e0597f4e..eb78a5fe8ea 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -6,12 +6,16 @@ RSpec.describe Gitlab::Database::Reindexing do
include ExclusiveLeaseHelpers
describe '.perform' do
- subject { described_class.perform(indexes) }
+ subject { described_class.perform(candidate_indexes) }
let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
+ let(:index_selection) { instance_double(Gitlab::Database::Reindexing::IndexSelection) }
+ let(:candidate_indexes) { double }
let(:indexes) { double }
it 'delegates to Coordinator' do
+ expect(Gitlab::Database::Reindexing::IndexSelection).to receive(:new).with(candidate_indexes).and_return(index_selection)
+ expect(index_selection).to receive(:take).with(2).and_return(indexes)
expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator)
expect(coordinator).to receive(:perform)
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index ca9f9ab915f..4048fc69591 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -118,8 +118,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService
expect(result[:status]).to eq(:success)
expect(project.name).to eq(described_class::PROJECT_NAME)
expect(project.description).to eq(
- 'This project is automatically generated and will be used to help monitor this GitLab instance. ' \
- "[More information](#{docs_path})"
+ 'This project is automatically generated and helps monitor this GitLab instance. ' \
+ "[Learn more](#{docs_path})."
)
expect(File).to exist("doc/#{path}.md")
end
diff --git a/spec/lib/gitlab/deploy_key_access_spec.rb b/spec/lib/gitlab/deploy_key_access_spec.rb
new file mode 100644
index 00000000000..e186e993d8f
--- /dev/null
+++ b/spec/lib/gitlab/deploy_key_access_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DeployKeyAccess do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:deploy_key) { create(:deploy_key, user: user) }
+ let(:project) { create(:project, :repository) }
+ let(:protected_branch) { create(:protected_branch, :no_one_can_push, project: project) }
+
+ subject(:access) { described_class.new(deploy_key, container: project) }
+
+ before do
+ project.add_guest(user)
+ create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key)
+ end
+
+ describe '#can_create_tag?' do
+ context 'push tag that matches a protected tag pattern via a deploy key' do
+ it 'still pushes that tag' do
+ create(:protected_tag, project: project, name: 'v*')
+
+ expect(access.can_create_tag?('v0.1.2')).to be_truthy
+ end
+ end
+ end
+
+ describe '#can_push_for_ref?' do
+ context 'push to a protected branch of this project via a deploy key' do
+ before do
+ create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key)
+ end
+
+ context 'when the project has active deploy key owned by this user' do
+ it 'returns true' do
+ expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy
+ end
+ end
+
+ context 'when the project has active deploy keys, but not by this user' do
+ let(:deploy_key) { create(:deploy_key, user: create(:user)) }
+
+ it 'returns false' do
+ expect(access.can_push_for_ref?(protected_branch.name)).to be_falsey
+ end
+ end
+
+ context 'when there is another branch no one can push to' do
+ let(:another_branch) { create(:protected_branch, :no_one_can_push, name: 'another_branch', project: project) }
+
+ it 'returns false when trying to push to that other branch' do
+ expect(access.can_push_for_ref?(another_branch.name)).to be_falsey
+ end
+
+ context 'and the deploy key added for the first protected branch is also added for this other branch' do
+ it 'returns true for both protected branches' do
+ create(:protected_branch_push_access_level, protected_branch: another_branch, deploy_key: deploy_key)
+
+ expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy
+ expect(access.can_push_for_ref?(another_branch.name)).to be_truthy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_collection/commit_spec.rb b/spec/lib/gitlab/diff/file_collection/commit_spec.rb
index 7773604a638..3d995b36b6f 100644
--- a/spec/lib/gitlab/diff/file_collection/commit_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/commit_spec.rb
@@ -4,17 +4,75 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::FileCollection::Commit do
let(:project) { create(:project, :repository) }
+ let(:diffable) { project.commit }
- it_behaves_like 'diff statistics' do
- let(:collection_default_args) do
- { diff_options: {} }
- end
+ let(:collection_default_args) do
+ { diff_options: {} }
+ end
- let(:diffable) { project.commit }
+ it_behaves_like 'diff statistics' do
let(:stub_path) { 'bar/branch-test.txt' }
end
- it_behaves_like 'unfoldable diff' do
- let(:diffable) { project.commit }
+ it_behaves_like 'unfoldable diff'
+
+ it_behaves_like 'sortable diff files' do
+ let(:diffable) { project.commit('913c66a') }
+
+ let(:unsorted_diff_files_paths) do
+ [
+ '.DS_Store',
+ 'CHANGELOG',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'VERSION',
+ 'encoding/feature-1.txt',
+ 'encoding/feature-2.txt',
+ 'encoding/hotfix-1.txt',
+ 'encoding/hotfix-2.txt',
+ 'encoding/russian.rb',
+ 'encoding/test.txt',
+ 'encoding/テスト.txt',
+ 'encoding/テスト.xls',
+ 'files/.DS_Store',
+ 'files/html/500.html',
+ 'files/images/logo-black.png',
+ 'files/images/logo-white.png',
+ 'files/js/application.js',
+ 'files/js/commit.js.coffee',
+ 'files/markdown/ruby-style-guide.md',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb'
+ ]
+ end
+
+ let(:sorted_diff_files_paths) do
+ [
+ 'encoding/feature-1.txt',
+ 'encoding/feature-2.txt',
+ 'encoding/hotfix-1.txt',
+ 'encoding/hotfix-2.txt',
+ 'encoding/russian.rb',
+ 'encoding/test.txt',
+ 'encoding/テスト.txt',
+ 'encoding/テスト.xls',
+ 'files/html/500.html',
+ 'files/images/logo-black.png',
+ 'files/images/logo-white.png',
+ 'files/js/application.js',
+ 'files/js/commit.js.coffee',
+ 'files/markdown/ruby-style-guide.md',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb',
+ 'files/.DS_Store',
+ '.DS_Store',
+ 'CHANGELOG',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'VERSION'
+ ]
+ end
end
end
diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
index dda4513a3a1..f3326f4f03d 100644
--- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
@@ -27,4 +27,43 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
let(:diffable) { Compare.new(raw_compare, project) }
let(:stub_path) { '.gitignore' }
end
+
+ it_behaves_like 'sortable diff files' do
+ let(:diffable) { Compare.new(raw_compare, project) }
+ let(:collection_default_args) do
+ {
+ project: diffable.project,
+ diff_options: {},
+ diff_refs: diffable.diff_refs
+ }
+ end
+
+ let(:unsorted_diff_files_paths) do
+ [
+ '.DS_Store',
+ '.gitignore',
+ '.gitmodules',
+ 'Gemfile.zip',
+ 'files/.DS_Store',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb',
+ 'gitlab-shell'
+ ]
+ end
+
+ let(:sorted_diff_files_paths) do
+ [
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb',
+ 'files/.DS_Store',
+ '.DS_Store',
+ '.gitignore',
+ '.gitmodules',
+ 'Gemfile.zip',
+ 'gitlab-shell'
+ ]
+ end
+ end
end
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 72a66b0451e..670c734ce08 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
@@ -18,6 +18,10 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
let(:diff_files) { subject.diff_files }
+ before do
+ stub_feature_flags(diffs_gradual_load: false)
+ end
+
describe 'initialize' do
it 'memoizes pagination_data' do
expect(subject.pagination_data).to eq(current_page: 1, next_page: 2, total_pages: 2)
@@ -97,6 +101,18 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
expect(collection.diff_files.map(&:new_path)).to eq(expected_batch_files)
end
end
+
+ context 'with diffs gradual load feature flag enabled' do
+ let(:batch_page) { 0 }
+
+ before do
+ stub_feature_flags(diffs_gradual_load: true)
+ end
+
+ it 'returns correct diff files' do
+ expect(subject.diffs.map(&:new_path)).to eq(diff_files_relation.page(1).per(batch_size).map(&:new_path))
+ end
+ end
end
it_behaves_like 'unfoldable diff' do
@@ -114,6 +130,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
end
let(:diffable) { merge_request.merge_request_diff }
+ let(:batch_page) { 2 }
let(:stub_path) { '.gitignore' }
subject do
@@ -127,4 +144,18 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
it_behaves_like 'cacheable diff collection' do
let(:cacheable_files_count) { batch_size }
end
+
+ it_behaves_like 'unsortable diff files' do
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:collection_default_args) do
+ { diff_options: {} }
+ end
+
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ batch_page,
+ batch_size,
+ **collection_default_args)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 429e552278d..03a9b9bd21e 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -54,4 +54,11 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiff do
it 'returns a valid instance of a DiffCollection' do
expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
end
+
+ it_behaves_like 'unsortable diff files' do
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:collection_default_args) do
+ { diff_options: {} }
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
new file mode 100644
index 00000000000..8822fc55c6e
--- /dev/null
+++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Diff::FileCollectionSorter do
+ let(:diffs) do
+ [
+ double(new_path: '.dir/test', old_path: '.dir/test'),
+ double(new_path: '', old_path: '.file'),
+ double(new_path: '1-folder/A-file.ext', old_path: '1-folder/A-file.ext'),
+ double(new_path: nil, old_path: '1-folder/M-file.ext'),
+ double(new_path: '1-folder/Z-file.ext', old_path: '1-folder/Z-file.ext'),
+ double(new_path: '', old_path: '1-folder/nested/A-file.ext'),
+ double(new_path: '1-folder/nested/M-file.ext', old_path: '1-folder/nested/M-file.ext'),
+ double(new_path: nil, old_path: '1-folder/nested/Z-file.ext'),
+ double(new_path: '2-folder/A-file.ext', old_path: '2-folder/A-file.ext'),
+ double(new_path: '', old_path: '2-folder/M-file.ext'),
+ double(new_path: '2-folder/Z-file.ext', old_path: '2-folder/Z-file.ext'),
+ double(new_path: nil, old_path: '2-folder/nested/A-file.ext'),
+ double(new_path: 'A-file.ext', old_path: 'A-file.ext'),
+ double(new_path: '', old_path: 'M-file.ext'),
+ double(new_path: 'Z-file.ext', old_path: 'Z-file.ext')
+ ]
+ end
+
+ subject { described_class.new(diffs) }
+
+ describe '#sort' do
+ let(:sorted_files_paths) { subject.sort.map { |file| file.new_path.presence || file.old_path } }
+
+ it 'returns list sorted directory first' do
+ expect(sorted_files_paths).to eq([
+ '.dir/test',
+ '1-folder/nested/A-file.ext',
+ '1-folder/nested/M-file.ext',
+ '1-folder/nested/Z-file.ext',
+ '1-folder/A-file.ext',
+ '1-folder/M-file.ext',
+ '1-folder/Z-file.ext',
+ '2-folder/nested/A-file.ext',
+ '2-folder/A-file.ext',
+ '2-folder/M-file.ext',
+ '2-folder/Z-file.ext',
+ '.file',
+ 'A-file.ext',
+ 'M-file.ext',
+ 'Z-file.ext'
+ ])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
index b891f9e8285..4163c0eced5 100644
--- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb
+++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
@@ -188,7 +188,7 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do
let(:old_blob) { Blob.decorate(Gitlab::Git::Blob.new(data: raw_old_blob, size: 10)) }
let(:diff) do
- Gitlab::Git::Diff.new(diff: raw_diff,
+ Gitlab::Git::Diff.new({ diff: raw_diff,
new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
a_mode: "100644",
@@ -196,7 +196,7 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do
new_file: false,
renamed_file: false,
deleted_file: false,
- too_large: false)
+ too_large: false })
end
let(:diff_file) do
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index 2ebfb054a96..32b451f8329 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -254,7 +254,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
new_issue = Issue.last
- expect(new_issue.service_desk_reply_to).to eq('finn@adventuretime.ooo')
+ expect(new_issue.external_author).to eq('finn@adventuretime.ooo')
end
end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 575ff7f357b..bc4c6cf007d 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -6,7 +6,7 @@ require "spec_helper"
RSpec.describe Gitlab::Email::ReplyParser do
describe '#execute' do
def test_parse_body(mail_string, params = {})
- described_class.new(Mail::Message.new(mail_string), params).execute
+ described_class.new(Mail::Message.new(mail_string), **params).execute
end
it "returns an empty string if the message is blank" do
diff --git a/spec/lib/gitlab/email/smime/certificate_spec.rb b/spec/lib/gitlab/email/smime/certificate_spec.rb
index e4a085d971b..f7bb933e348 100644
--- a/spec/lib/gitlab/email/smime/certificate_spec.rb
+++ b/spec/lib/gitlab/email/smime/certificate_spec.rb
@@ -69,8 +69,8 @@ RSpec.describe Gitlab::Email::Smime::Certificate do
describe '.from_files' do
it 'parses correctly a certificate and key' do
- allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s)
- allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem)
+ stub_file_read('a_key', content: @cert[:key].to_s)
+ stub_file_read('a_cert', content: @cert[:cert].to_pem)
parsed_cert = described_class.from_files('a_key', 'a_cert')
@@ -79,9 +79,9 @@ RSpec.describe Gitlab::Email::Smime::Certificate do
context 'with optional ca_certs' do
it 'parses correctly certificate, key and ca_certs' do
- allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s)
- allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem)
- allow(File).to receive(:read).with('a_ca_cert').and_return(@intermediate_ca[:cert].to_pem)
+ stub_file_read('a_key', content: @cert[:key].to_s)
+ stub_file_read('a_cert', content: @cert[:cert].to_pem)
+ stub_file_read('a_ca_cert', content: @intermediate_ca[:cert].to_pem)
parsed_cert = described_class.from_files('a_key', 'a_cert', 'a_ca_cert')
@@ -94,8 +94,8 @@ RSpec.describe Gitlab::Email::Smime::Certificate do
it 'parses correctly a certificate and key' do
cert = generate_cert(signer_ca: @root_ca)
- allow(File).to receive(:read).with('a_key').and_return(cert[:key].to_s)
- allow(File).to receive(:read).with('a_cert').and_return(cert[:cert].to_pem)
+ stub_file_read('a_key', content: cert[:key].to_s)
+ stub_file_read('a_cert', content: cert[:cert].to_pem)
parsed_cert = described_class.from_files('a_key', 'a_cert')
diff --git a/spec/lib/gitlab/encrypted_configuration_spec.rb b/spec/lib/gitlab/encrypted_configuration_spec.rb
new file mode 100644
index 00000000000..eadc2cf71a7
--- /dev/null
+++ b/spec/lib/gitlab/encrypted_configuration_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::EncryptedConfiguration do
+ subject(:configuration) { described_class.new }
+
+ let!(:config_tmp_dir) { Dir.mktmpdir('config-') }
+
+ after do
+ FileUtils.rm_f(config_tmp_dir)
+ end
+
+ describe '#initialize' do
+ it 'accepts all args as optional fields' do
+ expect { configuration }.not_to raise_exception
+
+ expect(configuration.key).to be_nil
+ expect(configuration.previous_keys).to be_empty
+ end
+
+ it 'generates 32 byte key when provided a larger base key' do
+ configuration = described_class.new(base_key: 'A' * 64)
+
+ expect(configuration.key.bytesize).to eq 32
+ end
+
+ it 'generates 32 byte key when provided a smaller base key' do
+ configuration = described_class.new(base_key: 'A' * 16)
+
+ expect(configuration.key.bytesize).to eq 32
+ end
+
+ it 'throws an error when the base key is too small' do
+ expect { described_class.new(base_key: 'A' * 12) }.to raise_error 'Base key too small'
+ end
+ end
+
+ context 'when provided a config file but no key' do
+ let(:config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') }
+
+ it 'throws an error when writing without a key' do
+ expect { described_class.new(content_path: config_path).write('test') }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError
+ end
+
+ it 'throws an error when reading without a key' do
+ config = described_class.new(content_path: config_path)
+ File.write(config_path, 'test')
+ expect { config.read }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError
+ end
+ end
+
+ context 'when provided key and config file' do
+ let(:credentials_config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') }
+ let(:credentials_key) { SecureRandom.hex(64) }
+
+ describe '#write' do
+ it 'encrypts the file using the provided key' do
+ encryptor = ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(credentials_key), cipher: 'aes-256-gcm')
+ config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
+
+ config.write('sample-content')
+ expect(encryptor.decrypt_and_verify(File.read(credentials_config_path))).to eq('sample-content')
+ end
+ end
+
+ describe '#read' do
+ it 'reads yaml configuration' do
+ config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
+
+ config.write({ foo: { bar: true } }.to_yaml)
+ expect(config[:foo][:bar]).to be true
+ end
+
+ it 'allows referencing top level keys via dot syntax' do
+ config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
+
+ config.write({ foo: { bar: true } }.to_yaml)
+ expect(config.foo[:bar]).to be true
+ end
+
+ it 'throws a custom error when referencing an invalid key map config' do
+ config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
+
+ config.write("stringcontent")
+ expect { config[:foo] }.to raise_error Gitlab::EncryptedConfiguration::InvalidConfigError
+ end
+ end
+
+ describe '#change' do
+ it 'changes yaml configuration' do
+ config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
+
+ config.write({ foo: { bar: true } }.to_yaml)
+ config.change do |unencrypted_contents|
+ contents = YAML.safe_load(unencrypted_contents, permitted_classes: [Symbol])
+ contents.merge(beef: "stew").to_yaml
+ end
+ expect(config.foo[:bar]).to be true
+ expect(config.beef).to eq('stew')
+ end
+ end
+
+ context 'when provided previous_keys for rotation' do
+ let(:credential_key_original) { SecureRandom.hex(64) }
+ let(:credential_key_latest) { SecureRandom.hex(64) }
+ let(:config_path_original) { File.join(config_tmp_dir, 'credentials-orig.yml.enc') }
+ let(:config_path_latest) { File.join(config_tmp_dir, 'credentials-latest.yml.enc') }
+
+ def encryptor(key)
+ ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(key), cipher: 'aes-256-gcm')
+ end
+
+ describe '#write' do
+ it 'rotates the key when provided a new key' do
+ config1 = described_class.new(content_path: config_path_original, base_key: credential_key_original)
+ config1.write('sample-content1')
+
+ config2 = described_class.new(content_path: config_path_latest, base_key: credential_key_latest, previous_keys: [credential_key_original])
+ config2.write('sample-content2')
+
+ original_key_encryptor = encryptor(credential_key_original) # can read with the initial key
+ latest_key_encryptor = encryptor(credential_key_latest) # can read with the new key
+ both_key_encryptor = encryptor(credential_key_latest) # can read with either key
+ both_key_encryptor.rotate(Gitlab::EncryptedConfiguration.generate_key(credential_key_original))
+
+ expect(original_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1')
+ expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1')
+ expect(latest_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2')
+ expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2')
+ expect { original_key_encryptor.decrypt_and_verify(File.read(config_path_latest)) }.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage)
+ end
+ end
+
+ describe '#read' do
+ it 'supports reading using rotated config' do
+ described_class.new(content_path: config_path_original, base_key: credential_key_original).write({ foo: { bar: true } }.to_yaml)
+
+ config = described_class.new(content_path: config_path_original, base_key: credential_key_latest, previous_keys: [credential_key_original])
+ expect(config[:foo][:bar]).to be true
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
index 2fe3d36daf7..03cb89ee033 100644
--- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -6,12 +6,10 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
backwards_compatible_test_experiment: {
- environment: environment,
tracking_category: 'Team',
use_backwards_compatible_subject_index: true
},
test_experiment: {
- environment: environment,
tracking_category: 'Team'
}
}
@@ -21,7 +19,6 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage)
end
- let(:environment) { Rails.env.test? }
let(:enabled_percentage) { 10 }
controller(ApplicationController) do
@@ -78,29 +75,24 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
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)
+ stub_experiment_for_subject(my_experiment: true)
allow(controller).to receive(:gon).and_return(gon)
- expect(gon).to receive(:push).with(experiments, true)
+ expect(gon).to receive(:push).with({ experiments: { 'myExperiment' => true } }, true)
controller.push_frontend_experiment(:my_experiment)
end
end
describe '#experiment_enabled?' do
- def check_experiment(exp_key = :test_experiment)
- controller.experiment_enabled?(exp_key)
+ def check_experiment(exp_key = :test_experiment, subject = nil)
+ controller.experiment_enabled?(exp_key, subject: subject)
end
subject { check_experiment }
context 'cookie is not present' do
- 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)
- check_experiment
- end
+ it { is_expected.to eq(false) }
end
context 'cookie is present' do
@@ -112,37 +104,56 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
where(:experiment_key, :index_value) do
- :test_experiment | 40 # Zlib.crc32('test_experimentabcd-1234') % 100 = 40
- :backwards_compatible_test_experiment | 76 # 'abcd1234'.hex % 100 = 76
+ :test_experiment | 'abcd-1234'
+ :backwards_compatible_test_experiment | 'abcd1234'
end
with_them do
- it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do
- expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, index_value)
+ it 'calls Gitlab::Experimentation.in_experiment_group?? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do
+ expect(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, subject: index_value)
+
check_experiment(experiment_key)
end
end
- end
- it 'returns true when DNT: 0 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
- controller.request.headers['DNT'] = '0'
+ context 'when subject is given' do
+ let(:user) { build(:user) }
+
+ it 'uses the subject' do
+ expect(Gitlab::Experimentation).to receive(:in_experiment_group?).with(:test_experiment, subject: user)
- is_expected.to be_truthy
+ check_experiment(:test_experiment, user)
+ end
+ end
end
- it 'returns false when DNT: 1 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
- controller.request.headers['DNT'] = '1'
+ context 'do not track' do
+ before do
+ allow(Gitlab::Experimentation).to receive(:in_experiment_group?) { true }
+ end
+
+ context 'when do not track is disabled' do
+ before do
+ controller.request.headers['DNT'] = '0'
+ end
+
+ it { is_expected.to eq(true) }
+ end
- is_expected.to be_falsy
+ context 'when do not track is enabled' do
+ before do
+ controller.request.headers['DNT'] = '1'
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
- describe 'URL parameter to force enable experiment' do
+ context 'URL parameter to force enable experiment' do
it 'returns true unconditionally' do
get :index, params: { force_experiment: :test_experiment }
- is_expected.to be_truthy
+ is_expected.to eq(true)
end
end
end
@@ -155,7 +166,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'the user is part of the experimental group' do
before do
- stub_experiment_for_user(test_experiment: true)
+ stub_experiment_for_subject(test_experiment: true)
end
it 'tracks the event with the right parameters' do
@@ -172,7 +183,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'the user is part of the control group' do
before do
- stub_experiment_for_user(test_experiment: false)
+ stub_experiment_for_subject(test_experiment: false)
end
it 'tracks the event with the right parameters' do
@@ -215,6 +226,59 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
expect_no_snowplow_event
end
end
+
+ context 'subject is provided' do
+ before do
+ stub_experiment_for_subject(test_experiment: false)
+ end
+
+ it "provides the subject's hashed global_id as label" do
+ experiment_subject = double(:subject, to_global_id: 'abc')
+
+ controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1,
+ label: Digest::MD5.hexdigest('abc')
+ )
+ end
+
+ it "provides the subject's hashed string representation as label" do
+ experiment_subject = 'somestring'
+
+ controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1,
+ label: Digest::MD5.hexdigest('somestring')
+ )
+ end
+ end
+
+ context 'no subject is provided but cookie is set' do
+ before do
+ get :index
+ stub_experiment_for_subject(test_experiment: false)
+ end
+
+ it 'uses the experimentation_subject_id as fallback' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1,
+ label: cookies.permanent.signed[:experimentation_subject_id]
+ )
+ end
+ end
end
context 'when the experiment is disabled' do
@@ -238,7 +302,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'the user is part of the experimental group' do
before do
- stub_experiment_for_user(test_experiment: true)
+ stub_experiment_for_subject(test_experiment: true)
end
it 'pushes the right parameters to gon' do
@@ -256,9 +320,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'the user is part of the control group' do
before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
- end
+ stub_experiment_for_subject(test_experiment: false)
end
it 'pushes the right parameters to gon' do
@@ -311,7 +373,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
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
+ expect(Gon.method_defined?(:tracking_data)).to eq(false)
end
end
end
@@ -322,7 +384,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
it 'does not push data to gon' do
- expect(Gon.method_defined?(:tracking_data)).to be_falsey
+ expect(Gon.method_defined?(:tracking_data)).to eq(false)
controller.track_experiment_event(:test_experiment, 'start')
end
end
@@ -330,6 +392,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
describe '#record_experiment_user' do
let(:user) { build(:user) }
+ let(:context) { { a: 42 } }
context 'when the experiment is enabled' do
before do
@@ -339,27 +402,25 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
context 'the user is part of the experimental group' do
before do
- stub_experiment_for_user(test_experiment: true)
+ stub_experiment_for_subject(test_experiment: true)
end
it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user)
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
context 'the user is part of the control group' do
before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
- end
+ stub_experiment_for_subject(test_experiment: false)
end
it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user, context)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
end
@@ -373,7 +434,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it 'does not call add_user on the Experiment model' do
expect(::Experiment).not_to receive(:add_user)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
@@ -385,27 +446,26 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it 'does not call add_user on the Experiment model' do
expect(::Experiment).not_to receive(:add_user)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
context 'do not track' do
before do
+ stub_experiment(test_experiment: true)
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'
+ stub_experiment_for_subject(test_experiment: false)
end
it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user, context)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
@@ -417,12 +477,62 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it 'does not call add_user on the Experiment model' do
expect(::Experiment).not_to receive(:add_user)
- controller.record_experiment_user(:test_experiment)
+ controller.record_experiment_user(:test_experiment, context)
end
end
end
end
+ describe '#record_experiment_conversion_event' do
+ let(:user) { build(:user) }
+
+ before do
+ allow(controller).to receive(:dnt_enabled?).and_return(false)
+ allow(controller).to receive(:current_user).and_return(user)
+ stub_experiment(test_experiment: true)
+ end
+
+ subject(:record_conversion_event) do
+ controller.record_experiment_conversion_event(:test_experiment)
+ end
+
+ it 'records the conversion event for the experiment & user' do
+ expect(::Experiment).to receive(:record_conversion_event).with(:test_experiment, user)
+ record_conversion_event
+ end
+
+ shared_examples 'does not record the conversion event' do
+ it 'does not record the conversion event' do
+ expect(::Experiment).not_to receive(:record_conversion_event)
+ record_conversion_event
+ end
+ end
+
+ context 'when DNT is enabled' do
+ before do
+ allow(controller).to receive(:dnt_enabled?).and_return(true)
+ end
+
+ include_examples 'does not record the conversion event'
+ end
+
+ context 'when there is no current user' do
+ before do
+ allow(controller).to receive(:current_user).and_return(nil)
+ end
+
+ include_examples 'does not record the conversion event'
+ end
+
+ context 'when the experiment is not enabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ end
+
+ include_examples 'does not record the conversion event'
+ end
+ end
+
describe '#experiment_tracking_category_and_group' do
let_it_be(:experiment_key) { :test_something }
@@ -430,7 +540,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it 'returns a string with the experiment tracking category & group joined with a ":"' do
expect(controller).to receive(:tracking_category).with(experiment_key).and_return('Experiment::Category')
- expect(controller).to receive(:tracking_group).with(experiment_key, '_group').and_return('experimental_group')
+ expect(controller).to receive(:tracking_group).with(experiment_key, '_group', subject: nil).and_return('experimental_group')
expect(subject).to eq('Experiment::Category:experimental_group')
end
diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb
new file mode 100644
index 00000000000..7b1d1763010
--- /dev/null
+++ b/spec/lib/gitlab/experimentation/experiment_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Experimentation::Experiment do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:percentage) { 50 }
+ let(:params) do
+ {
+ tracking_category: 'Category1',
+ use_backwards_compatible_subject_index: true
+ }
+ end
+
+ before do
+ feature = double('FeatureFlag', percentage_of_time_value: percentage )
+ expect(Feature).to receive(:get).with(:experiment_key_experiment_percentage).and_return(feature)
+ end
+
+ subject(:experiment) { described_class.new(:experiment_key, **params) }
+
+ describe '#active?' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(on_gitlab_com)
+ end
+
+ subject { experiment.active? }
+
+ where(:on_gitlab_com, :percentage, :is_active) do
+ true | 0 | false
+ true | 10 | true
+ false | 0 | false
+ false | 10 | false
+ end
+
+ with_them do
+ it { is_expected.to eq(is_active) }
+ end
+ end
+
+ describe '#enabled_for_index?' do
+ subject { experiment.enabled_for_index?(index) }
+
+ where(:index, :percentage, :is_enabled) do
+ 50 | 40 | false
+ 40 | 50 | true
+ nil | 50 | false
+ end
+
+ with_them do
+ it { is_expected.to eq(is_enabled) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index ebf98a0151f..a68c050d829 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -13,11 +13,8 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
:invite_members_version_a,
:invite_members_version_b,
:invite_members_empty_group_version_a,
- :new_create_project_ui,
:contact_sales_btn_in_app,
:customize_homepage,
- :invite_email,
- :invitation_reminders,
:group_only_trials,
:default_to_issues_board
]
@@ -29,127 +26,150 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
end
end
-RSpec.describe Gitlab::Experimentation, :snowplow do
+RSpec.describe Gitlab::Experimentation do
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
backwards_compatible_test_experiment: {
- environment: environment,
tracking_category: 'Team',
use_backwards_compatible_subject_index: true
},
test_experiment: {
- environment: environment,
tracking_category: 'Team'
}
})
Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage)
Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage)
+ allow(Gitlab).to receive(:com?).and_return(true)
end
- let(:environment) { Rails.env.test? }
let(:enabled_percentage) { 10 }
- describe '.enabled?' do
- subject { described_class.enabled?(:test_experiment) }
+ describe '.get_experiment' do
+ subject { described_class.get_experiment(:test_experiment) }
- context 'feature toggle is enabled, we are on the right environment and we are selected' do
- it { is_expected.to be_truthy }
+ context 'returns experiment' do
+ it { is_expected.to be_instance_of(Gitlab::Experimentation::Experiment) }
+ end
+
+ context 'experiment is not defined' do
+ subject { described_class.get_experiment(:missing_experiment) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.active?' do
+ subject { described_class.active?(:test_experiment) }
+
+ context 'feature toggle is enabled' do
+ it { is_expected.to eq(true) }
end
describe 'experiment is not defined' do
it 'returns false' do
- expect(described_class.enabled?(:missing_experiment)).to be_falsey
+ expect(described_class.active?(:missing_experiment)).to eq(false)
end
end
describe 'experiment is disabled' do
let(:enabled_percentage) { 0 }
- it { is_expected.to be_falsey }
+ it { is_expected.to eq(false) }
end
+ end
- describe 'we are on the wrong environment' do
- let(:environment) { ::Gitlab.com? }
+ describe '.in_experiment_group?' do
+ context 'with new index calculation' do
+ let(:enabled_percentage) { 50 }
+ let(:experiment_subject) { 'z' } # Zlib.crc32('test_experimentz') % 100 = 33
- it { is_expected.to be_falsey }
+ subject { described_class.in_experiment_group?(:test_experiment, subject: experiment_subject) }
- it 'ensures the typically less expensive environment is checked before the more expensive call to database for Feature' do
- expect_next_instance_of(described_class::Experiment) do |experiment|
- expect(experiment).not_to receive(:enabled?)
+ context 'when experiment is active' do
+ context 'when subject is part of the experiment' do
+ it { is_expected.to eq(true) }
end
- subject
- end
- end
- end
+ context 'when subject is not part of the experiment' do
+ let(:experiment_subject) { 'a' } # Zlib.crc32('test_experimenta') % 100 = 61
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when subject has a global_id' do
+ let(:experiment_subject) { double(:subject, to_global_id: 'z') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when subject is nil' do
+ let(:experiment_subject) { nil }
- describe '.enabled_for_value?' do
- subject { described_class.enabled_for_value?(:test_experiment, experimentation_subject_index) }
+ it { is_expected.to eq(false) }
+ end
- let(:experimentation_subject_index) { 9 }
+ context 'when subject is an empty string' do
+ let(:experiment_subject) { '' }
- context 'experiment is disabled' do
- before do
- allow(described_class).to receive(:enabled?).and_return(false)
+ it { is_expected.to eq(false) }
+ end
end
- it { is_expected.to be_falsey }
- end
+ context 'when experiment is not active' do
+ before do
+ allow(described_class).to receive(:active?).and_return(false)
+ end
- context 'experiment is enabled' do
- before do
- allow(described_class).to receive(:enabled?).and_return(true)
+ it { is_expected.to eq(false) }
end
+ end
- it { is_expected.to be_truthy }
+ context 'with backwards compatible index calculation' do
+ let(:experiment_subject) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7
- describe 'experimentation_subject_index' do
- context 'experimentation_subject_index is not set' do
- let(:experimentation_subject_index) { nil }
+ subject { described_class.in_experiment_group?(:backwards_compatible_test_experiment, subject: experiment_subject) }
- it { is_expected.to be_falsey }
+ context 'when experiment is active' do
+ before do
+ allow(described_class).to receive(:active?).and_return(true)
end
- context 'experimentation_subject_index is an empty string' do
- let(:experimentation_subject_index) { '' }
-
- it { is_expected.to be_falsey }
+ context 'when subject is part of the experiment' do
+ it { is_expected.to eq(true) }
end
- context 'experimentation_subject_index outside enabled ratio' do
- let(:experimentation_subject_index) { 11 }
+ context 'when subject is not part of the experiment' do
+ let(:experiment_subject) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17
- it { is_expected.to be_falsey }
+ it { is_expected.to eq(false) }
end
- end
- end
- end
- describe '.enabled_for_attribute?' do
- subject { described_class.enabled_for_attribute?(:test_experiment, attribute) }
+ context 'when subject has a global_id' do
+ let(:experiment_subject) { double(:subject, to_global_id: 'abcd') }
- let(:attribute) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7
+ it { is_expected.to eq(true) }
+ end
- context 'experiment is disabled' do
- before do
- allow(described_class).to receive(:enabled?).and_return(false)
- end
+ context 'when subject is nil' do
+ let(:experiment_subject) { nil }
- it { is_expected.to be false }
- end
+ it { is_expected.to eq(false) }
+ end
- context 'experiment is enabled' do
- before do
- allow(described_class).to receive(:enabled?).and_return(true)
- end
+ context 'when subject is an empty string' do
+ let(:experiment_subject) { '' }
- it { is_expected.to be true }
+ it { is_expected.to eq(false) }
+ end
+ end
- context 'outside enabled ratio' do
- let(:attribute) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17
+ context 'when experiment is not active' do
+ before do
+ allow(described_class).to receive(:active?).and_return(false)
+ end
- it { is_expected.to be false }
+ it { is_expected.to eq(false) }
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 6dfa791f70b..c917945499c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -929,7 +929,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'with max_count' do
- it 'returns the number of commits with path ' do
+ it 'returns the number of commits with path' do
options = { ref: 'master', max_count: 5 }
expect(repository.count_commits(options)).to eq(5)
@@ -937,7 +937,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'with path' do
- it 'returns the number of commits with path ' do
+ it 'returns the number of commits with path' do
options = { ref: 'master', path: 'encoding' }
expect(repository.count_commits(options)).to eq(2)
@@ -965,7 +965,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
context 'with max_count' do
- it 'returns the number of commits with path ' do
+ it 'returns the number of commits with path' do
options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 }
expect(repository.count_commits(options)).to eq([1, 1])
@@ -1185,6 +1185,66 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#find_changed_paths' do
+ let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
+ let(:commit_1_files) do
+ [
+ OpenStruct.new(status: :ADDED, path: "files/executables/ls"),
+ OpenStruct.new(status: :ADDED, path: "files/executables/touch"),
+ OpenStruct.new(status: :ADDED, path: "files/links/regex.rb"),
+ OpenStruct.new(status: :ADDED, path: "files/links/ruby-style-guide.md"),
+ OpenStruct.new(status: :ADDED, path: "files/links/touch"),
+ OpenStruct.new(status: :MODIFIED, path: ".gitmodules"),
+ OpenStruct.new(status: :ADDED, path: "deeper/nested/six"),
+ OpenStruct.new(status: :ADDED, path: "nested/six")
+ ]
+ end
+
+ let(:commit_2_files) do
+ [OpenStruct.new(status: :ADDED, path: "bin/executable")]
+ end
+
+ let(:commit_3_files) do
+ [
+ OpenStruct.new(status: :MODIFIED, path: ".gitmodules"),
+ OpenStruct.new(status: :ADDED, path: "gitlab-shell")
+ ]
+ end
+
+ it 'returns a list of paths' do
+ collection = repository.find_changed_paths([commit_1, commit_2, commit_3])
+
+ expect(collection).to be_a(Enumerable)
+ expect(collection.to_a).to eq(commit_1_files + commit_2_files + commit_3_files)
+ end
+
+ it 'returns no paths when SHAs are invalid' do
+ collection = repository.find_changed_paths(['invalid', commit_1])
+
+ expect(collection).to be_a(Enumerable)
+ expect(collection.to_a).to be_empty
+ end
+
+ it 'returns a list of paths even when containing a blank ref' do
+ collection = repository.find_changed_paths([nil, commit_1])
+
+ expect(collection).to be_a(Enumerable)
+ expect(collection.to_a).to eq(commit_1_files)
+ end
+
+ it 'returns no paths when the commits are nil' do
+ expect_any_instance_of(Gitlab::GitalyClient::CommitService)
+ .not_to receive(:find_changed_paths)
+
+ collection = repository.find_changed_paths([nil, nil])
+
+ expect(collection).to be_a(Enumerable)
+ expect(collection.to_a).to be_empty
+ end
+ end
+
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
diff --git a/spec/lib/gitlab/git_access_project_spec.rb b/spec/lib/gitlab/git_access_project_spec.rb
index f80915b2be9..953b74cf1a9 100644
--- a/spec/lib/gitlab/git_access_project_spec.rb
+++ b/spec/lib/gitlab/git_access_project_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Gitlab::GitAccessProject do
let(:actor) { user }
let(:project_path) { project.path }
let(:namespace_path) { project&.namespace&.path }
+ let(:repository_path) { "#{namespace_path}/#{project_path}.git" }
let(:protocol) { 'ssh' }
let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:changes) { Gitlab::GitAccess::ANY }
@@ -17,7 +18,7 @@ RSpec.describe Gitlab::GitAccessProject do
let(:access) do
described_class.new(actor, container, protocol,
authentication_abilities: authentication_abilities,
- repository_path: project_path, namespace_path: namespace_path)
+ repository_path: repository_path)
end
describe '#check_namespace!' do
@@ -103,6 +104,20 @@ RSpec.describe Gitlab::GitAccessProject do
end
end
+ context 'when namespace is blank' do
+ let(:repository_path) { 'project.git' }
+
+ it_behaves_like 'no project is created' do
+ let(:raise_specific_error) { raise_namespace_not_found }
+ end
+ end
+
+ context 'when namespace does not exist' do
+ let(:namespace_path) { 'unknown' }
+
+ it_behaves_like 'no project is created'
+ end
+
context 'when user cannot create project in namespace' do
let(:user2) { create(:user) }
let(:namespace_path) { user2.namespace.path }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 21607edbc32..780f4329bcc 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -10,8 +10,7 @@ RSpec.describe Gitlab::GitAccess do
let(:actor) { user }
let(:project) { create(:project, :repository) }
- let(:project_path) { project&.path }
- let(:namespace_path) { project&.namespace&.path }
+ let(:repository_path) { "#{project.full_path}.git" }
let(:protocol) { 'ssh' }
let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil }
@@ -210,10 +209,9 @@ RSpec.describe Gitlab::GitAccess do
end
end
- context 'when the project is nil' do
+ context 'when the project does not exist' do
let(:project) { nil }
- let(:project_path) { "new-project" }
- let(:namespace_path) { user.namespace.path }
+ let(:repository_path) { "#{user.namespace.path}/new-project.git" }
it 'blocks push and pull with "not found"' do
aggregate_failures do
@@ -389,6 +387,108 @@ RSpec.describe Gitlab::GitAccess do
end
end
+ describe '#check_otp_session!' do
+ let_it_be(:user) { create(:user, :two_factor_via_otp)}
+ let_it_be(:key) { create(:key, user: user) }
+ let_it_be(:actor) { key }
+
+ before do
+ project.add_developer(user)
+ stub_feature_flags(two_factor_for_cli: true)
+ end
+
+ context 'with an OTP session', :clean_gitlab_redis_shared_state do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true)
+ end
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+
+ context 'without OTP session' do
+ it 'does not allow push or pull access' do
+ user = 'jane.doe'
+ host = 'fridge.ssh'
+ port = 42
+
+ stub_config(
+ gitlab_shell: {
+ ssh_user: user,
+ ssh_host: host,
+ ssh_port: port
+ }
+ )
+
+ error_message = "OTP verification is required to access the repository.\n\n"\
+ " Use: ssh #{user}@#{host} -p #{port} 2fa_verify"
+
+ aggregate_failures do
+ expect { push_access_check }.to raise_forbidden(error_message)
+ expect { pull_access_check }.to raise_forbidden(error_message)
+ end
+ end
+
+ context 'when protocol is HTTP' do
+ let(:protocol) { 'http' }
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when actor is not an SSH key' do
+ let(:deploy_key) { create(:deploy_key, user: user) }
+ let(:actor) { deploy_key }
+
+ before do
+ deploy_key.deploy_keys_projects.create(project: project, can_push: true)
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when 2FA is not enabled for the user' do
+ let(:user) { create(:user)}
+ let(:actor) { create(:key, user: user) }
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(two_factor_for_cli: false)
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+
describe '#check_db_accessibility!' do
context 'when in a read-only GitLab instance' do
before do
@@ -452,9 +552,8 @@ RSpec.describe Gitlab::GitAccess do
context 'when project is public' do
let(:public_project) { create(:project, :public, :repository) }
- let(:project_path) { public_project.path }
- let(:namespace_path) { public_project.namespace.path }
- let(:access) { access_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], repository_path: project_path, namespace_path: namespace_path) }
+ let(:repository_path) { "#{public_project.full_path}.git" }
+ let(:access) { access_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], repository_path: repository_path) }
context 'when repository is enabled' do
it 'give access to download code' do
@@ -1169,7 +1268,7 @@ RSpec.describe Gitlab::GitAccess do
def access
access_class.new(actor, project, protocol,
authentication_abilities: authentication_abilities,
- namespace_path: namespace_path, repository_path: project_path,
+ repository_path: repository_path,
redirected_path: redirected_path, auth_result_type: auth_result_type)
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index b09bd9dff1b..157c2393ce1 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -145,6 +145,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
end
+ describe '#find_changed_paths' do
+ let(:commits) { %w[1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 cfe32cf61b73a0d5e9f13e774abde7ff789b1660] }
+
+ it 'sends an RPC request and returns the stats' do
+ request = Gitaly::FindChangedPathsRequest.new(repository: repository_message,
+ commits: commits)
+
+ changed_paths_response = Gitaly::FindChangedPathsResponse.new(
+ paths: [{
+ path: "app/assets/javascripts/boards/components/project_select.vue",
+ status: :MODIFIED
+ }])
+
+ expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:find_changed_paths)
+ .with(request, kind_of(Hash)).and_return([changed_paths_response])
+
+ returned_value = described_class.new(repository).find_changed_paths(commits)
+
+ mapped_returned_value = returned_value.map(&:to_h)
+ mapped_expected_value = changed_paths_response.paths.map(&:to_h)
+
+ expect(mapped_returned_value).to eq(mapped_expected_value)
+ end
+ end
+
describe '#tree_entries' do
let(:path) { '/' }
@@ -357,7 +382,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
it 'sends an RPC request with the correct payload' do
- expect(client.commits_by_message(query, options)).to match_array(wrap_commits(commits))
+ expect(client.commits_by_message(query, **options)).to match_array(wrap_commits(commits))
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 16dd2bbee6d..7fcb11c4dfd 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Gitlab::GitalyClient do
describe '.filesystem_id_from_disk' do
it 'catches errors' do
[Errno::ENOENT, Errno::EACCES, JSON::ParserError].each do |error|
- allow(File).to receive(:read).with(described_class.storage_metadata_file_path('default')).and_raise(error)
+ stub_file_read(described_class.storage_metadata_file_path('default'), error: error)
expect(described_class.filesystem_id_from_disk('default')).to be_nil
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index bc734644d29..4000e0b2611 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -28,6 +28,17 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#pull_request_reviews' do
+ it 'returns the pull request reviews' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:pull_request_reviews).with('foo/bar', 999)
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.pull_request_reviews('foo/bar', 999)
+ end
+ end
+
describe '#repository' do
it 'returns the details of a repository' do
client = described_class.new('foo')
@@ -39,6 +50,17 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#pull_request' do
+ it 'returns the details of a pull_request' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:pull_request).with('foo/bar', 999)
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.pull_request('foo/bar', 999)
+ end
+ end
+
describe '#labels' do
it 'returns the labels' do
client = described_class.new('foo')
@@ -478,7 +500,7 @@ RSpec.describe Gitlab::GithubImport::Client do
it 'searches for repositories based on name' do
expected_search_query = 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2'
- expect(client).to receive(:each_page).with(:search_repositories, expected_search_query)
+ expect(client.octokit).to receive(:search_repositories).with(expected_search_query, {})
client.search_repos_by_name('test')
end
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index 6188ba8ec3f..8ee534734f0 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -49,6 +49,57 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
importer.execute
end
end
+
+ context 'when LFS list download fails' do
+ it 'rescues and logs the known exceptions' do
+ exception = StandardError.new('Invalid Project URL')
+ importer = described_class.new(project, client, parallel: false)
+
+ expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_raise(exception)
+ end
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(
+ message: 'importer failed',
+ import_source: :github,
+ project_id: project.id,
+ parallel: false,
+ importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter',
+ 'error.message': 'Invalid Project URL'
+ )
+ end
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ exception,
+ import_source: :github,
+ parallel: false,
+ project_id: project.id,
+ importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter'
+ ).and_call_original
+
+ importer.execute
+ end
+
+ it 'raises and logs the unknown exceptions' do
+ exception = Exception.new('Really bad news')
+ importer = described_class.new(project, client, parallel: false)
+
+ expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_raise(exception)
+ end
+
+ expect { importer.execute }.to raise_error(exception)
+ end
+ end
end
describe '#sequential_import' do
@@ -56,18 +107,16 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
importer = described_class.new(project, client, parallel: false)
lfs_object_importer = double(:lfs_object_importer)
- allow(importer)
- .to receive(:each_object_to_import)
- .and_yield(lfs_download_object)
+ expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
+ expect(service).to receive(:execute).and_return([lfs_download_object])
+ end
expect(Gitlab::GithubImport::Importer::LfsObjectImporter)
- .to receive(:new)
- .with(
+ .to receive(:new).with(
an_instance_of(Gitlab::GithubImport::Representation::LfsObject),
project,
client
- )
- .and_return(lfs_object_importer)
+ ).and_return(lfs_object_importer)
expect(lfs_object_importer).to receive(:execute)
@@ -79,9 +128,9 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
it 'imports each lfs object in parallel' do
importer = described_class.new(project, client)
- allow(importer)
- .to receive(:each_object_to_import)
- .and_yield(lfs_download_object)
+ expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
+ expect(service).to receive(:execute).and_return([lfs_download_object])
+ end
expect(Gitlab::GithubImport::ImportLfsObjectWorker)
.to receive(:perform_async)
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 46850618945..c7388314253 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
describe '#execute' do
it 'imports the pull request' do
- mr = double(:merge_request, id: 10)
+ mr = double(:merge_request, id: 10, merged?: false)
expect(importer)
.to receive(:create_merge_request)
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
new file mode 100644
index 00000000000..2999dc5bb41
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :clean_gitlab_redis_cache do
+ let_it_be(:merge_request) { create(:merged_merge_request) }
+ let(:project) { merge_request.project }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00).utc }
+ let(:client_double) { double(user: double(id: 999, login: 'merger', email: 'merger@email.com')) }
+
+ let(:pull_request) do
+ instance_double(
+ Gitlab::GithubImport::Representation::PullRequest,
+ iid: merge_request.iid,
+ created_at: created_at,
+ merged_by: double(id: 999, login: 'merger')
+ )
+ end
+
+ subject { described_class.new(pull_request, project, client_double) }
+
+ it 'assigns the merged by user when mapped' do
+ merge_user = create(:user, email: 'merger@email.com')
+
+ subject.execute
+
+ expect(merge_request.metrics.reload.merged_by).to eq(merge_user)
+ end
+
+ it 'adds a note referencing the merger user when the user cannot be mapped' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ .and not_change(merge_request, :updated_at)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("*Merged by: merger*")
+ expect(last_note.created_at).to eq(created_at)
+ expect(last_note.author).to eq(project.creator)
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
new file mode 100644
index 00000000000..b2f993ac47c
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean_gitlab_redis_cache do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:client_double) { double(user: double(id: 999, login: 'author', email: 'author@email.com')) }
+ let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc }
+
+ subject { described_class.new(review, project, client_double) }
+
+ context 'when the review author can be mapped to a gitlab user' do
+ let_it_be(:author) { create(:user, email: 'author@email.com') }
+
+ context 'when the review has no note text' do
+ context 'when the review is "APPROVED"' do
+ let(:review) { create_review(type: 'APPROVED', note: '') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }.to change(Note, :count)
+
+ last_note = merge_request.notes.last
+ expect(last_note.note).to eq('approved this merge request')
+ expect(last_note.author).to eq(author)
+ expect(last_note.created_at).to eq(submitted_at)
+ expect(last_note.system_note_metadata.action).to eq('approved')
+
+ expect(merge_request.approved_by_users.reload).to include(author)
+ expect(merge_request.approvals.last.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "COMMENTED"' do
+ let(:review) { create_review(type: 'COMMENTED', note: '') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }.not_to change(Note, :count)
+ end
+ end
+
+ context 'when the review is "CHANGES_REQUESTED"' do
+ let(:review) { create_review(type: 'CHANGES_REQUESTED', note: '') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }.not_to change(Note, :count)
+ end
+ end
+ end
+
+ context 'when the review has a note text' do
+ context 'when the review is "APPROVED"' do
+ let(:review) { create_review(type: 'APPROVED') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }
+ .to change(Note, :count).by(2)
+ .and change(Approval, :count).by(1)
+
+ note = merge_request.notes.where(system: false).last
+ expect(note.note).to eq("**Review:** Approved\n\nnote")
+ expect(note.author).to eq(author)
+ expect(note.created_at).to eq(submitted_at)
+
+ system_note = merge_request.notes.where(system: true).last
+ expect(system_note.note).to eq('approved this merge request')
+ expect(system_note.author).to eq(author)
+ expect(system_note.created_at).to eq(submitted_at)
+ expect(system_note.system_note_metadata.action).to eq('approved')
+
+ expect(merge_request.approved_by_users.reload).to include(author)
+ expect(merge_request.approvals.last.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "COMMENTED"' do
+ let(:review) { create_review(type: 'COMMENTED') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ .and not_change(Approval, :count)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("**Review:** Commented\n\nnote")
+ expect(last_note.author).to eq(author)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "CHANGES_REQUESTED"' do
+ let(:review) { create_review(type: 'CHANGES_REQUESTED') }
+
+ it 'creates a note for the review' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ .and not_change(Approval, :count)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("**Review:** Changes requested\n\nnote")
+ expect(last_note.author).to eq(author)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+ end
+ end
+
+ context 'when the review author cannot be mapped to a gitlab user' do
+ context 'when the review has no note text' do
+ context 'when the review is "APPROVED"' do
+ let(:review) { create_review(type: 'APPROVED', note: '') }
+
+ it 'creates a note for the review with *Approved by by<author>*' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+
+ last_note = merge_request.notes.last
+ expect(last_note.note).to eq("*Created by author*\n\n**Review:** Approved")
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "COMMENTED"' do
+ let(:review) { create_review(type: 'COMMENTED', note: '') }
+
+ it 'creates a note for the review with *Commented by<author>*' do
+ expect { subject.execute }.not_to change(Note, :count)
+ end
+ end
+
+ context 'when the review is "CHANGES_REQUESTED"' do
+ let(:review) { create_review(type: 'CHANGES_REQUESTED', note: '') }
+
+ it 'creates a note for the review with *Changes requested by <author>*' do
+ expect { subject.execute }.not_to change(Note, :count)
+ end
+ end
+ end
+
+ context 'when the review has a note text' do
+ context 'when the review is "APPROVED"' do
+ let(:review) { create_review(type: 'APPROVED') }
+
+ it 'creates a note for the review with *Approved by by<author>*' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("*Created by author*\n\n**Review:** Approved\n\nnote")
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "COMMENTED"' do
+ let(:review) { create_review(type: 'COMMENTED') }
+
+ it 'creates a note for the review with *Commented by<author>*' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("*Created by author*\n\n**Review:** Commented\n\nnote")
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+
+ context 'when the review is "CHANGES_REQUESTED"' do
+ let(:review) { create_review(type: 'CHANGES_REQUESTED') }
+
+ it 'creates a note for the review with *Changes requested by <author>*' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+
+ last_note = merge_request.notes.last
+
+ expect(last_note.note).to eq("*Created by author*\n\n**Review:** Changes requested\n\nnote")
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+ end
+ end
+
+ def create_review(type:, note: 'note')
+ Gitlab::GithubImport::Representation::PullRequestReview.from_json_hash(
+ merge_request_id: merge_request.id,
+ review_type: type,
+ note: note,
+ submitted_at: submitted_at.to_s,
+ author: { id: 999, login: 'author' }
+ )
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index 0835c6155b9..8a7867f3841 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -29,6 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
milestone: double(:milestone, number: 4),
user: double(:user, id: 4, login: 'alice'),
assignee: double(:user, id: 4, login: 'alice'),
+ merged_by: double(:user, id: 4, login: 'alice'),
created_at: 1.second.ago,
updated_at: 1.second.ago,
merged_at: 1.second.ago
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb
new file mode 100644
index 00000000000..b859cc727a6
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
+ let(:client) { double }
+ let(:project) { create(:project, import_source: 'http://somegithub.com') }
+
+ subject { described_class.new(project, client) }
+
+ it { is_expected.to include_module(Gitlab::GithubImport::ParallelScheduling) }
+
+ describe '#representation_class' do
+ it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::PullRequest) }
+ end
+
+ describe '#importer_class' do
+ it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestMergedByImporter) }
+ end
+
+ describe '#collection_method' do
+ it { expect(subject.collection_method).to eq(:pull_requests_merged_by) }
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it { expect(subject.id_for_already_imported_cache(double(number: 1))).to eq(1) }
+ end
+
+ describe '#each_object_to_import' do
+ it 'fetchs the merged pull requests data' do
+ pull_request = double
+ create(
+ :merged_merge_request,
+ iid: 999,
+ source_project: project,
+ target_project: project
+ )
+
+ allow(client)
+ .to receive(:pull_request)
+ .with('http://somegithub.com', 999)
+ .and_return(pull_request)
+
+ expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(pull_request)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
new file mode 100644
index 00000000000..5e2302f9662
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do
+ let(:client) { double }
+ let(:project) { create(:project, import_source: 'github/repo') }
+
+ subject { described_class.new(project, client) }
+
+ it { is_expected.to include_module(Gitlab::GithubImport::ParallelScheduling) }
+
+ describe '#representation_class' do
+ it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::PullRequestReview) }
+ end
+
+ describe '#importer_class' do
+ it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestReviewImporter) }
+ end
+
+ describe '#collection_method' do
+ it { expect(subject.collection_method).to eq(:pull_request_reviews) }
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it { expect(subject.id_for_already_imported_cache(double(github_id: 1))).to eq(1) }
+ end
+
+ describe '#each_object_to_import' do
+ it 'fetchs the merged pull requests data' do
+ merge_request = create(:merge_request, source_project: project)
+ review = double
+
+ expect(review)
+ .to receive(:merge_request_id=)
+ .with(merge_request.id)
+
+ allow(client)
+ .to receive(:pull_request_reviews)
+ .with('github/repo', merge_request.iid)
+ .and_return([review])
+
+ expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(review)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index 578743be96b..1e31cd2f007 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
Class.new do
include(Gitlab::GithubImport::ParallelScheduling)
+ def importer_class
+ Class
+ end
+
def collection_method
:issues
end
@@ -63,6 +67,82 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
importer.execute
end
+
+ it 'logs the the process' do
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_return([])
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(
+ message: 'starting importer',
+ import_source: :github,
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+ expect(logger)
+ .to receive(:info)
+ .with(
+ message: 'importer finished',
+ import_source: :github,
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+ end
+
+ importer.execute
+ end
+
+ it 'logs the error when it fails' do
+ exception = StandardError.new('some error')
+
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_raise(exception)
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(
+ message: 'starting importer',
+ import_source: :github,
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+ expect(logger)
+ .to receive(:error)
+ .with(
+ message: 'importer failed',
+ import_source: :github,
+ project_id: project.id,
+ parallel: false,
+ importer: 'Class',
+ 'error.message': 'some error'
+ )
+ end
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ exception,
+ import_source: :github,
+ parallel: false,
+ project_id: project.id,
+ importer: 'Class'
+ )
+ .and_call_original
+
+ expect { importer.execute }.to raise_error(exception)
+ end
end
describe '#sequential_import' do
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
new file mode 100644
index 00000000000..f9763455468
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
+ let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc }
+
+ shared_examples 'a PullRequest review' do
+ it 'returns an instance of PullRequest' do
+ expect(review).to be_an_instance_of(described_class)
+ expect(review.author).to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+ expect(review.author.id).to eq(4)
+ expect(review.author.login).to eq('alice')
+ expect(review.note).to eq('note')
+ expect(review.review_type).to eq('APPROVED')
+ expect(review.submitted_at).to eq(submitted_at)
+ expect(review.github_id).to eq(999)
+ expect(review.merge_request_id).to eq(42)
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ id: 999,
+ merge_request_id: 42,
+ body: 'note',
+ state: 'APPROVED',
+ user: double(:user, id: 4, login: 'alice'),
+ submitted_at: submitted_at
+ )
+ end
+
+ it_behaves_like 'a PullRequest review' do
+ let(:review) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ review = described_class.from_api_response(response)
+
+ expect(review.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ let(:hash) do
+ {
+ 'github_id' => 999,
+ 'merge_request_id' => 42,
+ 'note' => 'note',
+ 'review_type' => 'APPROVED',
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'submitted_at' => submitted_at.to_s
+ }
+ end
+
+ it_behaves_like 'a PullRequest review' do
+ let(:review) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ review = described_class.from_json_hash(hash.except('author'))
+
+ expect(review.author).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
index 370eac1d993..27a82951b01 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -115,6 +115,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do
milestone: double(:milestone, number: 4),
user: double(:user, id: 4, login: 'alice'),
assignee: double(:user, id: 4, login: 'alice'),
+ merged_by: double(:user, id: 4, login: 'alice'),
created_at: created_at,
updated_at: updated_at,
merged_at: merged_at
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
deleted file mode 100644
index 402d2169432..00000000000
--- a/spec/lib/gitlab/google_code_import/client_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-RSpec.describe Gitlab::GoogleCodeImport::Client do
- let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
-
- subject { described_class.new(raw_data) }
-
- describe "#valid?" do
- context "when the data is valid" do
- it "returns true" do
- expect(subject).to be_valid
- end
- end
-
- context "when the data is invalid" do
- let(:raw_data) { "No clue" }
-
- it "returns true" do
- expect(subject).not_to be_valid
- end
- end
- end
-
- describe "#repos" do
- it "returns only Git repositories" do
- expect(subject.repos.length).to eq(1)
- expect(subject.incompatible_repos.length).to eq(1)
- end
- end
-
- describe "#repo" do
- it "returns the referenced repository" do
- expect(subject.repo("tint2").name).to eq("tint2")
- end
- end
-end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
deleted file mode 100644
index a22e80ae1c0..00000000000
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-RSpec.describe Gitlab::GoogleCodeImport::Importer do
- let(:mapped_user) { create(:user, username: "thilo123") }
- let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
- let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
- let(:import_data) do
- {
- 'repo' => client.repo('tint2').raw_data,
- 'user_map' => { 'thilo...' => "@#{mapped_user.username}" }
- }
- end
-
- let(:project) { create(:project) }
-
- subject { described_class.new(project) }
-
- before do
- project.add_maintainer(project.creator)
- project.create_import_data(data: import_data)
- end
-
- describe "#execute" do
- it "imports status labels" do
- subject.execute
-
- %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status|
- expect(project.labels.find_by(name: "Status: #{status}")).not_to be_nil
- end
- end
-
- it "imports labels" do
- subject.execute
-
- %w(
- Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
- Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
- Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
- Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
- ).each do |label|
- label = label.sub("-", ": ")
- expect(project.labels.find_by(name: label)).not_to be_nil
- end
- end
-
- it "imports issues" do
- subject.execute
-
- issue = project.issues.first
- expect(issue).not_to be_nil
- expect(issue.iid).to eq(169)
- expect(issue.author).to eq(project.creator)
- expect(issue.assignees).to eq([mapped_user])
- expect(issue.state).to eq("closed")
- expect(issue.label_names).to include("Priority: Medium")
- expect(issue.label_names).to include("Status: Fixed")
- expect(issue.label_names).to include("Type: Enhancement")
- expect(issue.title).to eq("Scrolling through tasks")
- expect(issue.state).to eq("closed")
- expect(issue.description).to include("schattenpr\\.\\.\\.")
- expect(issue.description).to include("November 18, 2009 00:20")
- expect(issue.description).to include("Google Code")
- expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel (like in fluxbox).')
- expect(issue.description).to include('Patch is attached that adds two new mouse-actions (next_task+prev_task)')
- expect(issue.description).to include('that can be used for exactly that purpose.')
- expect(issue.description).to include('all the best!')
- expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)')
- expect(issue.description).to include('![screenshot.png](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot.png)')
- expect(issue.description).to include('![screenshot1.PNG](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot1.PNG)')
- end
-
- it "imports issue comments" do
- subject.execute
-
- note = project.issues.first.notes.first
- expect(note).not_to be_nil
- expect(note.note).to include("Comment 1")
- expect(note.note).to include("@#{mapped_user.username}")
- expect(note.note).to include("November 18, 2009 05:14")
- expect(note.note).to include("applied, thanks.")
- expect(note.note).to include("Status: Fixed")
- expect(note.note).to include("~~Type: Defect~~")
- expect(note.note).to include("Type: Enhancement")
- end
- end
-end
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
deleted file mode 100644
index cfebe57aed3..00000000000
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::GoogleCodeImport::ProjectCreator do
- let(:user) { create(:user) }
- let(:repo) do
- Gitlab::GoogleCodeImport::Repository.new(
- "name" => 'vim',
- "summary" => 'VI Improved',
- "repositoryUrls" => ["https://vim.googlecode.com/git/"]
- )
- end
-
- let(:namespace) { create(:group) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- expect_next_instance_of(Project) do |project|
- expect(project).to receive(:add_import_job)
- end
-
- project_creator = described_class.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.import_url).to eq("https://vim.googlecode.com/git/")
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-end
diff --git a/spec/lib/gitlab/graphql/docs/renderer_spec.rb b/spec/lib/gitlab/graphql/docs/renderer_spec.rb
index d1be962a4f8..064e0c6828b 100644
--- a/spec/lib/gitlab/graphql/docs/renderer_spec.rb
+++ b/spec/lib/gitlab/graphql/docs/renderer_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
Class.new(Types::BaseObject) do
graphql_name 'ArrayTest'
- field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description'
+ field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description.'
end
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Field | Type | Description |
| ----- | ---- | ----------- |
- | `foo` | String! => Array | A description |
+ | `foo` | String! => Array | A description. |
DOC
is_expected.to include(expectation)
@@ -52,8 +52,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
Class.new(Types::BaseObject) do
graphql_name 'OrderingTest'
- field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field'
- field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field'
+ field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field.'
+ field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field.'
end
end
@@ -63,8 +63,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Field | Type | Description |
| ----- | ---- | ----------- |
- | `bar` | String! | A description of bar field |
- | `foo` | String! | A description of foo field |
+ | `bar` | String! | A description of bar field. |
+ | `foo` | String! | A description of foo field. |
DOC
is_expected.to include(expectation)
@@ -76,7 +76,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
Class.new(Types::BaseObject) do
graphql_name 'DeprecatedTest'
- field :foo, GraphQL::STRING_TYPE, null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description'
+ field :foo, GraphQL::STRING_TYPE, null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description.'
end
end
@@ -86,7 +86,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Field | Type | Description |
| ----- | ---- | ----------- |
- | `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10 |
+ | `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10. |
DOC
is_expected.to include(expectation)
@@ -98,14 +98,14 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
enum_type = Class.new(Types::BaseEnum) do
graphql_name 'MyEnum'
- value 'BAZ', description: 'A description of BAZ'
- value 'BAR', description: 'A description of BAR', deprecated: { reason: 'This is deprecated', milestone: '1.10' }
+ value 'BAZ', description: 'A description of BAZ.'
+ value 'BAR', description: 'A description of BAR.', deprecated: { reason: 'This is deprecated', milestone: '1.10' }
end
Class.new(Types::BaseObject) do
graphql_name 'EnumTest'
- field :foo, enum_type, null: false, description: 'A description of foo field'
+ field :foo, enum_type, null: false, description: 'A description of foo field.'
end
end
@@ -115,8 +115,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Value | Description |
| ----- | ----------- |
- | `BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10 |
- | `BAZ` | A description of BAZ |
+ | `BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10. |
+ | `BAZ` | A description of BAZ. |
DOC
is_expected.to include(expectation)
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index 82090f992eb..0e36ea14ac3 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
.to raise_error(expected_error)
end
+ # TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
+ # so that until that time, the developer check is there
it 'raises when passing a resolve block' do
expect { class_with_markdown_field(:test_html, null: true, resolve: -> (_, _, _) { 'not really' } ) }
.to raise_error(expected_error)
diff --git a/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb
new file mode 100644
index 00000000000..03cf53bb990
--- /dev/null
+++ b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Graphql::Pagination::ArrayConnection do
+ let(:nodes) { (1..10) }
+
+ subject(:connection) { described_class.new(nodes, max_page_size: 100) }
+
+ it_behaves_like 'a connection with collection methods'
+
+ it_behaves_like 'a redactable connection' do
+ let(:unwanted) { 5 }
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
index 932bcd8cd92..d2475d1edb9 100644
--- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
@@ -10,7 +10,13 @@ RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection d
let(:arguments) { {} }
subject(:connection) do
- described_class.new(all_nodes, { max_page_size: values.size }.merge(arguments))
+ described_class.new(all_nodes, **{ max_page_size: values.size }.merge(arguments))
+ end
+
+ it_behaves_like 'a connection with collection methods'
+
+ it_behaves_like 'a redactable connection' do
+ let(:unwanted) { 3 }
end
describe '#nodes' do
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index c8f368b15fc..0ac54a20fcc 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -10,17 +10,24 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
subject(:connection) do
- described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments))
+ described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments))
end
def encoded_cursor(node)
- described_class.new(nodes, { context: context }).cursor_for(node)
+ described_class.new(nodes, context: context).cursor_for(node)
end
def decoded_cursor(cursor)
Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
+ it_behaves_like 'a connection with collection methods'
+
+ it_behaves_like 'a redactable connection' do
+ let_it_be(:projects) { create_list(:project, 2) }
+ let(:unwanted) { projects.second }
+ end
+
describe '#cursor_for' do
let(:project) { create(:project) }
let(:cursor) { connection.cursor_for(project) }
diff --git a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb
index 86f35de94ed..1ca7c1c3c69 100644
--- a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb
@@ -6,4 +6,15 @@ RSpec.describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
it 'subclasses from GraphQL::Relay::RelationConnection' do
expect(described_class.superclass).to eq GraphQL::Pagination::ActiveRecordRelationConnection
end
+
+ it_behaves_like 'a connection with collection methods' do
+ let(:connection) { described_class.new(Project.all) }
+ end
+
+ it_behaves_like 'a redactable connection' do
+ let_it_be(:users) { create_list(:user, 2) }
+
+ let(:connection) { described_class.new(User.all, max_page_size: 10) }
+ let(:unwanted) { users.second }
+ end
end
diff --git a/spec/lib/gitlab/graphql/timeout_spec.rb b/spec/lib/gitlab/graphql/timeout_spec.rb
index 3669a89ba7c..999840019d2 100644
--- a/spec/lib/gitlab/graphql/timeout_spec.rb
+++ b/spec/lib/gitlab/graphql/timeout_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Graphql::Timeout do
- it 'inherits from ' do
+ it 'inherits from' do
expect(described_class.superclass).to eq GraphQL::Schema::Timeout
end
diff --git a/spec/lib/gitlab/hook_data/group_member_builder_spec.rb b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb
new file mode 100644
index 00000000000..78c62fd23c7
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::GroupMemberBuilder do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_member) { create(:group_member, :developer, group: group, expires_at: 1.day.from_now) }
+
+ describe '#build' do
+ let(:data) { described_class.new(group_member).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :expires_at, :group_name, :group_path,
+ :group_id, :user_id, :user_username, :user_name, :user_email, :group_access
+ ]
+ end
+
+ context 'data' do
+ shared_examples_for 'includes the required attributes' do
+ it 'includes the required attributes' do
+ expect(data).to include(*attributes)
+
+ expect(data[:group_name]).to eq(group.name)
+ expect(data[:group_path]).to eq(group.path)
+ expect(data[:group_id]).to eq(group.id)
+ expect(data[:user_username]).to eq(group_member.user.username)
+ expect(data[:user_name]).to eq(group_member.user.name)
+ expect(data[:user_email]).to eq(group_member.user.email)
+ expect(data[:user_id]).to eq(group_member.user.id)
+ expect(data[:group_access]).to eq('Developer')
+ expect(data[:created_at]).to eq(group_member.created_at&.xmlschema)
+ expect(data[:updated_at]).to eq(group_member.updated_at&.xmlschema)
+ expect(data[:expires_at]).to eq(group_member.expires_at&.xmlschema)
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('user_add_to_group') }
+ it_behaves_like 'includes the required attributes'
+ end
+
+ context 'on update' do
+ let(:event) { :update }
+
+ it { expect(event_name).to eq('user_update_for_group') }
+ it_behaves_like 'includes the required attributes'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('user_remove_from_group') }
+ it_behaves_like 'includes the required attributes'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb
index e04c0b49480..f2ee6bb72d9 100644
--- a/spec/lib/gitlab/i18n/po_linter_spec.rb
+++ b/spec/lib/gitlab/i18n/po_linter_spec.rb
@@ -6,7 +6,7 @@ require 'simple_po_parser'
# Disabling this cop to allow for multi-language examples in comments
# rubocop:disable Style/AsciiComments
RSpec.describe Gitlab::I18n::PoLinter do
- let(:linter) { described_class.new(po_path: po_path, html_todolist: {}) }
+ let(:linter) { described_class.new(po_path: po_path) }
let(:po_path) { 'spec/fixtures/valid.po' }
def fake_translation(msgid:, translation:, plural_id: nil, plurals: [])
@@ -24,8 +24,7 @@ RSpec.describe Gitlab::I18n::PoLinter do
Gitlab::I18n::TranslationEntry.new(
entry_data: data,
- nplurals: plurals.size + 1,
- html_allowed: nil
+ nplurals: plurals.size + 1
)
end
@@ -160,53 +159,6 @@ RSpec.describe Gitlab::I18n::PoLinter do
]
end
end
-
- context 'when an entry contains html on the todolist' do
- subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) }
-
- let(:po_path) { 'spec/fixtures/potential_html.po' }
- let(:todolist) do
- {
- 'String with a legitimate < use' => {
- 'plural_id' => 'String with lots of < > uses',
- 'translations' => [
- 'Translated string with a legitimate < use',
- 'Translated string with lots of < > uses'
- ]
- }
- }
- end
-
- it 'does not present an error' do
- message_id = 'String with a legitimate < use'
-
- expect(errors[message_id]).to be_nil
- end
- end
-
- context 'when an entry on the html todolist has changed' do
- subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) }
-
- let(:po_path) { 'spec/fixtures/potential_html.po' }
- let(:todolist) do
- {
- 'String with a legitimate < use' => {
- 'plural_id' => 'String with lots of < > uses',
- 'translations' => [
- 'Translated string with a different legitimate < use',
- 'Translated string with lots of < > uses'
- ]
- }
- }
- end
-
- it 'presents an error for the changed component' do
- message_id = 'String with a legitimate < use'
-
- expect(errors[message_id])
- .to include a_string_starting_with('translation contains < or >.')
- end
- end
end
describe '#parse_po' do
@@ -276,8 +228,7 @@ RSpec.describe Gitlab::I18n::PoLinter do
fake_entry = Gitlab::I18n::TranslationEntry.new(
entry_data: { msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' },
- nplurals: 2,
- html_allowed: nil
+ nplurals: 2
)
errors = []
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
index 2c95b0b0124..f05346d07d3 100644
--- a/spec/lib/gitlab/i18n/translation_entry_spec.rb
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#singular_translation' do
it 'returns the normal `msgstr` for translations without plural' do
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.singular_translation).to eq('Bonjour monde')
end
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
'msgstr[0]' => 'Bonjour monde',
'msgstr[1]' => 'Bonjour mondes'
}
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.singular_translation).to eq('Bonjour monde')
end
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#all_translations' do
it 'returns all translations for singular translations' do
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.all_translations).to eq(['Bonjour monde'])
end
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
'msgstr[0]' => 'Bonjour monde',
'msgstr[1]' => 'Bonjour mondes'
}
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes'])
end
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
msgid_plural: 'Hello worlds',
'msgstr[0]' => 'Bonjour monde'
}
- entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 1)
expect(entry.plural_translations).to eq(['Bonjour monde'])
end
@@ -65,7 +65,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
'msgstr[1]' => 'Bonjour mondes',
'msgstr[2]' => 'Bonjour tous les mondes'
}
- entry = described_class.new(entry_data: data, nplurals: 3, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 3)
expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes'])
end
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
msgid: 'hello world',
msgstr: 'hello'
}
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry).to have_singular_translation
end
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
"msgstr[0]" => 'hello world',
"msgstr[1]" => 'hello worlds'
}
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry).to have_singular_translation
end
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
msgid_plural: 'hello worlds',
"msgstr[0]" => 'hello worlds'
}
- entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 1)
expect(entry).not_to have_singular_translation
end
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#msgid_contains_newlines' do
it 'is true when the msgid is an array' do
data = { msgid: %w(hello world) }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.msgid_has_multiple_lines?).to be_truthy
end
@@ -118,7 +118,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#plural_id_contains_newlines' do
it 'is true when the msgid is an array' do
data = { msgid_plural: %w(hello world) }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.plural_id_has_multiple_lines?).to be_truthy
end
@@ -127,7 +127,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#translations_contain_newlines' do
it 'is true when the msgid is an array' do
data = { msgstr: %w(hello world) }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_have_multiple_lines?).to be_truthy
end
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#contains_unescaped_chars' do
let(:data) { { msgid: '' } }
- let(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
+ let(:entry) { described_class.new(entry_data: data, nplurals: 2) }
it 'is true when the msgid is an array' do
string = '「100%確定」'
@@ -177,7 +177,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#msgid_contains_unescaped_chars' do
it 'is true when the msgid contains a `%`' do
data = { msgid: '「100%確定」' }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
expect(entry.msgid_contains_unescaped_chars?).to be_truthy
@@ -187,7 +187,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#plural_id_contains_unescaped_chars' do
it 'is true when the plural msgid contains a `%`' do
data = { msgid_plural: '「100%確定」' }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
expect(entry.plural_id_contains_unescaped_chars?).to be_truthy
@@ -197,7 +197,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
describe '#translations_contain_unescaped_chars' do
it 'is true when the translation contains a `%`' do
data = { msgstr: '「100%確定」' }
- entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
+ entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
expect(entry.translations_contain_unescaped_chars?).to be_truthy
@@ -205,7 +205,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
end
describe '#msgid_contains_potential_html?' do
- subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
+ subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
context 'when there are no angle brackets in the msgid' do
let(:data) { { msgid: 'String with no brackets' } }
@@ -225,7 +225,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
end
describe '#plural_id_contains_potential_html?' do
- subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
+ subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
context 'when there are no angle brackets in the plural_id' do
let(:data) { { msgid_plural: 'String with no brackets' } }
@@ -245,7 +245,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
end
describe '#translations_contain_potential_html?' do
- subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
+ subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
context 'when there are no angle brackets in the translations' do
let(:data) { { msgstr: 'This string has no angle brackets' } }
@@ -263,78 +263,4 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
end
end
end
-
- describe '#msgid_html_allowed?' do
- subject(:entry) do
- described_class.new(entry_data: { msgid: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo)
- end
-
- context 'when the html in the string is in the todolist' do
- let(:html_todo) { { 'plural_id' => nil, 'translations' => [] } }
-
- it 'returns true' do
- expect(entry.msgid_html_allowed?).to be true
- end
- end
-
- context 'when the html in the string is not in the todolist' do
- let(:html_todo) { nil }
-
- it 'returns false' do
- expect(entry.msgid_html_allowed?).to be false
- end
- end
- end
-
- describe '#plural_id_html_allowed?' do
- subject(:entry) do
- described_class.new(entry_data: { msgid_plural: 'String with many <strong>' }, nplurals: 2, html_allowed: html_todo)
- end
-
- context 'when the html in the string is in the todolist' do
- let(:html_todo) { { 'plural_id' => 'String with many <strong>', 'translations' => [] } }
-
- it 'returns true' do
- expect(entry.plural_id_html_allowed?).to be true
- end
- end
-
- context 'when the html in the string is not in the todolist' do
- let(:html_todo) { { 'plural_id' => 'String with some <strong>', 'translations' => [] } }
-
- it 'returns false' do
- expect(entry.plural_id_html_allowed?).to be false
- end
- end
- end
-
- describe '#translations_html_allowed?' do
- subject(:entry) do
- described_class.new(entry_data: { msgstr: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo)
- end
-
- context 'when the html in the string is in the todolist' do
- let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a <strong>'] } }
-
- it 'returns true' do
- expect(entry.translations_html_allowed?).to be true
- end
- end
-
- context 'when the html in the string is not in the todolist' do
- let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a different <strong>'] } }
-
- it 'returns false' do
- expect(entry.translations_html_allowed?).to be false
- end
- end
-
- context 'when the todolist only has the msgid' do
- let(:html_todo) { { 'plural_id' => nil, 'translations' => nil } }
-
- it 'returns false' do
- expect(entry.translations_html_allowed?).to be false
- end
- end
- end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 38fe2781331..fba32ae0673 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -28,6 +28,7 @@ issues:
- events
- merge_requests_closing_issues
- metrics
+- metric_images
- timelogs
- issuable_severity
- issuable_sla
@@ -85,6 +86,7 @@ label:
- issues
- merge_requests
- priorities
+- epic_board_labels
milestone:
- group
- project
@@ -105,6 +107,7 @@ snippets:
- user_mentions
- snippet_repository
- statistics
+- repository_storage_moves
releases:
- author
- project
@@ -349,6 +352,7 @@ project:
- services
- campfire_service
- confluence_service
+- datadog_service
- discord_service
- drone_ci_service
- emails_on_push_service
@@ -540,6 +544,7 @@ project:
- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
+- compliance_management_frameworks
- metrics_users_starred_dashboards
- alert_management_alerts
- repository_storage_moves
@@ -548,10 +553,13 @@ project:
- build_report_results
- vulnerability_statistic
- vulnerability_historical_statistics
+- vulnerability_remediations
- product_analytics_events
- pipeline_artifacts
- terraform_states
- alert_management_http_integrations
+- exported_protected_branches
+- incident_management_oncall_schedules
award_emoji:
- awardable
- user
@@ -639,6 +647,7 @@ boards:
- lists
- destroyable_lists
- milestone
+- iteration
- board_labels
- board_assignee
- assignee
@@ -648,6 +657,7 @@ boards:
lists:
- user
- milestone
+- iteration
- board
- label
- list_user_preferences
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
index 2eb983cc050..2794acb8980 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -75,12 +75,31 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
before do
setup_import_export_config('group_exports/child_with_no_parent')
+ end
+
+ it 'captures import failures when a child group does not have a valid parent_id' do
+ group_tree_restorer.restore
- expect(group_tree_restorer.restore).to be_falsey
+ expect(group.import_failures.first.exception_message).to eq('Parent group not found')
end
+ end
+
+ context 'when child group creation fails' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group) }
+
+ before do
+ setup_import_export_config('group_exports/child_short_name')
+ end
+
+ it 'captures import failure' do
+ exception_message = 'Validation failed: Group URL is too short (minimum is 2 characters)'
+
+ group_tree_restorer.restore
- it 'fails when a child group does not have a valid parent_id' do
- expect(shared.errors).to include('Parent group not found')
+ expect(group.import_failures.first.exception_message).to eq(exception_message)
end
end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 0db038785d3..75db3167ebc 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do
[
Gitlab::ImportExport::AvatarRestorer,
Gitlab::ImportExport::RepoRestorer,
- Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer,
@@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do
end
end
+ it 'calls RepoRestorer with project and wiki' do
+ wiki_repo_path = File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
+ repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
+ restorer = double(Gitlab::ImportExport::RepoRestorer)
+
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
+
+ expect(restorer).to receive(:restore).and_return(true).twice
+
+ importer.execute
+ end
+
context 'with sample_data_template' do
it 'initializes the Sample::TreeRestorer' do
project.create_or_update_import_data(data: { sample_data: true })
diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
index 0af74dee604..2a5e802bdc5 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonWriter do
describe "#write_relation" do
context "when single relation is serialized" do
- it "appends json in correct file " do
+ it "appends json in correct file" do
relation = "relation"
value = { "key" => "value_1", "key_1" => "value_1" }
subject.write_relation(exportable_path, relation, value)
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 fd3b71deb37..e2bf87bf29f 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -674,10 +674,12 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
end
it 'does not allow setting params that are excluded from import_export settings' do
- project.create_import_data(data: { override_params: { lfs_enabled: true } })
+ original_value = project.lfs_enabled?
+
+ project.create_import_data(data: { override_params: { lfs_enabled: !original_value } })
expect(restored_project_json).to eq(true)
- expect(project.lfs_enabled).to be_falsey
+ expect(project.lfs_enabled).to eq(original_value)
end
it 'overrides project feature access levels' 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 bd9ac6d6697..d3c14b1f8fe 100644
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
@@ -113,12 +113,31 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
include_examples 'logging of relations creation'
end
+ end
+
+ context 'using ndjson reader' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+
+ it_behaves_like 'import project successfully'
+ end
- context 'using ndjson reader' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
- let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+ context 'with invalid relations' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
- it_behaves_like 'import project successfully'
+ it 'logs the invalid relation and its errors' do
+ expect(relation_tree_restorer.shared.logger)
+ .to receive(:warn)
+ .with(
+ error_messages: "Title can't be blank. Title is invalid",
+ message: '[Project/Group Import] Invalid object relation built',
+ relation_class: 'ProjectLabel',
+ relation_index: 0,
+ relation_key: 'labels'
+ ).once
+
+ relation_tree_restorer.restore
end
end
end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index b32ae60fbcc..a6b917457c2 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -5,35 +5,42 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RepoRestorer do
include GitHelpers
+ let_it_be(:project_with_repo) do
+ create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p|
+ p.wiki.create_page('page', 'foobar', :markdown, 'created page')
+ end
+ end
+
+ let!(:project) { create(:project) }
+
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { project.import_export_shared }
+
+ before do
+ allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ bundler.save
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
describe 'bundle a project Git repo' do
- let(:user) { create(:user) }
- let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let!(:project) { create(:project) }
- let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
- before do
- allow_next_instance_of(Gitlab::ImportExport) do |instance|
- allow(instance).to receive(:storage_path).and_return(export_path)
- end
-
- bundler.save
- end
-
after do
- FileUtils.rm_rf(export_path)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
- FileUtils.rm_rf(project.repository.path_to_repo)
- end
+ Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
it 'restores the repo successfully' do
+ expect(project.repository.exists?).to be false
expect(subject.restore).to be_truthy
+
+ expect(project.repository.empty?).to be false
end
context 'when the repository already exists' do
@@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
end
end
+
+ describe 'restore a wiki Git repo' do
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
+
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
+
+ after do
+ Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
+ end
+
+ it 'restores the wiki repo successfully' do
+ expect(project.wiki_repository_exists?).to be false
+
+ subject.restore
+ project.wiki.repository.expire_status_cache
+
+ expect(project.wiki_repository_exists?).to be true
+ end
+
+ describe 'no wiki in the bundle' do
+ let!(:project_without_wiki) { create(:project) }
+
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
+
+ it 'does not creates an empty wiki' do
+ expect(subject.restore).to be true
+ expect(project.wiki_repository_exists?).to be false
+ end
+ 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 b33462b4096..a93ee051ccf 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -26,7 +26,7 @@ Issue:
- weight
- time_estimate
- relative_position
-- service_desk_reply_to
+- external_author
- last_edited_at
- last_edited_by_id
- discussion_locked
@@ -219,6 +219,7 @@ MergeRequestDiff:
- start_commit_sha
- commits_count
- files_count
+- sorted
MergeRequestDiffCommit:
- merge_request_diff_id
- relative_order
@@ -577,6 +578,8 @@ ProjectFeature:
- pages_access_level
- metrics_dashboard_access_level
- requirements_access_level
+- analytics_access_level
+- operations_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
@@ -742,6 +745,7 @@ Board:
- updated_at
- group_id
- milestone_id
+- iteration_id
- weight
- name
- hide_backlog_list
diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
deleted file mode 100644
index 6c80c410d07..00000000000
--- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::ImportExport::WikiRestorer do
- describe 'restore a wiki Git repo' do
- let!(:project_with_wiki) { create(:project, :wiki_repo) }
- let!(:project_without_wiki) { create(:project) }
- let!(:project) { create(:project) }
- let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:shared) { project.import_export_shared }
- let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) }
- let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
- let(:restorer) do
- described_class.new(path_to_bundle: bundle_path,
- shared: shared,
- project: project.wiki,
- wiki_enabled: true)
- end
-
- before do
- allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
-
- bundler.save
- end
-
- after do
- FileUtils.rm_rf(export_path)
- Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
- Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
- end
-
- it 'restores the wiki repo successfully' do
- expect(restorer.restore).to be true
- end
-
- describe "no wiki in the bundle" do
- let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
-
- it 'creates an empty wiki' do
- expect(restorer.restore).to be true
-
- expect(project.wiki_repository_exists?).to be true
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 0dfd8a2ee50..416d651b0de 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -53,7 +53,6 @@ RSpec.describe Gitlab::ImportSources do
bitbucket
bitbucket_server
gitlab
- google_code
fogbugz
gitlab_project
gitea
@@ -70,7 +69,7 @@ RSpec.describe Gitlab::ImportSources do
'bitbucket' => Gitlab::BitbucketImport::Importer,
'bitbucket_server' => Gitlab::BitbucketServerImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
- 'google_code' => Gitlab::GoogleCodeImport::Importer,
+ 'google_code' => nil,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 88f2def34d9..c00b0fdf043 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -34,7 +34,10 @@ RSpec.describe Gitlab::InstrumentationHelper do
:redis_shared_state_calls,
:redis_shared_state_duration_s,
:redis_shared_state_read_bytes,
- :redis_shared_state_write_bytes
+ :redis_shared_state_write_bytes,
+ :db_count,
+ :db_write_count,
+ :db_cached_count
]
expect(described_class.keys).to eq(expected_keys)
@@ -46,10 +49,10 @@ RSpec.describe Gitlab::InstrumentationHelper do
subject { described_class.add_instrumentation_data(payload) }
- it 'adds nothing' do
+ it 'adds only DB counts by default' do
subject
- expect(payload).to eq({})
+ expect(payload).to eq(db_count: 0, db_cached_count: 0, db_write_count: 0)
end
context 'when Gitaly calls are made' do
diff --git a/spec/lib/gitlab/kubernetes/deployment_spec.rb b/spec/lib/gitlab/kubernetes/deployment_spec.rb
new file mode 100644
index 00000000000..2433e854e5b
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/deployment_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Deployment do
+ include KubernetesHelpers
+
+ let(:pods) { {} }
+
+ subject(:deployment) { described_class.new(params, pods: pods) }
+
+ describe '#name' do
+ let(:params) { named(:selected) }
+
+ it { expect(deployment.name).to eq(:selected) }
+ end
+
+ describe '#labels' do
+ let(:params) { make('metadata', 'labels' => :selected) }
+
+ it { expect(deployment.labels).to eq(:selected) }
+ end
+
+ describe '#outdated?' do
+ context 'when outdated' do
+ let(:params) { generation(2, 1, 0) }
+
+ it { expect(deployment.outdated?).to be_truthy }
+ end
+
+ context 'when up to date' do
+ let(:params) { generation(2, 2, 0) }
+
+ it { expect(deployment.outdated?).to be_falsy }
+ end
+
+ context 'when ahead of latest' do
+ let(:params) { generation(1, 2, 0) }
+
+ it { expect(deployment.outdated?).to be_falsy }
+ end
+ end
+
+ describe '#instances' do
+ context 'when unnamed' do
+ let(:pods) do
+ [
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(generation(1, 1, 4)) }
+
+ it 'returns all pods with generated names and pending' do
+ expected = [
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ # When replica count is higher than pods it is considered that pod was not
+ # able to spawn for some reason like limited resources.
+ context 'when number of pods is less than wanted replicas' do
+ let(:wanted_replicas) { 3 }
+ let(:pods) { [kube_pod(name: nil, status: 'Running')] }
+ let(:params) { combine(generation(1, 1, wanted_replicas)) }
+
+ it 'returns not spawned pods as pending and unknown and running' do
+ expected = [
+ { status: 'running', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Running)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'when outdated' do
+ let(:pods) do
+ [
+ kube_pod(status: 'Pending'),
+ kube_pod(name: 'kube-pod1', status: 'Pending'),
+ kube_pod(name: 'kube-pod2', status: 'Pending'),
+ kube_pod(name: 'kube-pod3', status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(named('foo'), generation(1, 0, 4)) }
+
+ it 'returns all instances as named and waiting' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'with pods of each type' do
+ let(:pods) do
+ [
+ kube_pod(status: 'Succeeded'),
+ kube_pod(name: 'kube-pod1', status: 'Running'),
+ kube_pod(name: 'kube-pod2', status: 'Pending'),
+ kube_pod(name: 'kube-pod3', status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(named('foo'), generation(1, 1, 4)) }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'succeeded', pod_name: 'kube-pod', tooltip: 'kube-pod (Succeeded)', track: 'stable', stable: true },
+ { status: 'running', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Running)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'with track label' do
+ let(:pods) { [kube_pod(status: 'Pending')] }
+ let(:labels) { { 'track' => track } }
+ let(:params) { combine(named('foo', labels), generation(1, 0, 1)) }
+
+ context 'when marked as stable' do
+ let(:track) { 'stable' }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'when marked as canary' do
+ let(:track) { 'canary' }
+ let(:pods) { [kube_pod(status: 'Pending', track: track)] }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'canary', stable: false }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+ end
+ end
+
+ def generation(expected, observed, replicas)
+ combine(
+ make('metadata', 'generation' => expected),
+ make('status', 'observedGeneration' => observed),
+ make('spec', 'replicas' => replicas)
+ )
+ end
+
+ def named(name = "foo", labels = {})
+ make('metadata', 'name' => name, 'labels' => labels)
+ end
+
+ def make(key, values = {})
+ hsh = {}
+ hsh[key] = values
+ hsh
+ end
+
+ def combine(*hashes)
+ out = {}
+ hashes.each { |hsh| out = out.deep_merge(hsh) }
+ out
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb
index 9e580cea397..2a3a4cec2b0 100644
--- a/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb
@@ -12,32 +12,14 @@ RSpec.describe Gitlab::Kubernetes::Helm::V2::ResetCommand do
it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
- helm reset
- kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
- kubectl delete clusterrolebinding tiller-admin
+ export HELM_HOST="localhost:44134"
+ tiller -listen ${HELM_HOST} -alsologtostderr &
+ helm init --client-only
+ helm reset --force
EOS
end
end
- context 'when there is a ca.pem file' do
- let(:files) { { 'ca.pem': 'some file content' } }
-
- it_behaves_like 'helm command generator' do
- let(:commands) do
- <<~EOS1.squish + "\n" + <<~EOS2
- helm reset
- --tls
- --tls-ca-cert /data/helm/helm/config/ca.pem
- --tls-cert /data/helm/helm/config/cert.pem
- --tls-key /data/helm/helm/config/key.pem
- EOS1
- kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
- kubectl delete clusterrolebinding tiller-admin
- EOS2
- end
- end
- end
-
describe '#pod_name' do
subject { reset_command.pod_name }
diff --git a/spec/lib/gitlab/kubernetes/ingress_spec.rb b/spec/lib/gitlab/kubernetes/ingress_spec.rb
new file mode 100644
index 00000000000..e4d6bf4086f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/ingress_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Ingress do
+ include KubernetesHelpers
+
+ let(:ingress) { described_class.new(params) }
+
+ describe '#canary?' do
+ subject { ingress.canary? }
+
+ context 'with canary ingress parameters' do
+ let(:params) { canary_metadata }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with stable ingress parameters' do
+ let(:params) { stable_metadata }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#canary_weight' do
+ subject { ingress.canary_weight }
+
+ context 'with canary ingress parameters' do
+ let(:params) { canary_metadata }
+
+ it { is_expected.to eq(50) }
+ end
+
+ context 'with stable ingress parameters' do
+ let(:params) { stable_metadata }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#name' do
+ subject { ingress.name }
+
+ let(:params) { stable_metadata }
+
+ it { is_expected.to eq('production-auto-deploy') }
+ end
+
+ def stable_metadata
+ kube_ingress(track: :stable)
+ end
+
+ def canary_metadata
+ kube_ingress(track: :canary)
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb
new file mode 100644
index 00000000000..3ac97ddc75d
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::RolloutInstances do
+ include KubernetesHelpers
+
+ def setup(deployments_attrs, pods_attrs)
+ deployments = deployments_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Deployment.new(attrs, pods: pods_attrs)
+ end
+
+ pods = pods_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Pod.new(attrs)
+ end
+
+ [deployments, pods]
+ end
+
+ describe '#pod_instances' do
+ it 'returns an instance for a deployment with one pod' do
+ deployments, pods = setup(
+ [kube_deployment(name: 'one', track: 'stable', replicas: 1)],
+ [kube_pod(name: 'one', status: 'Running', track: 'stable')]
+ )
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns a pending pod for a missing replica' do
+ deployments, pods = setup(
+ [kube_deployment(name: 'one', track: 'stable', replicas: 1)],
+ []
+ )
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'Not provided',
+ stable: true,
+ status: 'pending',
+ tooltip: 'Not provided (Pending)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns instances when there are two stable deployments' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: 'stable', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'stable'),
+ kube_pod(name: 'two', status: 'Running', track: 'stable')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }, {
+ pod_name: 'two',
+ stable: true,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns instances for two deployments with different tracks' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'mytrack', replicas: 1),
+ kube_deployment(name: 'two', track: 'othertrack', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'mytrack'),
+ kube_pod(name: 'two', status: 'Running', track: 'othertrack')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: false,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'mytrack'
+ }, {
+ pod_name: 'two',
+ stable: false,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'othertrack'
+ }])
+ end
+
+ it 'sorts stable tracks after canary tracks' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: 'canary', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'stable'),
+ kube_pod(name: 'two', status: 'Running', track: 'canary')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'two',
+ stable: false,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'canary'
+ }, {
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/rollout_status_spec.rb b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb
new file mode 100644
index 00000000000..8ed9fdd799c
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::RolloutStatus do
+ include KubernetesHelpers
+
+ let(:track) { nil }
+ let(:specs) { specs_all_finished }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "canary")
+ end
+
+ let(:ingresses) { [] }
+
+ let(:specs_all_finished) do
+ [
+ kube_deployment(name: 'one'),
+ kube_deployment(name: 'two', track: track)
+ ]
+ end
+
+ let(:specs_half_finished) do
+ [
+ kube_deployment(name: 'one'),
+ kube_deployment(name: 'two', track: track)
+ ]
+ end
+
+ subject(:rollout_status) { described_class.from_deployments(*specs, pods_attrs: pods, ingresses: ingresses) }
+
+ describe '#deployments' do
+ it 'stores the deployments' do
+ expect(rollout_status.deployments).to be_kind_of(Array)
+ expect(rollout_status.deployments.size).to eq(2)
+ expect(rollout_status.deployments.first).to be_kind_of(::Gitlab::Kubernetes::Deployment)
+ end
+ end
+
+ describe '#instances' do
+ context 'for stable track' do
+ let(:track) { "any" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "any")
+ end
+
+ it 'stores the union of deployment instances' do
+ expected = [
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }
+ ]
+
+ expect(rollout_status.instances).to eq(expected)
+ end
+ end
+
+ context 'for stable track' do
+ let(:track) { 'canary' }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track)
+ end
+
+ it 'sorts stable instances last' do
+ expected = [
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }
+ ]
+
+ expect(rollout_status.instances).to eq(expected)
+ end
+ end
+ end
+
+ describe '#completion' do
+ subject { rollout_status.completion }
+
+ context 'when all instances are finished' do
+ let(:track) { 'canary' }
+
+ it { is_expected.to eq(100) }
+ end
+
+ context 'when half of the instances are finished' do
+ let(:track) { "canary" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending")
+ end
+
+ let(:specs) { specs_half_finished }
+
+ it { is_expected.to eq(50) }
+ end
+
+ context 'with one deployment' do
+ it 'sets the completion percentage when a deployment has more running pods than desired' do
+ deployments = [kube_deployment(name: 'one', track: 'one', replicas: 2)]
+ pods = create_pods(name: 'one', track: 'one', count: 3)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+ end
+
+ context 'with two deployments on different tracks' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'one', replicas: 2),
+ kube_deployment(name: 'two', track: 'two', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'one', count: 2) + create_pods(name: 'two', track: 'two', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+ end
+
+ context 'with two deployments that both have track set to "stable"' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 2),
+ kube_deployment(name: 'two', track: 'stable', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: 'stable', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+
+ it 'sets the completion percentage when no pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 3),
+ kube_deployment(name: 'two', track: 'stable', replicas: 7)
+ ]
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: [])
+
+ expect(rollout_status.completion).to eq(0)
+ end
+
+ it 'sets the completion percentage when a quarter of the pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 6),
+ kube_deployment(name: 'two', track: 'stable', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(25)
+ end
+ end
+
+ context 'with two deployments, one with track set to "stable" and one with no track label' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 3),
+ kube_deployment(name: 'two', track: nil, replicas: 3)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 3) + create_pods(name: 'two', track: nil, count: 3)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+
+ it 'sets the completion percentage when no pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: nil, replicas: 1)
+ ]
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: [])
+
+ expect(rollout_status.completion).to eq(0)
+ end
+
+ it 'sets the completion percentage when a third of the pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 2),
+ kube_deployment(name: 'two', track: nil, replicas: 7)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: nil, count: 1)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(33)
+ end
+ end
+ end
+
+ describe '#complete?' do
+ subject { rollout_status.complete? }
+
+ context 'when all instances are finished' do
+ let(:track) { 'canary' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when half of the instances are finished' do
+ let(:track) { "canary" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending")
+ end
+
+ let(:specs) { specs_half_finished }
+
+ it { is_expected.to be_falsy}
+ end
+ end
+
+ describe '#found?' do
+ context 'when the specs are passed' do
+ it { is_expected.to be_found }
+ end
+
+ context 'when list of specs is empty' do
+ let(:specs) { [] }
+
+ it { is_expected.not_to be_found }
+ end
+ end
+
+ describe '.loading' do
+ subject { described_class.loading }
+
+ it { is_expected.to be_loading }
+ end
+
+ describe '#not_found?' do
+ context 'when the specs are passed' do
+ it { is_expected.not_to be_not_found }
+ end
+
+ context 'when list of specs is empty' do
+ let(:specs) { [] }
+
+ it { is_expected.to be_not_found }
+ end
+ end
+
+ describe '#canary_ingress_exists?' do
+ context 'when canary ingress exists' do
+ let(:ingresses) { [kube_ingress(track: :canary)] }
+
+ it 'returns true' do
+ expect(rollout_status.canary_ingress_exists?).to eq(true)
+ end
+ end
+
+ context 'when canary ingress does not exist' do
+ let(:ingresses) { [kube_ingress(track: :stable)] }
+
+ it 'returns false' do
+ expect(rollout_status.canary_ingress_exists?).to eq(false)
+ end
+ end
+ end
+
+ def create_pods(name:, count:, track: nil, status: 'Running' )
+ Array.new(count, kube_pod(name: name, status: status, track: track))
+ end
+end
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
deleted file mode 100644
index b2a53fe1626..00000000000
--- a/spec/lib/gitlab/metrics/background_transaction_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::BackgroundTransaction do
- let(:test_worker_class) { double(:class, name: 'TestWorker') }
- let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) }
-
- before do
- allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
- end
-
- subject { described_class.new(test_worker_class) }
-
- RSpec.shared_examples 'metric with worker labels' do |metric_method|
- it 'measures with correct labels and value' do
- value = 1
- expect(prometheus_metric).to receive(metric_method).with({ controller: 'TestWorker', action: 'perform', feature_category: '' }, value)
-
- subject.send(metric_method, :bau, value)
- end
- end
-
- describe '#label' do
- it 'returns labels based on class name' do
- expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform', feature_category: '')
- end
-
- it 'contains only the labels defined for metrics' do
- expect(subject.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABEL_KEYS)
- end
-
- it 'includes the feature category if there is one' do
- expect(test_worker_class).to receive(:get_feature_category).and_return('source_code_management')
- expect(subject.labels).to include(feature_category: 'source_code_management')
- end
- end
-
- describe '#increment' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, :increment, base_labels: {}) }
-
- it_behaves_like 'metric with worker labels', :increment
- end
-
- describe '#set' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, :set, base_labels: {}) }
-
- it_behaves_like 'metric with worker labels', :set
- end
-
- describe '#observe' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, :observe, base_labels: {}) }
-
- it_behaves_like 'metric with worker labels', :observe
- end
-end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
deleted file mode 100644
index 047d1e5d205..00000000000
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::SidekiqMiddleware do
- let(:middleware) { described_class.new }
- let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } }
-
- describe '#call' do
- it 'tracks the transaction' do
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
-
- expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |transaction|
- expect(transaction).to receive(:set).with(:gitlab_transaction_sidekiq_queue_duration_total, instance_of(Float))
- expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1)
- end
-
- middleware.call(worker, message, :test) do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
- end
- end
-
- it 'prevents database counters from leaking to the next transaction' do
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
-
- 2.times do
- Gitlab::WithRequestStore.with_request_store do
- middleware.call(worker, message, :test) do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
- end
- end
- end
-
- expect(message).to include(db_count: 1, db_write_count: 0, db_cached_count: 0)
- end
-
- it 'tracks the transaction (for messages without `enqueued_at`)', :aggregate_failures do
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
-
- expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
- .with(worker.class)
- .and_call_original
-
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
- .with(:gitlab_transaction_sidekiq_queue_duration_total, instance_of(Float))
-
- middleware.call(worker, {}, :test) { nil }
- end
-
- it 'tracks any raised exceptions', :aggregate_failures, :request_store do
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
-
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .to receive(:add_event).with(:sidekiq_exception)
-
- expect do
- middleware.call(worker, message, :test) do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
- raise RuntimeError
- end
- end.to raise_error(RuntimeError)
-
- expect(message).to include(db_count: 1, db_write_count: 0, db_cached_count: 0)
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index a31686b8061..edcd5b31941 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -18,59 +18,73 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
describe '#sql' do
- describe 'without a current transaction' do
- it 'simply returns' do
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .not_to receive(:increment)
+ shared_examples 'track query in metrics' do
+ before do
+ allow(subscriber).to receive(:current_transaction)
+ .at_least(:once)
+ .and_return(transaction)
+ end
+
+ it 'increments only db count value' do
+ described_class::DB_COUNTERS.each do |counter|
+ prometheus_counter = "gitlab_transaction_#{counter}_total".to_sym
+ if expected_counters[counter] > 0
+ expect(transaction).to receive(:increment).with(prometheus_counter, 1)
+ else
+ expect(transaction).not_to receive(:increment).with(prometheus_counter, 1)
+ end
+ end
subscriber.sql(event)
end
end
- describe 'with a current transaction' do
- shared_examples 'track executed query' do
- before do
- allow(subscriber).to receive(:current_transaction)
- .at_least(:once)
- .and_return(transaction)
- end
+ shared_examples 'track query in RequestStore' do
+ context 'when RequestStore is enabled' do
+ it 'caches db count value', :request_store, :aggregate_failures do
+ subscriber.sql(event)
- it 'increments only db count value' do
described_class::DB_COUNTERS.each do |counter|
- prometheus_counter = "gitlab_transaction_#{counter}_total".to_sym
- if expected_counters[counter] > 0
- expect(transaction).to receive(:increment).with(prometheus_counter, 1)
- else
- expect(transaction).not_to receive(:increment).with(prometheus_counter, 1)
- end
+ expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter]
end
-
- subscriber.sql(event)
end
- context 'when RequestStore is enabled' do
- it 'caches db count value', :request_store, :aggregate_failures do
- subscriber.sql(event)
+ it 'prevents db counters from leaking to the next transaction' do
+ 2.times do
+ Gitlab::WithRequestStore.with_request_store do
+ subscriber.sql(event)
- described_class::DB_COUNTERS.each do |counter|
- expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter]
+ described_class::DB_COUNTERS.each do |counter|
+ expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter]
+ end
end
end
+ end
+ end
+ end
+
+ describe 'without a current transaction' do
+ it 'does not track any metrics' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction)
+ .not_to receive(:increment)
- it 'prevents db counters from leaking to the next transaction' do
- 2.times do
- Gitlab::WithRequestStore.with_request_store do
- subscriber.sql(event)
+ subscriber.sql(event)
+ end
- described_class::DB_COUNTERS.each do |counter|
- expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter]
- end
- end
- end
- end
+ context 'with read query' do
+ let(:expected_counters) do
+ {
+ db_count: 1,
+ db_write_count: 0,
+ db_cached_count: 0
+ }
end
+
+ it_behaves_like 'track query in RequestStore'
end
+ end
+ describe 'with a current transaction' do
it 'observes sql_duration metric' do
expect(subscriber).to receive(:current_transaction)
.at_least(:once)
@@ -96,12 +110,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
}
end
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
context 'with only select' do
let(:payload) { { sql: 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
end
@@ -117,33 +133,38 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
context 'with select for update sql event' do
let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10 FOR UPDATE' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
context 'with common table expression' do
context 'with insert' do
let(:payload) { { sql: 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
end
context 'with delete sql event' do
let(:payload) { { sql: 'DELETE FROM users where id = 10' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
context 'with insert sql event' do
let(:payload) { { sql: 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
context 'with update sql event' do
let(:payload) { { sql: 'UPDATE users SET admin = true WHERE id = 10' } }
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
end
@@ -164,18 +185,20 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
}
end
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
context 'with cached payload name' do
let(:payload) do
{
- sql: 'SELECT * FROM users WHERE id = 10',
- name: 'CACHE'
+ sql: 'SELECT * FROM users WHERE id = 10',
+ name: 'CACHE'
}
end
- it_behaves_like 'track executed query'
+ it_behaves_like 'track query in metrics'
+ it_behaves_like 'track query in RequestStore'
end
end
@@ -227,8 +250,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
it 'skips schema/begin/commit sql commands' do
allow(subscriber).to receive(:current_transaction)
- .at_least(:once)
- .and_return(transaction)
+ .at_least(:once)
+ .and_return(transaction)
expect(transaction).not_to receive(:increment)
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 88293f11149..d4e5a1a94f2 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -20,14 +20,6 @@ RSpec.describe Gitlab::Metrics::Transaction do
end
end
- describe '#thread_cpu_duration' do
- it 'returns the duration of a transaction in seconds' do
- transaction.run { }
-
- expect(transaction.thread_cpu_duration).to be > 0
- end
- end
-
describe '#run' do
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 6903ce53f65..6ee9564ef75 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -80,13 +80,15 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
context 'when request goes to Grape endpoint' do
before do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
- endpoint = double(:endpoint, route: route)
+ endpoint = double(:endpoint, route: route,
+ options: { for: API::Projects, path: [":id/archive"] },
+ namespace: "/projects")
env['api.endpoint'] = endpoint
end
it 'provides labels with the method and path of the route in the grape endpoint' do
- expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: '' })
+ expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: 'projects' })
end
it 'contains only the labels defined for transactions' do
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index 156a440833c..132a0e9ca78 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -57,17 +57,45 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
end
context 'with branch_list_keyset_pagination feature on' do
+ let(:fake_request) { double(url: "#{incoming_api_projects_url}?#{query.to_query}") }
+ let(:branch1) { double 'branch', name: 'branch1' }
+ let(:branch2) { double 'branch', name: 'branch2' }
+ let(:branch3) { double 'branch', name: 'branch3' }
+
before do
stub_feature_flags(branch_list_keyset_pagination: project)
end
context 'without keyset pagination option' do
- it_behaves_like 'offset pagination'
+ context 'when first page is requested' do
+ let(:branches) { [branch1, branch2, branch3] }
+
+ it 'keyset pagination is used with offset headers' do
+ allow(request_context).to receive(:request).and_return(fake_request)
+ allow(project.repository).to receive(:branch_count).and_return(branches.size)
+
+ expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches)
+ expect(request_context).to receive(:header).with('X-Per-Page', '2')
+ expect(request_context).to receive(:header).with('X-Page', '1')
+ expect(request_context).to receive(:header).with('X-Next-Page', '2')
+ expect(request_context).to receive(:header).with('X-Prev-Page', '')
+ expect(request_context).to receive(:header).with('Link', kind_of(String))
+ expect(request_context).to receive(:header).with('X-Total', '3')
+ expect(request_context).to receive(:header).with('X-Total-Pages', '2')
+
+ pager.paginate(finder)
+ end
+ end
+
+ context 'when second page is requested' do
+ let(:base_query) { { per_page: 2, page: 2 } }
+
+ it_behaves_like 'offset pagination'
+ end
end
context 'with keyset pagination option' do
let(:query) { base_query.merge(pagination: 'keyset') }
- let(:fake_request) { double(url: "#{incoming_api_projects_url}?#{query.to_query}") }
before do
allow(request_context).to receive(:request).and_return(fake_request)
@@ -75,8 +103,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
end
context 'when next page could be available' do
- let(:branch1) { double 'branch', name: 'branch1' }
- let(:branch2) { double 'branch', name: 'branch2' }
let(:branches) { [branch1, branch2] }
let(:expected_next_page_link) { %Q(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
@@ -90,7 +116,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
end
context 'when the current page is the last page' do
- let(:branch1) { double 'branch', name: 'branch1' }
let(:branches) { [branch1] }
it 'uses keyset pagination without link headers' do
diff --git a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
new file mode 100644
index 00000000000..a415bad5135
--- /dev/null
+++ b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder do
+ let(:request) { double(url: 'http://localhost') }
+ let(:request_context) { double(header: nil, params: { per_page: 5 }, request: request) }
+
+ subject do
+ described_class.new(
+ request_context: request_context, per_page: 5, page: 2,
+ next_page: 3, prev_page: 1, total: 10, total_pages: 3
+ )
+ end
+
+ describe '#execute' do
+ let(:basic_links) do
+ %{<http://localhost?page=1&per_page=5>; rel="prev", <http://localhost?page=3&per_page=5>; rel="next", <http://localhost?page=1&per_page=5>; rel="first"}
+ end
+
+ let(:last_link) do
+ %{, <http://localhost?page=3&per_page=5>; rel="last"}
+ end
+
+ def expect_basic_headers
+ expect(request_context).to receive(:header).with('X-Per-Page', '5')
+ expect(request_context).to receive(:header).with('X-Page', '2')
+ expect(request_context).to receive(:header).with('X-Next-Page', '3')
+ expect(request_context).to receive(:header).with('X-Prev-Page', '1')
+ expect(request_context).to receive(:header).with('Link', basic_links + last_link)
+ end
+
+ it 'sets headers to request context' do
+ expect_basic_headers
+ expect(request_context).to receive(:header).with('X-Total', '10')
+ expect(request_context).to receive(:header).with('X-Total-Pages', '3')
+
+ subject.execute
+ end
+
+ context 'exclude total headers' do
+ it 'does not set total headers to request context' do
+ expect_basic_headers
+ expect(request_context).not_to receive(:header)
+
+ subject.execute(exclude_total_headers: true)
+ end
+ end
+
+ context 'pass data without counts' do
+ let(:last_link) { '' }
+
+ it 'does not set total headers to request context' do
+ expect_basic_headers
+ expect(request_context).not_to receive(:header)
+
+ subject.execute(data_without_counts: true)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index f320b8a66e8..8e9f7e372c5 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -433,37 +433,85 @@ RSpec.describe Gitlab::PathRegex do
it { is_expected.not_to match('gitlab.git') }
end
- shared_examples 'invalid snippet routes' do
- it { is_expected.not_to match('gitlab-org/gitlab/snippets/1.git') }
- it { is_expected.not_to match('snippets/1.git') }
- it { is_expected.not_to match('gitlab-org/gitlab/snippets/') }
- it { is_expected.not_to match('/gitlab-org/gitlab/snippets/1') }
- it { is_expected.not_to match('gitlab-org/gitlab/snippets/foo') }
- it { is_expected.not_to match('root/snippets/1') }
- it { is_expected.not_to match('/snippets/1') }
- it { is_expected.not_to match('snippets/') }
- it { is_expected.not_to match('snippets/foo') }
- end
+ context 'repository routes' do
+ # Paths that match a known container
+ let_it_be(:container_paths) do
+ [
+ 'gitlab-org',
+ 'gitlab-org/gitlab-test',
+ 'gitlab-org/gitlab-test/snippets/1',
+ 'gitlab-org/gitlab-test/snippets/foo', # ambiguous, we allow creating a sub-group called 'snippets'
+ 'snippets/1'
+ ]
+ end
+
+ # Paths that never match a container
+ let_it_be(:invalid_paths) do
+ [
+ 'gitlab/',
+ '/gitlab',
+ 'gitlab/foo/',
+ '?gitlab',
+ 'git lab',
+ '/snippets/1',
+ 'snippets/foo',
+ 'gitlab-org/gitlab/snippets/'
+ ]
+ end
+
+ let_it_be(:git_paths) { container_paths.map { |path| path + '.git' } }
+ let_it_be(:snippet_paths) { container_paths.grep(%r{snippets/\d}) }
+ let_it_be(:wiki_git_paths) { (container_paths - snippet_paths).map { |path| path + '.wiki.git' } }
+ let_it_be(:invalid_git_paths) { invalid_paths.map { |path| path + '.git' } }
+
+ def expect_route_match(paths)
+ paths.each { |path| is_expected.to match(path) }
+ end
+
+ def expect_no_route_match(paths)
+ paths.each { |path| is_expected.not_to match(path) }
+ end
+
+ describe '.repository_route_regex' do
+ subject { %r{\A#{described_class.repository_route_regex}\z} }
+
+ it 'matches the expected paths' do
+ expect_route_match(container_paths)
+ expect_no_route_match(invalid_paths + git_paths)
+ end
+ end
- describe '.full_snippets_repository_path_regex' do
- subject { described_class.full_snippets_repository_path_regex }
+ describe '.repository_git_route_regex' do
+ subject { %r{\A#{described_class.repository_git_route_regex}\z} }
- it { is_expected.to match('gitlab-org/gitlab/snippets/1') }
- it { is_expected.to match('snippets/1') }
+ it 'matches the expected paths' do
+ expect_route_match(git_paths + wiki_git_paths)
+ expect_no_route_match(container_paths + invalid_paths + invalid_git_paths)
+ end
+ end
- it_behaves_like 'invalid snippet routes'
- end
+ describe '.repository_wiki_git_route_regex' do
+ subject { %r{\A#{described_class.repository_wiki_git_route_regex}\z} }
- describe '.personal_and_project_snippets_path_regex' do
- subject { %r{\A#{described_class.personal_and_project_snippets_path_regex}\z} }
+ it 'matches the expected paths' do
+ expect_route_match(wiki_git_paths)
+ expect_no_route_match(git_paths + invalid_git_paths)
+ end
- it { is_expected.to match('gitlab-org/gitlab/snippets') }
- it { is_expected.to match('snippets') }
+ it { is_expected.not_to match('snippets/1.wiki.git') }
+ end
- it { is_expected.not_to match('gitlab-org/gitlab/snippets/1') }
- it { is_expected.not_to match('snippets/1') }
+ describe '.full_snippets_repository_path_regex' do
+ subject { described_class.full_snippets_repository_path_regex }
- it_behaves_like 'invalid snippet routes'
+ it 'matches the expected paths' do
+ expect_route_match(snippet_paths)
+ expect_no_route_match(container_paths - snippet_paths + git_paths + invalid_paths)
+ end
+
+ it { is_expected.not_to match('root/snippets/1') }
+ it { is_expected.not_to match('gitlab-org/gitlab-test/snippets/foo') }
+ end
end
describe '.container_image_regex' do
diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb
new file mode 100644
index 00000000000..bbc8b0d67e0
--- /dev/null
+++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do
+ include ExclusiveLeaseHelpers
+
+ let(:peek_adapter) do
+ Class.new do
+ prepend Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled
+
+ def initialize(client)
+ @client = client
+ end
+
+ def save(id)
+ # no-op
+ end
+ end
+ end
+
+ describe '#save' do
+ let(:client) { double }
+ let(:uuid) { 'foo' }
+
+ before do
+ allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
+ end
+
+ it 'stores request id and enqueues stats job' do
+ expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid)
+ expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid)
+ expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
+
+ peek_adapter.new(client).save('foo')
+ end
+
+ context 'when performance_bar_stats is disabled' do
+ before do
+ stub_feature_flags(performance_bar_stats: false)
+ end
+
+ it 'ignores stats processing for the request' do
+ expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in)
+ expect(client).not_to receive(:sadd)
+
+ peek_adapter.new(client).save('foo')
+ end
+ end
+
+ context 'when exclusive lease has been already taken' do
+ before do
+ stub_exclusive_lease_taken(GitlabPerformanceBarStatsWorker::LEASE_KEY)
+ end
+
+ it 'stores request id but does not enqueue any job' do
+ expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in)
+ expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
+
+ peek_adapter.new(client).save('foo')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb
new file mode 100644
index 00000000000..c34c6f7b31f
--- /dev/null
+++ b/spec/lib/gitlab/performance_bar/stats_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::PerformanceBar::Stats do
+ describe '#process' do
+ let(:request) { fixture_file('lib/gitlab/performance_bar/peek_data.json') }
+ let(:redis) { double(Gitlab::Redis::SharedState) }
+ let(:logger) { double(Gitlab::PerformanceBar::Logger) }
+ let(:request_id) { 'foo' }
+ let(:stats) { described_class.new(redis) }
+
+ describe '#process' do
+ subject(:process) { stats.process(request_id) }
+
+ before do
+ allow(stats).to receive(:logger).and_return(logger)
+ end
+
+ it 'logs each SQL query including its duration' do
+ allow(redis).to receive(:get).and_return(request)
+
+ expect(logger).to receive(:info)
+ .with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb',
+ filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql })
+ expect(logger).to receive(:info)
+ .with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb',
+ filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice
+
+ subject
+ end
+
+ it 'logs an error when the request could not be processed' do
+ allow(redis).to receive(:get).and_return(nil)
+
+ expect(logger).to receive(:error).with(message: anything)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb
new file mode 100644
index 00000000000..aa604dfab71
--- /dev/null
+++ b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RackAttack::UserAllowlist do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(input)}
+
+ where(:input, :elements) do
+ nil | []
+ '' | []
+ '123' | [123]
+ '123,456' | [123, 456]
+ '123,foobar, 456,' | [123, 456]
+ end
+
+ with_them do
+ it 'has the expected elements' do
+ expect(subject).to contain_exactly(*elements)
+ end
+
+ it 'implements empty?' do
+ expect(subject.empty?).to eq(elements.empty?)
+ end
+
+ it 'implements include?' do
+ unless elements.empty?
+ expect(subject).to include(elements.first)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb
new file mode 100644
index 00000000000..d72863b0103
--- /dev/null
+++ b/spec/lib/gitlab/rack_attack_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RackAttack, :aggregate_failures do
+ describe '.configure' do
+ let(:fake_rack_attack) { class_double("Rack::Attack") }
+ let(:fake_rack_attack_request) { class_double("Rack::Attack::Request") }
+
+ let(:throttles) do
+ {
+ throttle_unauthenticated: Gitlab::Throttle.unauthenticated_options,
+ throttle_authenticated_api: Gitlab::Throttle.authenticated_api_options,
+ throttle_product_analytics_collector: { limit: 100, period: 60 },
+ throttle_unauthenticated_protected_paths: Gitlab::Throttle.unauthenticated_options,
+ throttle_authenticated_protected_paths_api: Gitlab::Throttle.authenticated_api_options,
+ throttle_authenticated_protected_paths_web: Gitlab::Throttle.authenticated_web_options
+ }
+ end
+
+ before do
+ stub_const("Rack::Attack", fake_rack_attack)
+ stub_const("Rack::Attack::Request", fake_rack_attack_request)
+
+ # Expect rather than just allow, because this is actually fairly important functionality
+ expect(fake_rack_attack).to receive(:throttled_response_retry_after_header=).with(true)
+ allow(fake_rack_attack).to receive(:throttle)
+ allow(fake_rack_attack).to receive(:track)
+ allow(fake_rack_attack).to receive(:safelist)
+ allow(fake_rack_attack).to receive(:blocklist)
+ end
+
+ it 'extends the request class' do
+ described_class.configure(fake_rack_attack)
+
+ expect(fake_rack_attack_request).to include(described_class::Request)
+ end
+
+ it 'configures the safelist' do
+ described_class.configure(fake_rack_attack)
+
+ expect(fake_rack_attack).to have_received(:safelist).with('throttle_bypass_header')
+ end
+
+ it 'configures throttles if no dry-run was configured' do
+ described_class.configure(fake_rack_attack)
+
+ throttles.each do |throttle, options|
+ expect(fake_rack_attack).to have_received(:throttle).with(throttle.to_s, options)
+ end
+ end
+
+ it 'configures tracks if dry-run was configured for all throttles' do
+ stub_env('GITLAB_THROTTLE_DRY_RUN', '*')
+
+ described_class.configure(fake_rack_attack)
+
+ throttles.each do |throttle, options|
+ expect(fake_rack_attack).to have_received(:track).with(throttle.to_s, options)
+ end
+ expect(fake_rack_attack).not_to have_received(:throttle)
+ end
+
+ it 'configures tracks and throttles with a selected set of dry-runs' do
+ dry_run_throttles = throttles.each_key.first(2)
+ regular_throttles = throttles.keys[2..-1]
+ stub_env('GITLAB_THROTTLE_DRY_RUN', dry_run_throttles.join(','))
+
+ described_class.configure(fake_rack_attack)
+
+ dry_run_throttles.each do |throttle|
+ expect(fake_rack_attack).to have_received(:track).with(throttle.to_s, throttles[throttle])
+ end
+ regular_throttles.each do |throttle|
+ expect(fake_rack_attack).to have_received(:throttle).with(throttle.to_s, throttles[throttle])
+ end
+ end
+
+ context 'user allowlist' do
+ subject { described_class.user_allowlist }
+
+ it 'is empty' do
+ described_class.configure(fake_rack_attack)
+
+ expect(subject).to be_empty
+ end
+
+ it 'reflects GITLAB_THROTTLE_USER_ALLOWLIST' do
+ stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', '123,456')
+ described_class.configure(fake_rack_attack)
+
+ expect(subject).to contain_exactly(123, 456)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sample_data_template_spec.rb b/spec/lib/gitlab/sample_data_template_spec.rb
index 7d0d415b3af..09ca41fcfc2 100644
--- a/spec/lib/gitlab/sample_data_template_spec.rb
+++ b/spec/lib/gitlab/sample_data_template_spec.rb
@@ -6,8 +6,7 @@ RSpec.describe Gitlab::SampleDataTemplate do
describe '.all' do
it 'returns all templates' do
expected = %w[
- basic
- serenity_valley
+ sample
]
expect(described_class.all).to be_an(Array)
@@ -19,7 +18,7 @@ RSpec.describe Gitlab::SampleDataTemplate do
subject { described_class.find(query) }
context 'when there is a match' do
- let(:query) { :basic }
+ let(:query) { :sample }
it { is_expected.to be_a(described_class) }
end
diff --git a/spec/lib/gitlab/setup_helper/workhorse_spec.rb b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
new file mode 100644
index 00000000000..aa9b4595799
--- /dev/null
+++ b/spec/lib/gitlab/setup_helper/workhorse_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SetupHelper::Workhorse do
+ describe '.make' do
+ subject { described_class.make }
+
+ context 'when there is a gmake' do
+ it 'returns gmake' do
+ expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
+
+ expect(subject).to eq 'gmake'
+ end
+ end
+
+ context 'when there is no gmake' do
+ it 'returns make' do
+ expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 1])
+
+ expect(subject).to eq 'make'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/lib/gitlab/sidekiq_cluster_spec.rb
index 5517abe1010..3c6ea054968 100644
--- a/spec/lib/gitlab/sidekiq_cluster_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster_spec.rb
@@ -123,6 +123,14 @@ RSpec.describe Gitlab::SidekiqCluster do
end
end
+ describe '.count_by_queue' do
+ it 'tallies the queue counts' do
+ queues = [%w(foo), %w(bar baz), %w(foo)]
+
+ expect(described_class.count_by_queue(queues)).to eq(%w(foo) => 2, %w(bar baz) => 1)
+ end
+ end
+
describe '.concurrency' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
new file mode 100644
index 00000000000..96fef88de4e
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
+ describe '.handler' do
+ context 'when the job class has worker attributes' do
+ let(:test_worker) do
+ Class.new do
+ include WorkerAttributes
+
+ urgency :low
+ worker_has_external_dependencies!
+ worker_resource_boundary :cpu
+ feature_category :users
+ end
+ end
+
+ before do
+ stub_const('TestWorker', test_worker)
+ end
+
+ it 'uses the attributes from the worker' do
+ expect(described_class.counter)
+ .to receive(:increment)
+ .with(queue: 'test_queue', worker: 'TestWorker',
+ urgency: 'low', external_dependencies: 'yes',
+ feature_category: 'users', boundary: 'cpu')
+
+ described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
+ end
+ end
+
+ context 'when the job class does not have worker attributes' do
+ before do
+ stub_const('TestWorker', Class.new)
+ end
+
+ it 'uses blank attributes' do
+ expect(described_class.counter)
+ .to receive(:increment)
+ .with(queue: 'test_queue', worker: 'TestWorker',
+ urgency: '', external_dependencies: 'no',
+ feature_category: '', boundary: '')
+
+ described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 8ef61d4eae9..0285467ecab 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -131,31 +131,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
- describe '#droppable?' do
- where(:idempotent, :prevent_deduplication) do
- # [true, false].repeated_permutation(2)
- [[true, true],
- [true, false],
- [false, true],
- [false, false]]
- end
-
- with_them do
- before do
- allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent)
- stub_feature_flags("disable_#{queue}_deduplication" => prevent_deduplication)
- end
-
- it 'is droppable when all conditions are met' do
- if idempotent && !prevent_deduplication
- expect(duplicate_job).to be_droppable
- else
- expect(duplicate_job).not_to be_droppable
- end
- end
- end
- end
-
describe '#scheduled_at' do
let(:scheduled_at) { 42 }
let(:job) do
@@ -181,6 +156,46 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
+ describe '#idempotent?' do
+ context 'when worker class does not exist' do
+ let(:job) { { 'class' => '' } }
+
+ it 'returns false' do
+ expect(duplicate_job).not_to be_idempotent
+ end
+ end
+
+ context 'when worker class does not respond to #idempotent?' do
+ before do
+ stub_const('AuthorizedProjectsWorker', Class.new)
+ end
+
+ it 'returns false' do
+ expect(duplicate_job).not_to be_idempotent
+ end
+ end
+
+ context 'when worker class is not idempotent' do
+ before do
+ allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(duplicate_job).not_to be_idempotent
+ end
+ end
+
+ context 'when worker class is idempotent' do
+ before do
+ allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(duplicate_job).to be_idempotent
+ end
+ end
+ end
+
def set_idempotency_key(key, value = '1')
Sidekiq.redis { |r| r.set(key, value) }
end
diff --git a/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb b/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb
new file mode 100644
index 00000000000..63e2e930acd
--- /dev/null
+++ b/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Tracking::Destinations::ProductAnalytics do
+ let(:emitter) { SnowplowTracker::Emitter.new('localhost', buffer_size: 1) }
+ let(:tracker) { SnowplowTracker::Tracker.new(emitter, SnowplowTracker::Subject.new, 'namespace', 'app_id') }
+
+ describe '#event' do
+ shared_examples 'does not send an event' do
+ it 'does not send an event' do
+ expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event)
+
+ subject.event(allowed_category, allowed_action)
+ end
+ end
+
+ let(:allowed_category) { 'epics' }
+ let(:allowed_action) { 'promote' }
+ let(:self_monitoring_project) { create(:project) }
+
+ before do
+ stub_feature_flags(product_analytics_tracking: true)
+ stub_application_setting(self_monitoring_project_id: self_monitoring_project.id)
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ context 'with allowed event' do
+ it 'sends an event to Product Analytics snowplow collector' do
+ expect(SnowplowTracker::AsyncEmitter)
+ .to receive(:new)
+ .with(ProductAnalytics::Tracker::COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol)
+ .and_return(emitter)
+
+ expect(SnowplowTracker::Tracker)
+ .to receive(:new)
+ .with(emitter, an_instance_of(SnowplowTracker::Subject), Gitlab::Tracking::SNOWPLOW_NAMESPACE, self_monitoring_project.id.to_s)
+ .and_return(tracker)
+
+ freeze_time do
+ expect(tracker)
+ .to receive(:track_struct_event)
+ .with(allowed_category, allowed_action, 'label', 'property', 1.5, nil, (Time.now.to_f * 1000).to_i)
+
+ subject.event(allowed_category, allowed_action, label: 'label', property: 'property', value: 1.5)
+ end
+ end
+ end
+
+ context 'with non-allowed event' do
+ it 'does not send an event' do
+ expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event)
+
+ subject.event('category', 'action')
+ subject.event(allowed_category, 'action')
+ subject.event('category', allowed_action)
+ end
+ end
+
+ context 'when self-monitoring project does not exist' do
+ before do
+ stub_application_setting(self_monitoring_project_id: nil)
+ end
+
+ include_examples 'does not send an event'
+ end
+
+ context 'when product_analytics_tracking FF is disabled' do
+ before do
+ stub_feature_flags(product_analytics_tracking: false)
+ end
+
+ include_examples 'does not send an event'
+ end
+
+ context 'when usage ping is disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+ end
+
+ include_examples 'does not send an event'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
index ee63eb6de04..0e8647ad78a 100644
--- a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
+++ b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do
it 'sends event to tracker' do
allow(tracker).to receive(:track_self_describing_event).and_call_original
- subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar')
+ subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
expect(tracker).to have_received(:track_self_describing_event) do |event, context, timestamp|
expect(event.to_json[:schema]).to eq('iglu:com.gitlab/foo/jsonschema/1-0-0')
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do
it 'does not send event to tracker' do
expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_self_describing_event)
- subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar')
+ subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 805bd92fd43..57882de0974 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -36,6 +36,11 @@ RSpec.describe Gitlab::Tracking do
end
describe '.event' do
+ before do
+ allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow).to receive(:event)
+ allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event)
+ end
+
it 'delegates to snowplow destination' do
expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:event)
@@ -43,15 +48,23 @@ RSpec.describe Gitlab::Tracking do
described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5)
end
+
+ it 'delegates to ProductAnalytics destination' do
+ expect_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics)
+ .to receive(:event)
+ .with('category', 'action', label: 'label', property: 'property', value: 1.5, context: nil)
+
+ described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5)
+ end
end
describe '.self_describing_event' do
it 'delegates to snowplow destination' do
expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:self_describing_event)
- .with('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' }, context: nil)
+ .with('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }, context: nil)
- described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar')
+ described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
index e9fb5346eae..c0deb2aa00c 100644
--- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'aggregated metrics' do
Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event)
end
- failure_message do
+ failure_message do |event|
"Event with name: `#{event}` can not be found within `#{Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH}`"
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb
new file mode 100644
index 00000000000..4a31191d75f
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::BaseCounter do
+ describe '.fetch_supported_event' do
+ subject { described_class.fetch_supported_event(event_name) }
+
+ let(:event_name) { 'generic_event' }
+ let(:prefix) { 'generic' }
+ let(:known_events) { %w[event another_event] }
+
+ before do
+ allow(described_class).to receive(:prefix) { prefix }
+ allow(described_class).to receive(:known_events) { known_events }
+ end
+
+ it 'returns the matching event' do
+ is_expected.to eq 'event'
+ end
+
+ context 'when event is unknown' do
+ let(:event_name) { 'generic_unknown_event' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when prefix does not match the event name' do
+ let(:prefix) { 'special' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
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 f2c1d8718d7..82db3d94493 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
@@ -74,7 +74,19 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
end
- it 'can return the count of actions per user deduplicated ' do
+ context 'for SSE edit actions' do
+ it_behaves_like 'tracks and counts action' do
+ def track_action(params)
+ described_class.track_sse_edit_action(**params)
+ end
+
+ def count_unique(params)
+ described_class.count_sse_edit_actions(**params)
+ end
+ end
+ end
+
+ it 'can return the count of actions per user deduplicated' do
described_class.track_web_ide_edit_action(author: user1)
described_class.track_snippet_editor_edit_action(author: user1)
described_class.track_sfe_edit_action(author: user1)
diff --git a/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb
new file mode 100644
index 00000000000..d018100b041
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::GuestPackageEventCounter, :clean_gitlab_redis_shared_state do
+ shared_examples_for 'usage counter with totals' do |counter|
+ it 'increments counter and returns total count' do
+ expect(described_class.read(counter)).to eq(0)
+
+ 2.times { described_class.count(counter) }
+
+ expect(described_class.read(counter)).to eq(2)
+ end
+ end
+
+ it 'includes the right events' do
+ expect(described_class::KNOWN_EVENTS.size).to eq 33
+ end
+
+ described_class::KNOWN_EVENTS.each do |event|
+ it_behaves_like 'usage counter with totals', event
+ end
+
+ describe '.fetch_supported_event' do
+ subject { described_class.fetch_supported_event(event_name) }
+
+ let(:event_name) { 'package_guest_i_package_composer_guest_push' }
+
+ it { is_expected.to eq 'i_package_composer_guest_push' }
+ 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 93704a39555..70ee9871486 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
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'search',
'source_code',
'incident_management',
+ 'incident_management_alerts',
'testing',
'issues_edit',
'ci_secrets_management',
@@ -43,7 +44,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'golang_packages',
'debian_packages',
'container_packages',
- 'tag_packages'
+ 'tag_packages',
+ 'snippets'
)
end
end
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 803eff05efe..bf43f7552e6 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
@@ -118,6 +118,16 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
end
+ context 'for Issue cloned actions' do
+ it_behaves_like 'a tracked issue edit event' do
+ let(:action) { described_class::ISSUE_CLONED }
+
+ def track_action(params)
+ described_class.track_issue_cloned_action(**params)
+ end
+ end
+ end
+
context 'for Issue relate actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_RELATED }
diff --git a/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb
index b55e20ba555..17188a75ccb 100644
--- a/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb
@@ -20,4 +20,12 @@ RSpec.describe Gitlab::UsageDataCounters::SearchCounter, :clean_gitlab_redis_sha
context 'navbar_searches counter' do
it_behaves_like 'usage counter with totals', :navbar_searches
end
+
+ describe '.fetch_supported_event' do
+ subject { described_class.fetch_supported_event(event_name) }
+
+ let(:event_name) { 'all_searches' }
+
+ it { is_expected.to eq 'all_searches' }
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters_spec.rb b/spec/lib/gitlab/usage_data_counters_spec.rb
new file mode 100644
index 00000000000..379a2cb778d
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters do
+ describe '.usage_data_counters' do
+ subject { described_class.counters }
+
+ it { is_expected.to all(respond_to :totals) }
+ it { is_expected.to all(respond_to :fallback_totals) }
+ end
+
+ describe '.count' do
+ subject { described_class.count(event_name) }
+
+ let(:event_name) { 'static_site_editor_views' }
+
+ it 'increases a view counter' do
+ expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to receive(:count).with('views')
+
+ subject
+ end
+
+ context 'when event_name is not defined' do
+ let(:event_name) { 'unknown' }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::UsageDataCounters::UnknownEvent)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index d305b2c5bfe..c2d96369425 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -224,7 +224,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 2
},
projects_imported: {
- total: 20,
+ total: 2,
gitlab_project: 2,
gitlab: 2,
github: 2,
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 1
},
projects_imported: {
- total: 10,
+ total: 1,
gitlab_project: 1,
gitlab: 1,
github: 1,
@@ -456,6 +456,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects]).to eq(4)
expect(count_data[:projects_asana_active]).to eq(0)
expect(count_data[:projects_prometheus_active]).to eq(1)
+ expect(count_data[:projects_jenkins_active]).to eq(1)
expect(count_data[:projects_jira_active]).to eq(4)
expect(count_data[:projects_jira_server_active]).to eq(2)
expect(count_data[:projects_jira_cloud_active]).to eq(2)
@@ -653,6 +654,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
it { is_expected.to include(:kubernetes_agent_gitops_sync) }
it { is_expected.to include(:static_site_editor_views) }
+ it { is_expected.to include(:package_guest_i_package_composer_guest_pull) }
end
describe '.usage_data_counters' do
@@ -840,24 +842,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe '.cycle_analytics_usage_data' do
- subject { described_class.cycle_analytics_usage_data }
-
- it 'works when queries time out in new' do
- allow(Gitlab::CycleAnalytics::UsageData)
- .to receive(:new).and_raise(ActiveRecord::StatementInvalid.new(''))
-
- expect { subject }.not_to raise_error
- end
-
- it 'works when queries time out in to_json' do
- allow_any_instance_of(Gitlab::CycleAnalytics::UsageData)
- .to receive(:to_json).and_raise(ActiveRecord::StatementInvalid.new(''))
-
- expect { subject }.not_to raise_error
- end
- end
-
describe '.ingress_modsecurity_usage' do
subject { described_class.ingress_modsecurity_usage }
@@ -1054,6 +1038,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
end
+
+ describe ".system_usage_data_settings" do
+ subject { described_class.system_usage_data_settings }
+
+ it 'gathers settings usage data', :aggregate_failures do
+ expect(subject[:settings][:ldap_encrypted_secrets_enabled]).to eq(Gitlab::Auth::Ldap::Config.encrypted_secrets.active?)
+ end
+ end
end
describe '.merge_requests_users', :clean_gitlab_redis_shared_state do
@@ -1122,6 +1114,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
counter.track_web_ide_edit_action(author: user3, time: time - 3.days)
counter.track_snippet_editor_edit_action(author: user3)
+
+ counter.track_sse_edit_action(author: user1)
+ counter.track_sse_edit_action(author: user1)
+ counter.track_sse_edit_action(author: user2)
+ counter.track_sse_edit_action(author: user3)
+ counter.track_sse_edit_action(author: user2, time: time - 3.days)
end
it 'returns the distinct count of user actions within the specified time period' do
@@ -1134,7 +1132,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
action_monthly_active_users_web_ide_edit: 2,
action_monthly_active_users_sfe_edit: 2,
action_monthly_active_users_snippet_editor_edit: 2,
- action_monthly_active_users_ide_edit: 3
+ action_monthly_active_users_ide_edit: 3,
+ action_monthly_active_users_sse_edit: 3
}
)
end
@@ -1235,7 +1234,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.redis_hll_counters }
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
- let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management] }
+ let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets] }
it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)
@@ -1256,45 +1255,21 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe 'aggregated_metrics' do
- shared_examples 'aggregated_metrics_for_time_range' do
- context 'with product_analytics_aggregated_metrics feature flag on' do
- before do
- stub_feature_flags(product_analytics_aggregated_metrics: true)
- end
+ describe '.aggregated_metrics_weekly' do
+ subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly }
- it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
- expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(aggregated_metrics_data_method).and_return(global_search_gmau: 123)
- expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
- end
- end
-
- context 'with product_analytics_aggregated_metrics feature flag off' do
- before do
- stub_feature_flags(product_analytics_aggregated_metrics: false)
- end
-
- it 'returns empty hash', :aggregate_failures do
- expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(aggregated_metrics_data_method)
- expect(aggregated_metrics_payload).to be {}
- end
- end
+ it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_weekly_data).and_return(global_search_gmau: 123)
+ expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
+ end
- describe '.aggregated_metrics_weekly' do
- subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly }
-
- let(:aggregated_metrics_data_method) { :aggregated_metrics_weekly_data }
-
- it_behaves_like 'aggregated_metrics_for_time_range'
- end
-
- describe '.aggregated_metrics_monthly' do
- subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
-
- let(:aggregated_metrics_data_method) { :aggregated_metrics_monthly_data }
+ describe '.aggregated_metrics_monthly' do
+ subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
- it_behaves_like 'aggregated_metrics_for_time_range'
+ it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_monthly_data).and_return(global_search_gmau: 123)
+ expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
@@ -1323,7 +1298,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
context 'and product_analytics FF is enabled for it' do
before do
- stub_feature_flags(product_analytics: project)
+ stub_feature_flags(product_analytics_tracking: true)
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: 2.days.ago)
@@ -1339,7 +1314,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
context 'and product_analytics FF is disabled' do
before do
- stub_feature_flags(product_analytics: false)
+ stub_feature_flags(product_analytics_tracking: false)
end
it 'returns an empty hash' do
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index d6b1e3b2d4b..748a8336a25 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -310,4 +310,24 @@ RSpec.describe Gitlab::UserAccess do
end
end
end
+
+ describe '#can_push_for_ref?' do
+ let(:ref) { 'test_ref' }
+
+ context 'when user cannot push_code to a project repository (eg. as a guest)' do
+ it 'is false' do
+ project.add_user(user, :guest)
+
+ expect(access.can_push_for_ref?(ref)).to be_falsey
+ end
+ end
+
+ context 'when user can push_code to a project repository (eg. as a developer)' do
+ it 'is true' do
+ project.add_user(user, :developer)
+
+ expect(access.can_push_for_ref?(ref)).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 9c0dc69ccd1..521d6584a20 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -37,6 +37,36 @@ RSpec.describe Gitlab::Utils::UsageData do
end
end
+ describe '#estimate_batch_distinct_count' do
+ let(:relation) { double(:relation) }
+
+ it 'delegates counting to counter class instance' do
+ expect_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter, relation, 'column') do |instance|
+ expect(instance).to receive(:estimate_distinct_count)
+ .with(batch_size: nil, start: nil, finish: nil)
+ .and_return(5)
+ end
+
+ expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5)
+ end
+
+ it 'returns default fallback value when counting fails due to database error' do
+ stub_const("Gitlab::Utils::UsageData::FALLBACK", 15)
+ allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(ActiveRecord::StatementInvalid.new(''))
+
+ expect(described_class.estimate_batch_distinct_count(relation)).to eq(15)
+ end
+
+ it 'logs error and returns DISTRIBUTED_HLL_FALLBACK value when counting raises any error', :aggregate_failures do
+ error = StandardError.new('')
+ stub_const("Gitlab::Utils::UsageData::DISTRIBUTED_HLL_FALLBACK", 15)
+ allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(error)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error)
+ expect(described_class.estimate_batch_distinct_count(relation)).to eq(15)
+ end
+ end
+
describe '#sum' do
let(:relation) { double(:relation) }
diff --git a/spec/lib/gitlab/uuid_spec.rb b/spec/lib/gitlab/uuid_spec.rb
new file mode 100644
index 00000000000..a2e28f5a24d
--- /dev/null
+++ b/spec/lib/gitlab/uuid_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UUID do
+ let_it_be(:name) { "GitLab" }
+
+ describe '.v5' do
+ subject { described_class.v5(name) }
+
+ before do
+ # This is necessary to clear memoization for testing different environments
+ described_class.instance_variable_set(:@default_namespace_id, nil)
+ end
+
+ context 'in development' do
+ let_it_be(:development_proper_uuid) { "5b593e54-90f5-504b-8805-5394a4d14b94" }
+
+ before do
+ allow(Rails).to receive(:env).and_return(:development)
+ end
+
+ it { is_expected.to eq(development_proper_uuid) }
+ end
+
+ context 'in test' do
+ let_it_be(:test_proper_uuid) { "5b593e54-90f5-504b-8805-5394a4d14b94" }
+
+ it { is_expected.to eq(test_proper_uuid) }
+ end
+
+ context 'in staging' do
+ let_it_be(:staging_proper_uuid) { "dd190b37-7754-5c7c-80a0-85621a5823ad" }
+
+ before do
+ allow(Rails).to receive(:env).and_return(:staging)
+ end
+
+ it { is_expected.to eq(staging_proper_uuid) }
+ end
+
+ context 'in production' do
+ let_it_be(:production_proper_uuid) { "4961388b-9d8e-5da0-a499-3ef5da58daf0" }
+
+ before do
+ allow(Rails).to receive(:env).and_return(:production)
+ end
+
+ it { is_expected.to eq(production_proper_uuid) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/webpack/manifest_spec.rb b/spec/lib/gitlab/webpack/manifest_spec.rb
index 1427bdd7d4f..08b4774dd67 100644
--- a/spec/lib/gitlab/webpack/manifest_spec.rb
+++ b/spec/lib/gitlab/webpack/manifest_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::Webpack::Manifest do
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)
+ stub_file_read(::Rails.root.join("manifest_output/my_manifest.json"), content: manifest)
end
describe ".asset_paths" do
@@ -105,7 +105,7 @@ RSpec.describe Gitlab::Webpack::Manifest do
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)
+ stub_file_read(::Rails.root.join("manifest_output/broken.json"), error: Errno::ENOENT)
expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
end
end
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb
index e332647cf8a..ed668c52a0e 100644
--- a/spec/lib/gitlab_danger_spec.rb
+++ b/spec/lib/gitlab_danger_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe GitlabDanger do
let(:gitlab_danger_helper) { nil }
@@ -9,7 +9,7 @@ RSpec.describe GitlabDanger do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
- expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, product_analytics, utility_css, pajamas')
+ expect(described_class.local_warning_message).to eq("==> Only the following Danger rules can be run locally: #{described_class::LOCAL_RULES.join(', ')}")
end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 7c2758bf27e..e1b8323eb8e 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -26,18 +26,17 @@ RSpec.describe Gitlab do
end
it 'returns the actual Git revision' do
- expect(File).to receive(:read)
- .with(described_class.root.join('REVISION'))
- .and_return("abc123\n")
+ expect_file_read(described_class.root.join('REVISION'), content: "abc123\n")
expect(described_class.revision).to eq('abc123')
end
it 'memoizes the revision' do
+ stub_file_read(described_class.root.join('REVISION'), content: "abc123\n")
+
expect(File).to receive(:read)
- .once
- .with(described_class.root.join('REVISION'))
- .and_return("abc123\n")
+ .once
+ .with(described_class.root.join('REVISION'))
2.times { described_class.revision }
end
@@ -330,4 +329,24 @@ RSpec.describe Gitlab do
expect(described_class.http_proxy_env?).to eq(false)
end
end
+
+ describe '.maintenance_mode?' do
+ it 'returns true when maintenance mode is enabled' do
+ stub_application_setting(maintenance_mode: true)
+
+ expect(described_class.maintenance_mode?).to eq(true)
+ end
+
+ it 'returns false when maintenance mode is disabled' do
+ stub_application_setting(maintenance_mode: false)
+
+ expect(described_class.maintenance_mode?).to eq(false)
+ end
+
+ it 'returns false when maintenance mode feature flag is disabled' do
+ stub_feature_flags(maintenance_mode: false)
+
+ expect(described_class.maintenance_mode?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb
index c35d7e8420c..3b7892334dd 100644
--- a/spec/lib/microsoft_teams/notifier_spec.rb
+++ b/spec/lib/microsoft_teams/notifier_spec.rb
@@ -51,11 +51,11 @@ RSpec.describe MicrosoftTeams::Notifier do
describe '#body' do
it 'returns Markdown-based body when HTML was passed' do
- expect(subject.send(:body, options)).to eq(body.to_json)
+ expect(subject.send(:body, **options)).to eq(body.to_json)
end
it 'fails when empty Hash was passed' do
- expect { subject.send(:body, {}) }.to raise_error(ArgumentError)
+ expect { subject.send(:body, **{}) }.to raise_error(ArgumentError)
end
end
end
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 932d579c3cc..bd9d197afa0 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -162,6 +162,10 @@ RSpec.describe ObjectStorage::DirectUpload do
it 'enables the Workhorse client' do
expect(subject[:UseWorkhorseClient]).to be true
end
+
+ it 'omits the multipart upload URLs' do
+ expect(subject).not_to include(:MultipartUpload)
+ end
end
context 'when only server side encryption is used' do
@@ -292,6 +296,7 @@ RSpec.describe ObjectStorage::DirectUpload do
context 'when IAM profile is true' do
let(:use_iam_profile) { true }
+ let(:iam_credentials_v2_url) { "http://169.254.169.254/latest/api/token" }
let(:iam_credentials_url) { "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
let(:iam_credentials) do
{
@@ -303,6 +308,9 @@ RSpec.describe ObjectStorage::DirectUpload do
end
before do
+ # If IMDSv2 is disabled, we should still fall back to IMDSv1
+ stub_request(:put, iam_credentials_v2_url)
+ .to_return(status: 404)
stub_request(:get, iam_credentials_url)
.to_return(status: 200, body: "somerole", headers: {})
stub_request(:get, "#{iam_credentials_url}somerole")
@@ -310,6 +318,21 @@ RSpec.describe ObjectStorage::DirectUpload do
end
it_behaves_like 'a valid S3 upload without multipart data'
+
+ context 'when IMSDv2 is available' do
+ let(:iam_token) { 'mytoken' }
+
+ before do
+ stub_request(:put, iam_credentials_v2_url)
+ .to_return(status: 200, body: iam_token)
+ stub_request(:get, iam_credentials_url).with(headers: { "X-aws-ec2-metadata-token" => iam_token })
+ .to_return(status: 200, body: "somerole", headers: {})
+ stub_request(:get, "#{iam_credentials_url}somerole").with(headers: { "X-aws-ec2-metadata-token" => iam_token })
+ .to_return(status: 200, body: iam_credentials.to_json, headers: {})
+ end
+
+ it_behaves_like 'a valid S3 upload without multipart data'
+ end
end
end
@@ -321,6 +344,30 @@ RSpec.describe ObjectStorage::DirectUpload do
stub_object_storage_multipart_init(storage_url, "myUpload")
end
+ context 'when maximum upload size is 0' do
+ let(:maximum_size) { 0 }
+
+ it 'returns maximum number of parts' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(100)
+ end
+
+ it 'part size is minimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
+ end
+ end
+
+ context 'when maximum upload size is < 5 MB' do
+ let(:maximum_size) { 1024 }
+
+ it 'returns only 1 part' do
+ expect(subject[:MultipartUpload][:PartURLs].length).to eq(1)
+ end
+
+ it 'part size is minimum, 5MB' do
+ expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
+ end
+ end
+
context 'when maximum upload size is 10MB' do
let(:maximum_size) { 10.megabyte }
diff --git a/spec/lib/product_analytics/tracker_spec.rb b/spec/lib/product_analytics/tracker_spec.rb
index 0d0660235f1..52470c9c039 100644
--- a/spec/lib/product_analytics/tracker_spec.rb
+++ b/spec/lib/product_analytics/tracker_spec.rb
@@ -5,53 +5,4 @@ require 'spec_helper'
RSpec.describe ProductAnalytics::Tracker do
it { expect(described_class::URL).to eq('http://localhost/-/sp.js') }
it { expect(described_class::COLLECTOR_URL).to eq('localhost/-/collector') }
-
- describe '.event' do
- after do
- described_class.clear_memoization(:snowplow)
- end
-
- context 'when usage ping is enabled' do
- let(:tracker) { double }
- let(:project_id) { 1 }
-
- before do
- stub_application_setting(usage_ping_enabled: true, self_monitoring_project_id: project_id)
- end
-
- it 'sends an event to Product Analytics snowplow collector' do
- expect(SnowplowTracker::AsyncEmitter)
- .to receive(:new)
- .with(described_class::COLLECTOR_URL, { protocol: 'http' })
- .and_return('_emitter_')
-
- expect(SnowplowTracker::Tracker)
- .to receive(:new)
- .with('_emitter_', an_instance_of(SnowplowTracker::Subject), 'gl', project_id.to_s)
- .and_return(tracker)
-
- freeze_time do
- expect(tracker)
- .to receive(:track_struct_event)
- .with('category', 'action', '_label_', '_property_', '_value_', nil, (Time.current.to_f * 1000).to_i)
-
- described_class.event('category', 'action', label: '_label_', property: '_property_',
- value: '_value_', context: nil)
- end
- end
- end
-
- context 'when usage ping is disabled' do
- before do
- stub_application_setting(usage_ping_enabled: false)
- end
-
- it 'does not send an event' do
- expect(SnowplowTracker::Tracker).not_to receive(:new)
-
- described_class.event('category', 'action', label: '_label_', property: '_property_',
- value: '_value_', context: nil)
- end
- end
- end
end
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 0239c974755..2232d47234f 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration,tooling}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -103,7 +103,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration|tooling)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|tooling)})
end
end
@@ -174,6 +174,10 @@ RSpec.describe Quality::TestLevel do
expect(subject.level_for('spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb')).to eq(:migration)
end
+ it 'returns the correct level for an EE file without passing a prefix' do
+ expect(subject.level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration)
+ end
+
it 'returns the correct level for a geo migration test' do
expect(described_class.new('ee/').level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration)
end