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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/lib
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec/lib')
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb27
-rw-r--r--spec/lib/api/entities/user_spec.rb12
-rw-r--r--spec/lib/api/support/git_access_actor_spec.rb12
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb66
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb5
-rw-r--r--spec/lib/backup/files_spec.rb14
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/custom_emoji_filter_spec.rb80
-rw-r--r--spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb223
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/truncate_source_filter_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb5
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb12
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb118
-rw-r--r--spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb53
-rw-r--r--spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb66
-rw-r--r--spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb48
-rw-r--r--spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb28
-rw-r--r--spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb27
-rw-r--r--spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb24
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb32
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb32
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb35
-rw-r--r--spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb17
-rw-r--r--spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb30
-rw-r--r--spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb42
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb51
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb118
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb87
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb12
-rw-r--r--spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb24
-rw-r--r--spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb101
-rw-r--r--spec/lib/bulk_imports/importers/group_importer_spec.rb29
-rw-r--r--spec/lib/bulk_imports/pipeline/context_spec.rb38
-rw-r--r--spec/lib/bulk_imports/pipeline/extracted_data_spec.rb53
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb97
-rw-r--r--spec/lib/feature/gitaly_spec.rb67
-rw-r--r--spec/lib/gitlab/access/branch_protection_spec.rb10
-rw-r--r--spec/lib/gitlab/alert_management/payload/generic_spec.rb29
-rw-r--r--spec/lib/gitlab/alert_management/payload/prometheus_spec.rb63
-rw-r--r--spec/lib/gitlab/alert_management/payload_spec.rb15
-rw-r--r--spec/lib/gitlab/api_authentication/token_locator_spec.rb21
-rw-r--r--spec/lib/gitlab/api_authentication/token_resolver_spec.rb63
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb67
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb329
-rw-r--r--spec/lib/gitlab/auth/ip_rate_limiter_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/otp/session_enforcer_spec.rb41
-rw-r--r--spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb29
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb135
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb2
-rw-r--r--spec/lib/gitlab/changelog/ast_spec.rb246
-rw-r--r--spec/lib/gitlab/changelog/committer_spec.rb128
-rw-r--r--spec/lib/gitlab/changelog/config_spec.rb98
-rw-r--r--spec/lib/gitlab/changelog/generator_spec.rb164
-rw-r--r--spec/lib/gitlab/changelog/parser_spec.rb78
-rw-r--r--spec/lib/gitlab/changelog/release_spec.rb129
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb (renamed from spec/lib/gitlab/badge/coverage/metadata_spec.rb)4
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/report_spec.rb (renamed from spec/lib/gitlab/badge/coverage/report_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/template_spec.rb (renamed from spec/lib/gitlab/badge/coverage/template_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/metadata_spec.rb)4
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/status_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/status_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/template_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/template_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/shared/metadata.rb (renamed from spec/lib/gitlab/badge/shared/metadata.rb)0
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb (renamed from spec/lib/gitlab/ci/build/credentials/registry_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/charts_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb121
-rw-r--r--spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb123
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/jwt_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/instrumentation_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb (renamed from spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/sorted_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/variables/helpers_spec.rb103
-rw-r--r--spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb3
-rw-r--r--spec/lib/gitlab/cluster/lifecycle_events_spec.rb85
-rw-r--r--spec/lib/gitlab/composer/cache_spec.rb133
-rw-r--r--spec/lib/gitlab/composer/version_index_spec.rb16
-rw-r--r--spec/lib/gitlab/conan_token_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb70
-rw-r--r--spec/lib/gitlab/crypto_helper_spec.rb78
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb28
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb34
-rw-r--r--spec/lib/gitlab/danger/base_linter_spec.rb193
-rw-r--r--spec/lib/gitlab/danger/changelog_spec.rb229
-rw-r--r--spec/lib/gitlab/danger/commit_linter_spec.rb242
-rw-r--r--spec/lib/gitlab/danger/danger_spec_helper.rb17
-rw-r--r--spec/lib/gitlab/danger/emoji_checker_spec.rb38
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb602
-rw-r--r--spec/lib/gitlab/danger/merge_request_linter_spec.rb55
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb413
-rw-r--r--spec/lib/gitlab/danger/sidekiq_queues_spec.rb82
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb220
-rw-r--r--spec/lib/gitlab/danger/title_linting_spec.rb56
-rw-r--r--spec/lib/gitlab/danger/weightage/maintainers_spec.rb34
-rw-r--r--spec/lib/gitlab/danger/weightage/reviewers_spec.rb63
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb4
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb15
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb221
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb253
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb111
-rw-r--r--spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb39
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb3
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/char_diff_spec.rb77
-rw-r--r--spec/lib/gitlab/diff/file_collection_sorter_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb18
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb27
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb7
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb27
-rw-r--r--spec/lib/gitlab/experimentation/experiment_spec.rb3
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb66
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb8
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb3
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb7
-rw-r--r--spec/lib/gitlab/git/push_spec.rb10
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_spec.rb102
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb64
-rw-r--r--spec/lib/gitlab/graphql/pagination/connections_spec.rb97
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb4
-rw-r--r--spec/lib/gitlab/health_checks/master_check_spec.rb68
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb29
-rw-r--r--spec/lib/gitlab/hook_data/group_builder_spec.rb68
-rw-r--r--spec/lib/gitlab/hook_data/subgroup_builder_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml7
-rw-r--r--spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb50
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/design_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml4
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb1
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb60
-rw-r--r--spec/lib/gitlab/kas_spec.rb44
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb100
-rw-r--r--spec/lib/gitlab/metrics/subscribers/external_http_spec.rb172
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb203
-rw-r--r--spec/lib/gitlab/middleware/request_context_spec.rb43
-rw-r--r--spec/lib/gitlab/pages_transfer_spec.rb22
-rw-r--r--spec/lib/gitlab/patch/prependable_spec.rb18
-rw-r--r--spec/lib/gitlab/performance_bar/stats_spec.rb8
-rw-r--r--spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb89
-rw-r--r--spec/lib/gitlab/rack_attack_spec.rb3
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb7
-rw-r--r--spec/lib/gitlab/request_forgery_protection_spec.rb5
-rw-r--r--spec/lib/gitlab/runtime_spec.rb17
-rw-r--r--spec/lib/gitlab/search/query_spec.rb18
-rw-r--r--spec/lib/gitlab/search_results_spec.rb27
-rw-r--r--spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb44
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb178
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb21
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb22
-rw-r--r--spec/lib/gitlab/suggestions/commit_message_spec.rb11
-rw-r--r--spec/lib/gitlab/template/finders/global_template_finder_spec.rb23
-rw-r--r--spec/lib/gitlab/terraform/state_migration_helper_spec.rb21
-rw-r--r--spec/lib/gitlab/tracking/standard_context_spec.rb55
-rw-r--r--spec/lib/gitlab/tracking_spec.rb47
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb46
-rw-r--r--spec/lib/gitlab/url_blockers/url_allowlist_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/docs/renderer_spec.rb22
-rw-r--r--spec/lib/gitlab/usage/docs/value_formatter_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb29
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb281
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb146
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb29
-rw-r--r--spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb20
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb86
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb228
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb136
-rw-r--r--spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb163
-rw-r--r--spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb63
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb144
-rw-r--r--spec/lib/gitlab/utils/markdown_spec.rb44
-rw-r--r--spec/lib/gitlab/utils/override_spec.rb67
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb10
-rw-r--r--spec/lib/gitlab/utils_spec.rb8
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb47
-rw-r--r--spec/lib/gitlab_danger_spec.rb76
-rw-r--r--spec/lib/gitlab_spec.rb24
-rw-r--r--spec/lib/object_storage/config_spec.rb63
-rw-r--r--spec/lib/peek/views/external_http_spec.rb215
-rw-r--r--spec/lib/release_highlights/validator/entry_spec.rb2
-rw-r--r--spec/lib/release_highlights/validator_spec.rb2
-rw-r--r--spec/lib/security/ci_configuration/sast_build_actions_spec.rb539
216 files changed, 8372 insertions, 4060 deletions
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb
index fe4c27b70ae..8572b067984 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
let_it_be(:project) { create(:project, :public) }
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:labels) { create_list(:label, 3) }
- let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, :with_diffs, labels: labels) }
+ let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, labels: labels) }
# This mimics the behavior of the `Grape::Entity` serializer
def present(obj)
@@ -42,29 +42,14 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
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
+ before do
+ merge_request.reviewers = [user]
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)
+ it 'includes assigned reviewers' do
+ result = Gitlab::Json.parse(present(merge_request).to_json)
- expect(result.keys).not_to include('reviewers')
- end
+ expect(result['reviewers'][0]['username']).to eq user.username
end
end
end
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index 99ffe0eb925..e35deeb6263 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -23,4 +23,16 @@ RSpec.describe API::Entities::User do
expect(subject).not_to include(:created_at)
end
+
+ it 'exposes user as not a bot' do
+ expect(subject[:bot]).to be_falsey
+ end
+
+ context 'with bot user' do
+ let(:user) { create(:user, :security_bot) }
+
+ it 'exposes user as a bot' do
+ expect(subject[:bot]).to eq(true)
+ end
+ end
end
diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb
index 143cc6e56ee..a09cabf4cd7 100644
--- a/spec/lib/api/support/git_access_actor_spec.rb
+++ b/spec/lib/api/support/git_access_actor_spec.rb
@@ -152,6 +152,10 @@ RSpec.describe API::Support::GitAccessActor do
end
describe '#update_last_used_at!' do
+ before do
+ stub_feature_flags(disable_ssh_key_used_tracking: false)
+ end
+
context 'when initialized with a User' do
let(:user) { build(:user) }
@@ -170,6 +174,14 @@ RSpec.describe API::Support::GitAccessActor do
subject.update_last_used_at!
end
+
+ it 'does not update `last_used_at` when the functionality is disabled' do
+ stub_feature_flags(disable_ssh_key_used_tracking: true)
+
+ expect(key).not_to receive(:update_last_used_at)
+
+ subject.update_last_used_at!
+ 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 21ee40f22fe..5c8d4282118 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -18,15 +18,15 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- around do |example|
- freeze_time { example.run }
- end
-
describe '.generate_update_sequence_id' do
- it 'returns monotonic_time converted it to integer' do
- allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1.0)
+ it 'returns unix time in microseconds as integer', :aggregate_failures do
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 1)) do
+ expect(described_class.generate_update_sequence_id).to eq(1000)
+ end
- expect(described_class.generate_update_sequence_id).to eq(1)
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 5)) do
+ expect(described_class.generate_update_sequence_id).to eq(5000)
+ end
end
end
@@ -238,22 +238,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
expect(response['errorMessages']).to eq(%w(X Y Z))
end
end
-
- it 'does not call the API if the feature flag is not enabled' do
- stub_feature_flags(jira_sync_deployments: false)
-
- expect(subject).not_to receive(:post)
-
- subject.send(:store_deploy_info, project: project, deployments: deployments)
- end
-
- it 'does call the API if the feature flag enabled for the project' do
- stub_feature_flags(jira_sync_deployments: project)
-
- expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: Array }).and_call_original
-
- subject.send(:store_deploy_info, project: project, deployments: deployments)
- end
end
describe '#store_ff_info' do
@@ -319,24 +303,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
expect(response['errorMessages']).to eq(['a: X', 'a: Y', 'b: Z'])
end
end
-
- it 'does not call the API if the feature flag is not enabled' do
- stub_feature_flags(jira_sync_feature_flags: false)
-
- expect(subject).not_to receive(:post)
-
- subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
- end
-
- it 'does call the API if the feature flag enabled for the project' do
- stub_feature_flags(jira_sync_feature_flags: project)
-
- expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', {
- flags: Array, properties: Hash
- }).and_call_original
-
- subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
- end
end
describe '#store_build_info' do
@@ -384,24 +350,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
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 })
- .and_call_original
-
- subject.send(:store_build_info, project: project, pipelines: pipelines)
- end
-
context 'there are errors' do
let(:failures) do
[{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }]
diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
index 964801338cf..2d12cd1ed0a 100644
--- a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
subject { described_class.represent(feature_flag) }
context 'when the feature flag does not belong to any Jira issue' do
- let_it_be(:feature_flag) { create(:operations_feature_flag) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
describe '#issue_keys' do
it 'is empty' do
@@ -30,7 +30,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
context 'when the feature flag does belong to a Jira issue' do
let(:feature_flag) do
- create(:operations_feature_flag, description: 'THING-123')
+ create(:operations_feature_flag, project: project, description: 'THING-123')
end
describe '#issue_keys' do
@@ -66,6 +66,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
end
it 'has the correct summary' do
+ expect(entity.dig('summary', 'url')).to eq "http://localhost/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/edit"
expect(entity.dig('summary', 'status')).to eq(
'enabled' => true,
'defaultValue' => '',
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 450e396a389..92de191da2d 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -21,10 +21,6 @@ RSpec.describe Backup::Files do
allow(File).to receive(:realpath).with("/var/gitlab-pages").and_return("/var/gitlab-pages")
allow(File).to receive(:realpath).with("/var/gitlab-pages/..").and_return("/var")
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
end
@@ -150,7 +146,7 @@ RSpec.describe Backup::Files do
it 'excludes tmp dirs from rsync' do
expect(Gitlab::Popen).to receive(:popen)
- .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['', 0])
subject.dump
@@ -158,7 +154,7 @@ RSpec.describe Backup::Files do
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))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['rsync failed', 24], ['', 0])
expect do
@@ -168,7 +164,7 @@ RSpec.describe Backup::Files do
it 'raises an error and outputs an error message if rsync failed' do
allow(Gitlab::Popen).to receive(:popen)
- .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['rsync failed', 1])
expect do
@@ -186,8 +182,8 @@ RSpec.describe Backup::Files do
expect(subject.exclude_dirs(:tar)).to eq(['--exclude=lost+found', '--exclude=./@pages.tmp'])
end
- it 'prepends a leading slash to rsync excludes' do
- expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/@pages.tmp'])
+ it 'prepends a leading slash and app_files_dir basename to rsync excludes' do
+ expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/gitlab-pages/@pages.tmp'])
end
end
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index 1f886059bf6..81aa8d35ebc 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
stub_application_setting(asset_proxy_enabled: true)
stub_application_setting(asset_proxy_secret_key: 'shared-secret')
stub_application_setting(asset_proxy_url: 'https://assets.example.com')
- stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com))
+ stub_application_setting(asset_proxy_allowlist: %w(gitlab.com *.mydomain.com))
described_class.initialize_settings
@@ -39,16 +39,26 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i)
end
- context 'when whitelist is empty' do
+ context 'when allowlist is empty' do
it 'defaults to the install domain' do
stub_application_setting(asset_proxy_enabled: true)
- stub_application_setting(asset_proxy_whitelist: [])
+ stub_application_setting(asset_proxy_allowlist: [])
described_class.initialize_settings
expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host]
end
end
+
+ it 'supports deprecated whitelist settings' do
+ stub_application_setting(asset_proxy_enabled: true)
+ stub_application_setting(asset_proxy_whitelist: %w(foo.com bar.com))
+ stub_application_setting(asset_proxy_allowlist: [])
+
+ described_class.initialize_settings
+
+ expect(Gitlab.config.asset_proxy.allowlist).to eq %w(foo.com bar.com)
+ end
end
context 'when properly configured' do
diff --git a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
new file mode 100644
index 00000000000..ca8c9750e7f
--- /dev/null
+++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::CustomEmojiFilter do
+ include FilterSpecHelper
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) }
+ let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') }
+
+ it 'replaces supported name custom emoji' do
+ doc = filter('<p>:tanuki:</p>', project: project)
+
+ expect(doc.css('gl-emoji').first.attributes['title'].value).to eq('tanuki')
+ expect(doc.css('gl-emoji img').size).to eq 1
+ end
+
+ it 'ignores non existent custom emoji' do
+ exp = act = '<p>:foo:</p>'
+ doc = filter(act)
+
+ expect(doc.to_html).to match Regexp.escape(exp)
+ end
+
+ it 'correctly uses the custom emoji URL' do
+ doc = filter('<p>:tanuki:</p>')
+
+ expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file)
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter('tanuki (:tanuki:)')
+
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches multiple same custom emoji' do
+ doc = filter(':tanuki: :tanuki:')
+
+ expect(doc.css('img').size).to eq 2
+ end
+
+ it 'matches multiple custom emoji' do
+ doc = filter(':tanuki: (:happy_tanuki:)')
+
+ expect(doc.css('img').size).to eq 2
+ end
+
+ it 'does not match enclosed colons' do
+ doc = filter('tanuki:tanuki:')
+
+ expect(doc.css('img').size).to be 0
+ end
+
+ it 'keeps whitespace intact' do
+ doc = filter('This deserves a :tanuki:, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
+ end
+
+ it 'does not match emoji in a string' do
+ doc = filter("'2a00:tanuki:100::1'")
+
+ expect(doc.css('gl-emoji').size).to eq 0
+ end
+
+ it 'does not do N+1 query' do
+ create(:custom_emoji, name: 'party-parrot', group: group)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ filter('<p>:tanuki:</p>')
+ end
+
+ expect do
+ filter('<p>:tanuki: :party-parrot:</p>')
+ end.not_to exceed_all_query_limit(control_count.count)
+ end
+end
diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb
new file mode 100644
index 00000000000..2d7089853cf
--- /dev/null
+++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do
+ include FilterSpecHelper
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let_it_be(:reference) { feature_flag.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with internal reference' do
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Feature Flag (#{reference}.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
+ end
+
+ it 'ignores invalid feature flag IIDs' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.css('a').first.attr('title')).to eq feature_flag.name
+ end
+
+ it 'escapes the title attribute' do
+ allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="})
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.text).to eq "Feature Flag #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-feature_flag has-tooltip'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Feature Flag #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-feature-flag attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-feature-flag')
+ expect(link.attr('data-feature-flag')).to eq feature_flag.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Feature Flag #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true)
+ end
+ end
+
+ context 'with cross-project / cross-namespace complete reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eql(reference)
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{reference}.)")
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project / same-namespace complete reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project shorthand reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+ end
+
+ it 'ignores invalid feature flag IDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project URL reference' do
+ let_it_be(:namespace) { create(:namespace, name: 'cross-reference') }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { urls.edit_project_feature_flag_url(project2, feature_flag) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(feature_flag.to_reference(project))}</a>\.\)})
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
+ end
+ end
+
+ context 'with group context' do
+ let_it_be(:group) { create(:group) }
+
+ it 'links to a valid reference' do
+ reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]"
+ result = reference_filter("See #{reference}", { project: nil, group: group } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag))
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See [feature_flag:#{feature_flag.iid}]"
+
+ expect(reference_filter(act, project: nil, group: group).to_html).to eq exp
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index df78a3321ba..811c2aca342 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -216,7 +216,7 @@ RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do
end
context 'URL reference for a commit' do
- let(:mr) { create(:merge_request, :with_diffs) }
+ let(:mr) { create(:merge_request) }
let(:reference) do
urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}"
end
diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
index b0c6d91daa8..d5eb8b738b1 100644
--- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb
+++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do
it 'truncates UTF-8 text by bytes, on a character boundary' do
utf8_text = '日本語の文字が大きい'
- truncated = '日…'
+ truncated = '日...'
expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated)
expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text)
diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
index 41a91c56f3b..ad4256c2045 100644
--- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
@@ -3,11 +3,14 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
before do
stub_commonmark_sourcepos_disabled
end
- subject { described_class.to_html(exp, project: spy) }
+ subject { described_class.to_html(exp, project: project) }
context "allows `a` elements" do
let(:exp) { "<a>Link</a>" }
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 9391ca386cf..bcee6f8f65d 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -131,4 +131,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(output).to include("test [[<em>TOC</em>]]")
end
end
+
+ describe 'backslash escapes' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it 'does not convert an escaped reference' do
+ markdown = "\\#{issue.to_reference}"
+ output = described_class.to_html(markdown, project: project)
+
+ expect(output).to include("<span>#</span>#{issue.iid}")
+ end
+ end
end
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
new file mode 100644
index 00000000000..241d6db4f11
--- /dev/null
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
+ using RSpec::Parameterized::TableSyntax
+
+ describe 'backslash escapes' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ def correct_html_included(markdown, expected)
+ result = described_class.call(markdown, {})
+
+ expect(result[:output].to_html).to include(expected)
+
+ result
+ end
+
+ context 'when feature flag honor_escaped_markdown is disabled' do
+ before do
+ stub_feature_flags(honor_escaped_markdown: false)
+ end
+
+ it 'does not escape the markdown' do
+ result = described_class.call(%q(\!), project: project)
+ output = result[:output].to_html
+
+ expect(output).to eq('<p data-sourcepos="1:1-1:2">!</p>')
+ expect(result[:escaped_literals]).to be_falsey
+ end
+ end
+
+ # Test strings taken from https://spec.commonmark.org/0.29/#backslash-escapes
+ describe 'CommonMark tests', :aggregate_failures do
+ it 'converts all ASCII punctuation to literals' do
+ markdown = %q(\!\"\#\$\%\&\'\*\+\,\-\.\/\:\;\<\=\>\?\@\[\]\^\_\`\{\|\}\~) + %q[\(\)\\\\]
+ punctuation = %w(! " # $ % &amp; ' * + , - . / : ; &lt; = &gt; ? @ [ \\ ] ^ _ ` { | } ~) + %w[( )]
+
+ result = described_class.call(markdown, project: project)
+ output = result[:output].to_html
+
+ punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
+ expect(result[:escaped_literals]).to be_truthy
+ end
+
+ it 'does not convert other characters to literals' do
+ markdown = %q(\→\A\a\ \3\φ\«)
+ expected = '\→\A\a\ \3\φ\«'
+
+ result = correct_html_included(markdown, expected)
+ expect(result[:escaped_literals]).to be_falsey
+ end
+
+ describe 'escaped characters are treated as regular characters and do not have their usual Markdown meanings' do
+ where(:markdown, :expected) do
+ %q(\*not emphasized*) | %q(<span>*</span>not emphasized*)
+ %q(\<br/> not a tag) | %q(<span>&lt;</span>br/&gt; not a tag)
+ %q!\[not a link](/foo)! | %q!<span>[</span>not a link](/foo)!
+ %q(\`not code`) | %q(<span>`</span>not code`)
+ %q(1\. not a list) | %q(1<span>.</span> not a list)
+ %q(\# not a heading) | %q(<span>#</span> not a heading)
+ %q(\[foo]: /url "not a reference") | %q(<span>[</span>foo]: /url "not a reference")
+ %q(\&ouml; not a character entity) | %q(<span>&amp;</span>ouml; not a character entity)
+ end
+
+ with_them do
+ it 'keeps them as literals' do
+ correct_html_included(markdown, expected)
+ end
+ end
+ end
+
+ it 'backslash is itself escaped, the following character is not' do
+ markdown = %q(\\\\*emphasis*)
+ expected = %q(<span>\</span><em>emphasis</em>)
+
+ correct_html_included(markdown, expected)
+ end
+
+ it 'backslash at the end of the line is a hard line break' do
+ markdown = <<~MARKDOWN
+ foo\\
+ bar
+ MARKDOWN
+ expected = "foo<br>\nbar"
+
+ correct_html_included(markdown, expected)
+ end
+
+ describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do
+ where(:markdown, :expected) do
+ %q(`` \[\` ``) | %q(<code>\[\`</code>)
+ %q( \[\]) | %Q(<code>\\[\\]\n</code>)
+ %Q(~~~\n\\[\\]\n~~~) | %Q(<code>\\[\\]\n</code>)
+ %q(<http://example.com?find=\*>) | %q(<a href="http://example.com?find=%5C*">http://example.com?find=\*</a>)
+ %q[<a href="/bar\/)">] | %q[<a href="/bar%5C/)">]
+ end
+
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
+ end
+
+ describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
+ where(:markdown, :expected) do
+ %q![foo](/bar\* "ti\*tle")! | %q(<a href="/bar*" title="ti*tle">foo</a>)
+ %Q![foo]\n\n[foo]: /bar\\* "ti\\*tle"! | %q(<a href="/bar*" title="ti*tle">foo</a>)
+ %Q(``` foo\\+bar\nfoo\n```) | %Q(<code lang="foo+bar">foo\n</code>)
+ end
+
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
index f0498f41b61..c628d8d5b41 100644
--- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
@@ -30,6 +30,6 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do
result = described_class.call(text, limit: 12)
- expect(result[:output]).to eq('foo foo f…')
+ expect(result[:output]).to eq('foo foo f...')
end
end
diff --git a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
new file mode 100644
index 00000000000..288eb9ae360
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do
+ include ReferenceParserHelpers
+
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
+ let(:link) { empty_html_link }
+
+ describe '#nodes_visible_to_user' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ context 'when the link has a data-issue attribute' do
+ before do
+ link['data-feature-flag'] = feature_flag.id.to_s
+ end
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests" do
+ before do
+ project.add_developer(user) if enable_user?
+ end
+ end
+ end
+ end
+
+ describe '#referenced_by' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ describe 'when the link has a data-feature-flag attribute' do
+ context 'using an existing feature flag ID' do
+ it 'returns an Array of feature flags' do
+ link['data-feature-flag'] = feature_flag.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([feature_flag])
+ end
+ end
+
+ context 'using a non-existing feature flag ID' do
+ it 'returns an empty Array' do
+ link['data-feature-flag'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
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 2abd3df20fd..80607485b6e 100644
--- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
@@ -5,8 +5,18 @@ require 'spec_helper'
RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) }
let(:import_entity) { create(:bulk_import_entity) }
- let(:response) { double(original_hash: { foo: :bar }) }
- let(:query) { { query: double(to_s: 'test', variables: {}) } }
+ let(:response) { double(original_hash: { 'data' => { 'foo' => 'bar' }, 'page_info' => {} }) }
+ let(:options) do
+ {
+ query: double(
+ to_s: 'test',
+ variables: {},
+ data_path: %w[data foo],
+ page_info_path: %w[data page_info]
+ )
+ }
+ end
+
let(:context) do
instance_double(
BulkImports::Pipeline::Context,
@@ -14,58 +24,20 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
)
end
- subject { described_class.new(query) }
-
- before do
- allow(subject).to receive(:graphql_client).and_return(graphql_client)
- allow(graphql_client).to receive(:parse)
- end
+ subject { described_class.new(options) }
describe '#extract' do
before do
- allow(subject).to receive(:query_variables).and_return({})
- allow(graphql_client).to receive(:execute).and_return(response)
- end
-
- it 'returns original hash' do
- expect(subject.extract(context)).to eq({ foo: :bar })
- end
- end
-
- describe 'query variables' do
- before do
+ allow(subject).to receive(:graphql_client).and_return(graphql_client)
+ allow(graphql_client).to receive(:parse)
allow(graphql_client).to receive(:execute).and_return(response)
end
- context 'when variables are present' do
- let(:variables) { { foo: :bar } }
- let(:query) { { query: double(to_s: 'test', variables: variables) } }
-
- it 'builds graphql query variables for import entity' do
- expect(graphql_client).to receive(:execute).with(anything, variables)
-
- subject.extract(context).first
- end
- end
-
- context 'when no variables are present' do
- let(:query) { { query: double(to_s: 'test', variables: nil) } }
-
- it 'returns empty hash' do
- expect(graphql_client).to receive(:execute).with(anything, nil)
-
- subject.extract(context).first
- end
- end
-
- context 'when variables are empty hash' do
- let(:query) { { query: double(to_s: 'test', variables: {}) } }
-
- it 'makes graphql request with empty hash' do
- expect(graphql_client).to receive(:execute).with(anything, {})
+ it 'returns ExtractedData' do
+ extracted_data = subject.extract(context)
- subject.extract(context).first
- end
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(extracted_data.data).to contain_exactly('bar')
end
end
end
diff --git a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
index 4de7d95172f..57ffdfa9aee 100644
--- a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
+++ b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe BulkImports::Common::Loaders::EntityLoader do
it "creates entities for the given data" do
group = create(:group, path: "imported-group")
parent_entity = create(:bulk_import_entity, group: group, bulk_import: create(:bulk_import))
- context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity)
+ context = BulkImports::Pipeline::Context.new(parent_entity)
data = {
source_type: :group_entity,
diff --git a/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb
new file mode 100644
index 00000000000..5b560a30bf5
--- /dev/null
+++ b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
+ describe '#transform' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let(:hash) do
+ {
+ 'name' => 'thumbs up',
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+ end
+
+ before do
+ group.add_developer(user)
+ end
+
+ shared_examples 'sets user_id and removes user key' do
+ it 'sets found user_id and removes user key' do
+ transformed_hash = subject.transform(context, hash)
+
+ expect(transformed_hash['user']).to be_nil
+ expect(transformed_hash['user_id']).to eq(user.id)
+ end
+ end
+
+ context 'when user can be found by email' do
+ let(:email) { user.email }
+
+ include_examples 'sets user_id and removes user key'
+ end
+
+ context 'when user cannot be found by email' do
+ let(:user) { bulk_import.user }
+ let(:email) { nil }
+
+ include_examples 'sets user_id and removes user key'
+ 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
deleted file mode 100644
index 2b33701653e..00000000000
--- a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# 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/underscorify_keys_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb
deleted file mode 100644
index cdffa750694..00000000000
--- a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Common::Transformers::UnderscorifyKeysTransformer do
- describe '#transform' do
- it 'deep underscorifies hash keys' do
- data = {
- 'fullPath' => 'Foo',
- 'snakeKeys' => {
- 'snakeCaseKey' => 'Bar',
- 'moreKeys' => {
- 'anotherSnakeCaseKey' => 'Test'
- }
- }
- }
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to have_key('full_path')
- expect(transformed_data).to have_key('snake_keys')
- expect(transformed_data['snake_keys']).to have_key('snake_case_key')
- expect(transformed_data['snake_keys']).to have_key('more_keys')
- expect(transformed_data.dig('snake_keys', 'more_keys')).to have_key('another_snake_case_key')
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
new file mode 100644
index 00000000000..627247c04ab
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Extractors::SubgroupsExtractor do
+ describe '#extract' do
+ it 'returns ExtractedData response' do
+ bulk_import = create(:bulk_import)
+ create(:bulk_import_configuration, bulk_import: bulk_import)
+ entity = create(:bulk_import_entity, bulk_import: bulk_import)
+ response = [{ 'test' => 'group' }]
+ context = BulkImports::Pipeline::Context.new(entity)
+
+ allow_next_instance_of(BulkImports::Clients::Http) do |client|
+ allow(client).to receive(:each_page).and_return(response)
+ end
+
+ extracted_data = subject.extract(context)
+
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(extracted_data.data).to eq(response)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
new file mode 100644
index 00000000000..ef46da7062b
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
+ describe '#variables' do
+ let(:entity) { double(source_full_path: 'test', bulk_import: nil) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns query variables based on entity information' do
+ expected = { full_path: entity.source_full_path }
+
+ expect(described_class.variables(context)).to eq(expected)
+ end
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
new file mode 100644
index 00000000000..247da200d68
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
+ describe '#variables' do
+ let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page', bulk_import: nil) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns query variables based on entity information' do
+ expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
+
+ expect(described_class.variables(context)).to eq(expected)
+ end
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group labels nodes]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group labels page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
new file mode 100644
index 00000000000..5d05f5a2d30
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
+ it 'has a valid query' do
+ entity = create(:bulk_import_entity)
+ context = BulkImports::Pipeline::Context.new(entity)
+
+ query = GraphQL::Query.new(
+ GitlabSchema,
+ described_class.to_s,
+ variables: described_class.variables(context)
+ )
+ result = GitlabSchema.static_validator.validate(query)
+
+ expect(result[:errors]).to be_empty
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group group_members nodes]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group group_members page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
index b14dfc615a9..183292722d2 100644
--- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
+++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
@@ -7,21 +7,20 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
let(:user) { create(:user) }
let(:data) { { foo: :bar } }
let(:service_double) { instance_double(::Groups::CreateService) }
- let(:entity) { create(:bulk_import_entity) }
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- entity: entity,
- current_user: user
- )
- end
+ let(:bulk_import) { create(:bulk_import, user: user) }
+ let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
subject { described_class.new }
context 'when user can create group' do
shared_examples 'calls Group Create Service to create a new group' do
it 'calls Group Create Service to create a new group' do
- expect(::Groups::CreateService).to receive(:new).with(context.current_user, data).and_return(service_double)
+ expect(::Groups::CreateService)
+ .to receive(:new)
+ .with(context.current_user, data)
+ .and_return(service_double)
+
expect(service_double).to receive(:execute)
expect(entity).to receive(:update!)
diff --git a/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb
new file mode 100644
index 00000000000..ac2f9c8cb1d
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Loaders::LabelsLoader do
+ describe '#load' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:entity) { create(:bulk_import_entity, group: group) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let(:data) do
+ {
+ 'title' => 'label',
+ 'description' => 'description',
+ 'color' => '#FFFFFF'
+ }
+ end
+
+ it 'creates the label' do
+ expect { subject.load(context, data) }.to change(Label, :count).by(1)
+
+ label = group.labels.first
+
+ expect(label.title).to eq(data['title'])
+ expect(label.description).to eq(data['description'])
+ expect(label.color).to eq(data['color'])
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
new file mode 100644
index 00000000000..d552578e7be
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Loaders::MembersLoader do
+ describe '#load' do
+ let_it_be(:user_importer) { create(:user) }
+ let_it_be(:user_member) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user_importer) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let_it_be(:data) do
+ {
+ 'user_id' => user_member.id,
+ 'created_by_id' => user_importer.id,
+ 'access_level' => 30,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ }
+ end
+
+ it 'does nothing when there is no data' do
+ expect { subject.load(context, nil) }.not_to change(GroupMember, :count)
+ end
+
+ it 'creates the member' do
+ expect { subject.load(context, data) }.to change(GroupMember, :count).by(1)
+
+ member = group.members.last
+
+ expect(member.user).to eq(user_member)
+ expect(member.created_by).to eq(user_importer)
+ expect(member.access_level).to eq(30)
+ expect(member.created_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.updated_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.expires_at).to eq(nil)
+ 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 1a91f3d7a78..61950cdd9b0 100644
--- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
@@ -6,41 +6,34 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
describe '#run' do
let(:user) { create(:user) }
let(:parent) { create(:group) }
+ let(:bulk_import) { create(:bulk_import, user: user) }
let(:entity) do
create(
:bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
destination_namespace: parent.full_path
)
end
- let(:context) do
- BulkImports::Pipeline::Context.new(
- current_user: user,
- entity: entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:group_data) do
{
- 'data' => {
- 'group' => {
- 'name' => 'source_name',
- 'fullPath' => 'source/full/path',
- 'visibility' => 'private',
- 'projectCreationLevel' => 'developer',
- 'subgroupCreationLevel' => 'maintainer',
- 'description' => 'Group Description',
- 'emailsDisabled' => true,
- 'lfsEnabled' => false,
- 'mentionsDisabled' => true
- }
- }
+ 'name' => 'source_name',
+ 'full_path' => 'source/full/path',
+ 'visibility' => 'private',
+ 'project_creation_level' => 'developer',
+ 'subgroup_creation_level' => 'maintainer',
+ 'description' => 'Group Description',
+ 'emails_disabled' => true,
+ 'lfs_enabled' => false,
+ 'mentions_disabled' => true
}
end
- subject { described_class.new }
+ subject { described_class.new(context) }
before do
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
@@ -53,20 +46,20 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
it 'imports new group into destination group' do
group_path = 'my-destination-group'
- subject.run(context)
+ subject.run
imported_group = Group.find_by_path(group_path)
expect(imported_group).not_to be_nil
expect(imported_group.parent).to eq(parent)
expect(imported_group.path).to eq(group_path)
- expect(imported_group.description).to eq(group_data.dig('data', 'group', 'description'))
- expect(imported_group.visibility).to eq(group_data.dig('data', 'group', 'visibility'))
- expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data.dig('data', 'group', 'projectCreationLevel')])
- expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data.dig('data', 'group', 'subgroupCreationLevel')])
- expect(imported_group.lfs_enabled?).to eq(group_data.dig('data', 'group', 'lfsEnabled'))
- expect(imported_group.emails_disabled?).to eq(group_data.dig('data', 'group', 'emailsDisabled'))
- expect(imported_group.mentions_disabled?).to eq(group_data.dig('data', 'group', 'mentionsDisabled'))
+ expect(imported_group.description).to eq(group_data['description'])
+ expect(imported_group.visibility).to eq(group_data['visibility'])
+ expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data['project_creation_level']])
+ expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data['subgroup_creation_level']])
+ expect(imported_group.lfs_enabled?).to eq(group_data['lfs_enabled'])
+ expect(imported_group.emails_disabled?).to eq(group_data['emails_disabled'])
+ expect(imported_group.mentions_disabled?).to eq(group_data['mentions_disabled'])
end
end
@@ -87,8 +80,6 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
it 'has transformers' do
expect(described_class.transformers)
.to contain_exactly(
- { klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group] } },
- { klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil },
{ klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
{ klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil }
)
diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
new file mode 100644
index 00000000000..63f28916d9a
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:cursor) { 'cursor' }
+ let(:entity) do
+ create(
+ :bulk_import_entity,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Group',
+ destination_namespace: group.full_path,
+ group: group
+ )
+ end
+
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ subject { described_class.new(context) }
+
+ def extractor_data(title:, has_next_page:, cursor: nil)
+ data = [
+ {
+ 'title' => title,
+ 'description' => 'desc',
+ 'color' => '#428BCA'
+ }
+ ]
+
+ page_info = {
+ 'end_cursor' => cursor,
+ 'has_next_page' => has_next_page
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ end
+
+ describe '#run' do
+ it 'imports a group labels' do
+ first_page = extractor_data(title: 'label1', has_next_page: true, cursor: cursor)
+ last_page = extractor_data(title: 'label2', has_next_page: false)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor)
+ .to receive(:extract)
+ .and_return(first_page, last_page)
+ end
+
+ expect { subject.run }.to change(Label, :count).by(2)
+
+ label = group.labels.order(:created_at).last
+
+ expect(label.title).to eq('label2')
+ expect(label.description).to eq('desc')
+ expect(label.color).to eq('#428BCA')
+ end
+ end
+
+ describe '#after_run' do
+ context 'when extracted data has next page' do
+ it 'updates tracker information and runs pipeline again' do
+ data = extractor_data(title: 'label', has_next_page: true, cursor: cursor)
+
+ expect(subject).to receive(:run)
+
+ subject.after_run(data)
+
+ tracker = entity.trackers.find_by(relation: :labels)
+
+ expect(tracker.has_next_page).to eq(true)
+ expect(tracker.next_page).to eq(cursor)
+ end
+ end
+
+ context 'when extracted data has no next page' do
+ it 'updates tracker information and does not run pipeline' do
+ data = extractor_data(title: 'label', has_next_page: false)
+
+ expect(subject).not_to receive(:run)
+
+ subject.after_run(data)
+
+ tracker = entity.trackers.find_by(relation: :labels)
+
+ expect(tracker.has_next_page).to eq(false)
+ expect(tracker.next_page).to be_nil
+ end
+ end
+ end
+
+ describe 'pipeline parts' do
+ it { expect(described_class).to include_module(BulkImports::Pipeline) }
+ it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+
+ it 'has extractors' do
+ expect(described_class.get_extractor)
+ .to eq(
+ klass: BulkImports::Common::Extractors::GraphqlExtractor,
+ options: {
+ query: BulkImports::Groups::Graphql::GetLabelsQuery
+ }
+ )
+ end
+
+ it 'has transformers' do
+ expect(described_class.transformers)
+ .to contain_exactly(
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
+ )
+ end
+
+ it 'has loaders' do
+ expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::LabelsLoader, options: nil)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
new file mode 100644
index 00000000000..9f498f8154f
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do
+ let_it_be(:member_user1) { create(:user, email: 'email1@email.com') }
+ let_it_be(:member_user2) { create(:user, email: 'email2@email.com') }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:cursor) { 'cursor' }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ subject { described_class.new(context) }
+
+ describe '#run' do
+ it 'maps existing users to the imported group' do
+ first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor)
+ last_page = member_data(email: member_user2.email, has_next_page: false)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor)
+ .to receive(:extract)
+ .and_return(first_page, last_page)
+ end
+
+ expect { subject.run }.to change(GroupMember, :count).by(2)
+
+ members = group.members.map { |m| m.slice(:user_id, :access_level) }
+
+ expect(members).to contain_exactly(
+ { user_id: member_user1.id, access_level: 30 },
+ { user_id: member_user2.id, access_level: 30 }
+ )
+ end
+ end
+
+ describe 'pipeline parts' do
+ it { expect(described_class).to include_module(BulkImports::Pipeline) }
+ it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+
+ it 'has extractors' do
+ expect(described_class.get_extractor)
+ .to eq(
+ klass: BulkImports::Common::Extractors::GraphqlExtractor,
+ options: {
+ query: BulkImports::Groups::Graphql::GetMembersQuery
+ }
+ )
+ end
+
+ it 'has transformers' do
+ expect(described_class.transformers)
+ .to contain_exactly(
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
+ { klass: BulkImports::Groups::Transformers::MemberAttributesTransformer, options: nil }
+ )
+ end
+
+ it 'has loaders' do
+ expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::MembersLoader, options: nil)
+ end
+ end
+
+ def member_data(email:, has_next_page:, cursor: nil)
+ data = {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => 30
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+
+ page_info = {
+ 'end_cursor' => cursor,
+ 'has_next_page' => has_next_page
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ 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 e5a8ed7f47d..0404c52b895 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
@@ -14,13 +14,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
)
end
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- current_user: user,
- entity: parent_entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(parent_entity) }
let(:subgroup_data) do
[
@@ -31,7 +25,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
]
end
- subject { described_class.new }
+ subject { described_class.new(context) }
before do
allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor|
@@ -42,7 +36,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
end
it 'creates entities for the subgroups' do
- expect { subject.run(context) }.to change(BulkImports::Entity, :count).by(1)
+ expect { subject.run }.to change(BulkImports::Entity, :count).by(1)
subgroup_entity = BulkImports::Entity.last
diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
index 28a7859915d..5a7a51675d6 100644
--- a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
@@ -7,22 +7,18 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
let(:user) { create(:user) }
let(:parent) { create(:group) }
let(:group) { create(:group, name: 'My Source Group', parent: parent) }
+ let(:bulk_import) { create(:bulk_import, user: user) }
let(:entity) do
- instance_double(
- BulkImports::Entity,
+ create(
+ :bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: group.name,
destination_namespace: parent.full_path
)
end
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- current_user: user,
- entity: entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:data) do
{
@@ -85,16 +81,16 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
end
context 'when destination namespace is user namespace' do
- let(:entity) do
- instance_double(
- BulkImports::Entity,
+ it 'does not set parent id' do
+ entity = create(
+ :bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: group.name,
destination_namespace: user.namespace.full_path
)
- end
+ context = BulkImports::Pipeline::Context.new(entity)
- it 'does not set parent id' do
transformed_data = subject.transform(context, data)
expect(transformed_data).not_to have_key('parent_id')
diff --git a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
new file mode 100644
index 00000000000..f66c67fc6a2
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:secondary_email) { 'secondary@email.com' }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns nil when receives no data' do
+ expect(subject.transform(context, nil)).to eq(nil)
+ end
+
+ it 'returns nil when no user is found' do
+ expect(subject.transform(context, member_data)).to eq(nil)
+ expect(subject.transform(context, member_data(email: 'inexistent@email.com'))).to eq(nil)
+ end
+
+ context 'when the user is not confirmed' do
+ before do
+ user.update!(confirmed_at: nil)
+ end
+
+ it 'returns nil even when the primary email match' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+
+ it 'returns nil even when a secondary email match' do
+ user.emails << Email.new(email: secondary_email)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+ end
+
+ context 'when the user is confirmed' do
+ before do
+ user.update!(confirmed_at: Time.now.utc)
+ end
+
+ it 'finds the user by the primary email' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ it 'finds the user by the secondary email' do
+ user.emails << Email.new(email: secondary_email, confirmed_at: Time.now.utc)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ context 'format access level' do
+ it 'ignores record if no access level is given' do
+ data = member_data(email: user.email, access_level: nil)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+
+ it 'ignores record if is not a valid access level' do
+ data = member_data(email: user.email, access_level: 999)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+ end
+ end
+
+ def member_data(email: '', access_level: 30)
+ {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => access_level
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+ end
+end
diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb
index 87baf1b8026..b4fdb7b5e5b 100644
--- a/spec/lib/bulk_imports/importers/group_importer_spec.rb
+++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb
@@ -4,28 +4,29 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) }
+ let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import) }
- let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
+ let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
- let(:context) do
- BulkImports::Pipeline::Context.new(
- current_user: user,
- entity: bulk_import_entity,
- configuration: bulk_import_configuration
- )
- end
-
- subject { described_class.new(bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
before do
allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context)
end
+ subject { described_class.new(bulk_import_entity) }
+
describe '#execute' do
it 'starts the entity and run its pipelines' do
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
- expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
+
+ if Gitlab.ee?
+ expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context)
+ expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context)
+ end
subject.execute
@@ -33,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
context 'when failed' do
- let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) }
+ let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) }
it 'does not transition entity to finished state' do
allow(bulk_import_entity).to receive(:start!)
@@ -46,8 +47,8 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
def expect_to_run_pipeline(klass, context:)
- expect_next_instance_of(klass) do |pipeline|
- expect(pipeline).to receive(:run).with(context)
+ expect_next_instance_of(klass, context) do |pipeline|
+ expect(pipeline).to receive(:run)
end
end
end
diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb
index e9af6313ca4..c8c3fe3a861 100644
--- a/spec/lib/bulk_imports/pipeline/context_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/context_spec.rb
@@ -3,25 +3,29 @@
require 'spec_helper'
RSpec.describe BulkImports::Pipeline::Context do
- describe '#initialize' do
- it 'initializes with permitted attributes' do
- args = {
- current_user: create(:user),
- entity: create(:bulk_import_entity),
- configuration: create(:bulk_import_configuration)
- }
+ let(:group) { instance_double(Group) }
+ let(:user) { instance_double(User) }
+ let(:bulk_import) { instance_double(BulkImport, user: user, configuration: :config) }
- context = described_class.new(args)
+ let(:entity) do
+ instance_double(
+ BulkImports::Entity,
+ bulk_import: bulk_import,
+ group: group
+ )
+ end
+
+ subject { described_class.new(entity) }
- args.each do |k, v|
- expect(context.public_send(k)).to eq(v)
- end
- end
+ describe '#group' do
+ it { expect(subject.group).to eq(group) }
+ end
+
+ describe '#current_user' do
+ it { expect(subject.current_user).to eq(user) }
+ end
- context 'when invalid argument is passed' do
- it 'raises NoMethodError' do
- expect { described_class.new(test: 'test').test }.to raise_exception(NoMethodError)
- end
- end
+ describe '#current_user' do
+ it { expect(subject.configuration).to eq(bulk_import.configuration) }
end
end
diff --git a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
new file mode 100644
index 00000000000..25c5178227a
--- /dev/null
+++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Pipeline::ExtractedData do
+ let(:data) { 'data' }
+ let(:has_next_page) { true }
+ let(:cursor) { 'cursor' }
+ let(:page_info) do
+ {
+ 'has_next_page' => has_next_page,
+ 'end_cursor' => cursor
+ }
+ end
+
+ subject { described_class.new(data: data, page_info: page_info) }
+
+ describe '#has_next_page?' do
+ context 'when next page is present' do
+ it 'returns true' do
+ expect(subject.has_next_page?).to eq(true)
+ end
+ end
+
+ context 'when next page is not present' do
+ let(:has_next_page) { false }
+
+ it 'returns false' do
+ expect(subject.has_next_page?).to eq(false)
+ end
+ end
+ end
+
+ describe '#next_page' do
+ it 'returns next page cursor information' do
+ expect(subject.next_page).to eq(cursor)
+ end
+ end
+
+ describe '#each' do
+ context 'when block is present' do
+ it 'yields each data item' do
+ expect { |b| subject.each(&b) }.to yield_control
+ end
+ end
+
+ context 'when block is not present' do
+ it 'returns enumerator' do
+ expect(subject.each).to be_instance_of(Enumerator)
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 60833e83dcc..76e4e64a7d6 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -39,56 +39,94 @@ RSpec.describe BulkImports::Pipeline::Runner do
extractor BulkImports::Extractor
transformer BulkImports::Transformer
loader BulkImports::Loader
+
+ def after_run(_); end
end
stub_const('BulkImports::MyPipeline', pipeline)
end
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
+ let(:entity) { create(:bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'runs pipeline extractor, transformer, loader' do
- entries = [{ foo: :bar }]
+ extracted_data = BulkImports::Pipeline::ExtractedData.new(data: { foo: :bar })
expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor).to receive(:extract).with(context).and_return(entries)
+ expect(extractor)
+ .to receive(:extract)
+ .with(context)
+ .and_return(extracted_data)
end
expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first)
+ expect(transformer)
+ .to receive(:transform)
+ .with(context, extracted_data.data.first)
+ .and_return(extracted_data.data.first)
end
expect_next_instance_of(BulkImports::Loader) do |loader|
- expect(loader).to receive(:load).with(context, entries.first)
+ expect(loader)
+ .to receive(:load)
+ .with(context, extracted_data.data.first)
end
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
message: 'Pipeline started',
+ pipeline_class: 'BulkImports::MyPipeline'
+ )
+ expect(logger).to receive(:info)
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
pipeline_class: 'BulkImports::MyPipeline',
- bulk_import_entity_id: 1,
- bulk_import_entity_type: 'group'
+ pipeline_step: :extractor,
+ step_class: 'BulkImports::Extractor'
)
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', extractor: 'BulkImports::Extractor')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :transformer,
+ step_class: 'BulkImports::Transformer'
+ )
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', transformer: 'BulkImports::Transformer')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :loader,
+ step_class: 'BulkImports::Loader'
+ )
+ expect(logger).to receive(:info)
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :after_run
+ )
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', loader: 'BulkImports::Loader')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ message: 'Pipeline finished',
+ pipeline_class: 'BulkImports::MyPipeline'
+ )
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
context 'when exception is raised' do
let(:entity) { create(:bulk_import_entity, :created) }
- let(:context) { BulkImports::Pipeline::Context.new(entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
before do
allow_next_instance_of(BulkImports::Extractor) do |extractor|
@@ -97,12 +135,13 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
it 'logs import failure' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
failure = entity.failures.first
expect(failure).to be_present
expect(failure.pipeline_class).to eq('BulkImports::MyPipeline')
+ expect(failure.pipeline_step).to eq('extractor')
expect(failure.exception_class).to eq('StandardError')
expect(failure.exception_message).to eq('Error!')
end
@@ -113,7 +152,7 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
it 'marks entity as failed' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
expect(entity.failed?).to eq(true)
end
@@ -129,13 +168,13 @@ RSpec.describe BulkImports::Pipeline::Runner do
)
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
end
context 'when pipeline is not marked to abort on failure' do
it 'marks entity as failed' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
expect(entity.failed?).to eq(false)
end
@@ -144,25 +183,23 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
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
+ let(:entity) { create(:bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'logs and returns without execution' do
+ allow(entity).to receive(:failed?).and_return(true)
+
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'
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity'
)
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
end
end
diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb
index a2181a63335..696427bb8b6 100644
--- a/spec/lib/feature/gitaly_spec.rb
+++ b/spec/lib/feature/gitaly_spec.rb
@@ -3,35 +3,78 @@
require 'spec_helper'
RSpec.describe Feature::Gitaly do
- let(:feature_flag) { "mep_mep" }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+
+ before do
+ skip_feature_flags_yaml_validation
+ end
describe ".enabled?" do
- context 'when the gate is closed' do
- before do
- stub_feature_flags(gitaly_mep_mep: false)
+ context 'when the flag is set globally' do
+ let(:feature_flag) { 'global_flag' }
+
+ context 'when the gate is closed' do
+ before do
+ stub_feature_flags(gitaly_global_flag: false)
+ end
+
+ it 'returns false' do
+ expect(described_class.enabled?(feature_flag)).to be(false)
+ end
end
- it 'returns false' do
- expect(described_class.enabled?(feature_flag)).to be(false)
+ context 'when the flag defaults to on' do
+ it 'returns true' do
+ expect(described_class.enabled?(feature_flag)).to be(true)
+ end
end
end
- context 'when the flag defaults to on' do
- it 'returns true' do
- expect(described_class.enabled?(feature_flag)).to be(true)
+ context 'when the flag is enabled for a particular project' do
+ let(:feature_flag) { 'project_flag' }
+
+ before do
+ stub_feature_flags(gitaly_project_flag: project)
+ end
+
+ it 'returns true for that project' do
+ expect(described_class.enabled?(feature_flag, project)).to be(true)
+ end
+
+ it 'returns false for any other project' do
+ expect(described_class.enabled?(feature_flag, project_2)).to be(false)
+ end
+
+ it 'returns false when no project is passed' do
+ expect(described_class.enabled?(feature_flag)).to be(false)
end
end
end
describe ".server_feature_flags" do
before do
- stub_feature_flags(gitaly_mep_mep: true, foo: true)
+ stub_feature_flags(gitaly_global_flag: true, gitaly_project_flag: project, non_gitaly_flag: false)
end
subject { described_class.server_feature_flags }
- it { is_expected.to be_a(Hash) }
- it { is_expected.to eq("gitaly-feature-mep-mep" => "true") }
+ it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do
+ expect(subject).to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false')
+ end
+
+ context 'when a project is passed' do
+ it 'returns the value for the flag on the given project' do
+ expect(described_class.server_feature_flags(project))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'true')
+
+ expect(described_class.server_feature_flags(project_2))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false')
+ end
+ end
context 'when table does not exist' do
before do
diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb
index 9b736a30c7e..44c30d1f596 100644
--- a/spec/lib/gitlab/access/branch_protection_spec.rb
+++ b/spec/lib/gitlab/access/branch_protection_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Access::BranchProtection do
- describe '#any?' do
- using RSpec::Parameterized::TableSyntax
+ using RSpec::Parameterized::TableSyntax
+ describe '#any?' do
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true
@@ -19,8 +19,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#developer_can_push?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true
@@ -36,8 +34,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#developer_can_merge?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false
@@ -53,8 +49,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#fully_protected?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false
diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
index b7660462b0d..d022c629458 100644
--- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
@@ -19,7 +19,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
describe '#severity' do
subject { parsed_payload.severity }
- it_behaves_like 'parsable alert payload field with fallback', 'critical', 'severity'
+ context 'when set' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:raw_payload) { { 'severity' => payload_severity } }
+
+ where(:payload_severity, :expected_severity) do
+ 'critical' | :critical
+ 'high' | :high
+ 'medium' | :medium
+ 'low' | :low
+ 'info' | :info
+
+ 'CRITICAL' | :critical
+ 'cRiTiCaL' | :critical
+
+ 'unmapped' | nil
+ 1 | nil
+ nil | nil
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_severity) }
+ end
+ end
+
+ context 'without key' do
+ it { is_expected.to be_nil }
+ end
end
describe '#monitoring_tool' do
diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
index 457db58a28b..f574f5ba6a3 100644
--- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
@@ -156,8 +156,6 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
end
describe '#gitlab_fingerprint' do
- subject { parsed_payload.gitlab_fingerprint }
-
let(:raw_payload) do
{
'startsAt' => Time.current.to_s,
@@ -166,6 +164,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
}
end
+ subject { parsed_payload.gitlab_fingerprint }
+
it 'returns a fingerprint' do
plain_fingerprint = [
parsed_payload.send(:starts_at_raw),
@@ -237,4 +237,63 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
it { is_expected.to be_falsey }
end
end
+
+ describe '#severity' do
+ subject { parsed_payload.severity }
+
+ context 'when set' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:raw_payload) { { 'labels' => { 'severity' => payload_severity } } }
+
+ where(:payload_severity, :expected_severity) do
+ 'critical' | :critical
+ 'high' | :high
+ 'medium' | :medium
+ 'low' | :low
+ 'info' | :info
+
+ 's1' | :critical
+ 's2' | :high
+ 's3' | :medium
+ 's4' | :low
+ 's5' | :info
+ 'p1' | :critical
+ 'p2' | :high
+ 'p3' | :medium
+ 'p4' | :low
+ 'p5' | :info
+
+ 'CRITICAL' | :critical
+ 'cRiTiCaL' | :critical
+ 'S1' | :critical
+
+ 'unmapped' | nil
+ 1 | nil
+ nil | nil
+
+ 'debug' | :info
+ 'information' | :info
+ 'notice' | :info
+ 'warn' | :low
+ 'warning' | :low
+ 'minor' | :low
+ 'error' | :medium
+ 'major' | :high
+ 'emergency' | :critical
+ 'fatal' | :critical
+
+ 'alert' | :medium
+ 'page' | :high
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_severity) }
+ end
+ end
+
+ context 'without key' do
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb
index 44b55e228c5..7c129a8a48e 100644
--- a/spec/lib/gitlab/alert_management/payload_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload_spec.rb
@@ -56,5 +56,20 @@ RSpec.describe Gitlab::AlertManagement::Payload do
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic }
end
end
+
+ context 'with integration specified by caller' do
+ let(:integration) { instance_double(AlertManagement::HttpIntegration) }
+
+ subject { described_class.parse(project, payload, integration: integration) }
+
+ it 'passes an integration to a specific payload' do
+ expect(::Gitlab::AlertManagement::Payload::Generic)
+ .to receive(:new)
+ .with(project: project, payload: payload, integration: integration)
+ .and_call_original
+
+ subject
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
index 68ce48a70ea..e933fd8352e 100644
--- a/spec/lib/gitlab/api_authentication/token_locator_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
@@ -51,5 +51,26 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
end
end
end
+
+ context 'with :http_token' do
+ let(:type) { :http_token }
+
+ context 'without credentials' do
+ let(:request) { double(headers: {}) }
+
+ it 'returns nil' do
+ expect(subject).to be(nil)
+ end
+ end
+
+ context 'with credentials' do
+ let(:password) { 'bar' }
+ let(:request) { double(headers: { "Authorization" => password }) }
+
+ it 'returns the credentials' do
+ expect(subject.password).to eq(password)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
index 0028fb080ac..97a7c8ba7cf 100644
--- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
@@ -47,8 +47,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
subject { resolver.resolve(raw) }
- context 'with :personal_access_token' do
- let(:type) { :personal_access_token }
+ context 'with :personal_access_token_with_username' do
+ let(:type) { :personal_access_token_with_username }
let(:token) { personal_access_token }
context 'with valid credentials' do
@@ -62,10 +62,16 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an unauthorized request'
end
+
+ context 'with no username' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an unauthorized request'
+ end
end
- context 'with :job_token' do
- let(:type) { :job_token }
+ context 'with :job_token_with_username' do
+ let(:type) { :job_token_with_username }
let(:token) { ci_job }
context 'with valid credentials' do
@@ -93,8 +99,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
end
end
- context 'with :deploy_token' do
- let(:type) { :deploy_token }
+ context 'with :deploy_token_with_username' do
+ let(:type) { :deploy_token_with_username }
let(:token) { deploy_token }
context 'with a valid deploy token' do
@@ -109,6 +115,51 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an unauthorized request'
end
end
+
+ context 'with :personal_access_token' do
+ let(:type) { :personal_access_token }
+ let(:token) { personal_access_token }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
+
+ context 'with :job_token' do
+ let(:type) { :job_token }
+ let(:token) { ci_job }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+
+ context 'when the job is not running' do
+ let(:raw) { username_and_password(nil, ci_job_done.token) }
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'with an invalid job token' do
+ let(:raw) { username_and_password(nil, "not a valid CI job token") }
+
+ it_behaves_like 'an unauthorized request'
+ end
+ end
+
+ context 'with :deploy_token' do
+ let(:type) { :deploy_token }
+ let(:token) { deploy_token }
+
+ context 'with a valid deploy token' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
end
def username_and_password(username, password)
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 36e4decdead..08510d4652b 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -510,6 +510,73 @@ module Gitlab
expect(render(input, context)).to include(output.strip)
end
+
+ it 'does not convert a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <pre>blockdiag {
+ Kroki -&gt; generates -&gt; "Block diagrams";
+ Kroki -&gt; is -&gt; "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }</pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+
+ context 'with Kroki and BlockDiag (additional format) 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')
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
+ end
+
+ it 'converts a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 775f8f056b5..cddcaf09b74 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -7,7 +7,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do
include HttpBasicAuthHelpers
# Create the feed_token and static_object_token for the user
- let_it_be(:user) { create(:user).tap(&:feed_token).tap(&:static_object_token) }
+ let_it_be(:user, freeze: true) { create(:user).tap(&:feed_token).tap(&:static_object_token) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) }
+
+ let_it_be(:project, freeze: true) { create(:project, :private) }
+ let_it_be(:pipeline, freeze: true) { create(:ci_pipeline, project: project) }
+ let_it_be(:job, freeze: true) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ let_it_be(:failed_job, freeze: true) { create(:ci_build, :failed, pipeline: pipeline, user: user) }
+
+ let_it_be(:project2, freeze: true) { create(:project, :private) }
+ let_it_be(:pipeline2, freeze: true) { create(:ci_pipeline, project: project2) }
+ let_it_be(:job2, freeze: true) { create(:ci_build, :running, pipeline: pipeline2, user: user) }
+
let(:env) do
{
'rack.input' => ''
@@ -15,6 +26,12 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
let(:request) { ActionDispatch::Request.new(env) }
+ let(:params) { {} }
+
+ before_all do
+ project.add_developer(user)
+ project2.add_developer(user)
+ end
def set_param(key, value)
request.update_param(key, value)
@@ -28,75 +45,93 @@ RSpec.describe Gitlab::Auth::AuthFinders do
env.merge!(basic_auth_header(username, password))
end
- shared_examples 'find user from job token' do
+ def set_bearer_token(token)
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
+ end
+
+ shared_examples 'find user from job token' do |without_job_token_allowed|
context 'when route is allowed to be authenticated' do
let(:route_authentication_setting) { { job_token_allowed: true } }
- it "returns an Unauthorized exception for an invalid token" do
- set_token('invalid token')
+ context 'for an invalid token' do
+ let(:token) { 'invalid token' }
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it "returns an Unauthorized exception" do
+ expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(@current_authenticated_job).to be_nil
+ end
end
context 'with a running job' do
- before do
- job.update!(status: :running)
- end
-
- it 'return user if token is valid' do
- set_token(job.token)
+ let(:token) { job.token }
+ it 'return user' do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
end
end
context 'with a job that is not running' do
- before do
- job.update!(status: :failed)
- end
+ let(:token) { failed_job.token }
it 'returns an Unauthorized exception' do
- set_token(job.token)
-
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(@current_authenticated_job).to be_nil
+ end
+ end
+ end
+
+ context 'when route is not allowed to be authenticated' do
+ let(:route_authentication_setting) { { job_token_allowed: false } }
+
+ context 'with a running job' do
+ let(:token) { job.token }
+
+ if without_job_token_allowed == :error
+ it 'returns an Unauthorized exception' do
+ expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(@current_authenticated_job).to be_nil
+ end
+ elsif without_job_token_allowed == :user
+ it 'returns the user' do
+ expect(subject).to eq(user)
+ expect(@current_authenticated_job).to eq job
+ end
+ else
+ it 'returns nil' do
+ is_expected.to be_nil
+ expect(@current_authenticated_job).to be_nil
+ end
end
end
end
end
describe '#find_user_from_bearer_token' do
- let_it_be_with_reload(:job) { create(:ci_build, user: user) }
-
subject { find_user_from_bearer_token }
context 'when the token is passed as an oauth token' do
- def set_token(token)
- env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
+ before do
+ set_bearer_token(token)
end
- context 'with a job token' do
- it_behaves_like 'find user from job token'
- end
+ it_behaves_like 'find user from job token', :error
+ end
- context 'with oauth token' do
- let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
- let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api').token }
+ context 'with oauth token' do
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+ let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
- before do
- set_token(token)
- end
-
- it { is_expected.to eq user }
+ before do
+ set_bearer_token(doorkeeper_access_token.token)
end
+
+ it { is_expected.to eq user }
end
context 'with a personal access token' do
- let_it_be(:pat) { create(:personal_access_token, user: user) }
- let(:token) { pat.token }
-
before do
- env[described_class::PRIVATE_TOKEN_HEADER] = pat.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it { is_expected.to eq user }
@@ -277,7 +312,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#deploy_token_from_request' do
- let_it_be(:deploy_token) { create(:deploy_token) }
+ let_it_be(:deploy_token, freeze: true) { create(:deploy_token) }
let_it_be(:route_authentication_setting) { { deploy_token_allowed: true } }
subject { deploy_token_from_request }
@@ -293,11 +328,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with deploy token headers' do
- before do
- set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
- end
+ context 'with valid deploy token' do
+ before do
+ set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
+ end
- it { is_expected.to eq deploy_token }
+ it { is_expected.to eq deploy_token }
+ end
it_behaves_like 'an unauthenticated route'
@@ -311,17 +348,19 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with oauth headers' do
- before do
- set_header('HTTP_AUTHORIZATION', "Bearer #{deploy_token.token}")
- end
+ context 'with valid token' do
+ before do
+ set_bearer_token(deploy_token.token)
+ end
- it { is_expected.to eq deploy_token }
+ it { is_expected.to eq deploy_token }
- it_behaves_like 'an unauthenticated route'
+ it_behaves_like 'an unauthenticated route'
+ end
context 'with invalid token' do
before do
- set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
+ set_bearer_token('invalid_token')
end
it { is_expected.to be_nil }
@@ -348,8 +387,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_access_token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header('SCRIPT_NAME', 'url.atom')
end
@@ -374,24 +411,34 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with OAuth headers' do
- it 'returns user' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+ context 'with valid personal access token' do
+ before do
+ set_bearer_token(personal_access_token.token)
+ end
- expect(find_user_from_access_token).to eq user
+ it 'returns user' do
+ expect(find_user_from_access_token).to eq user
+ end
end
- it 'returns exception if invalid personal_access_token' do
- env['HTTP_AUTHORIZATION'] = 'Bearer invalid_20byte_token'
+ context 'with invalid personal_access_token' do
+ before do
+ set_bearer_token('invalid_20byte_token')
+ end
- expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it 'returns exception' do
+ expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
context 'when using a non-prefixed access token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :no_prefix, user: user) }
- it 'returns user' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+ before do
+ set_bearer_token(personal_access_token.token)
+ end
+ it 'returns user' do
expect(find_user_from_access_token).to eq user
end
end
@@ -399,8 +446,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_web_access_token' do
- let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
end
@@ -451,9 +496,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'when the token has read_api scope' do
- before do
- personal_access_token.update!(scopes: ['read_api'])
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user, scopes: ['read_api']) }
+ before do
set_header('SCRIPT_NAME', '/api/endpoint')
end
@@ -481,8 +526,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_personal_access_token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header('SCRIPT_NAME', 'url.atom')
end
@@ -516,21 +559,23 @@ RSpec.describe Gitlab::Auth::AuthFinders do
describe '#find_oauth_access_token' do
let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
- let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+ let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
context 'passed as header' do
- it 'returns token if valid oauth_access_token' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{token.token}")
+ before do
+ set_bearer_token(doorkeeper_access_token.token)
+ end
- expect(find_oauth_access_token.token).to eq token.token
+ it 'returns token if valid oauth_access_token' do
+ expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token
end
end
context 'passed as param' do
it 'returns user if valid oauth_access_token' do
- set_param(:access_token, token.token)
+ set_param(:access_token, doorkeeper_access_token.token)
- expect(find_oauth_access_token.token).to eq token.token
+ expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token
end
end
@@ -538,10 +583,14 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_oauth_access_token).to be_nil
end
- it 'returns exception if invalid oauth_access_token' do
- set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
+ context 'with invalid token' do
+ before do
+ set_bearer_token('invalid_token')
+ end
- expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it 'returns exception if invalid oauth_access_token' do
+ expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
end
@@ -551,7 +600,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'access token is valid' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'finds the token from basic auth' do
@@ -572,8 +620,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'route_setting is not set' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
it 'returns nil' do
auth_header_with(personal_access_token.token)
@@ -582,7 +628,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'route_setting is not correct' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
it 'returns nil' do
@@ -629,44 +674,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do
context 'with CI username' do
let(:username) { ::Gitlab::Auth::CI_JOB_USER }
- let_it_be(:user) { create(:user) }
- let_it_be(:build) { create(:ci_build, user: user, status: :running) }
-
- it 'returns nil without password' do
- set_basic_auth_header(username, nil)
-
- is_expected.to be_nil
- end
-
- it 'returns user with valid token' do
- set_basic_auth_header(username, build.token)
-
- is_expected.to eq user
- expect(@current_authenticated_job).to eq build
- end
-
- it 'raises error with invalid token' do
- set_basic_auth_header(username, 'token')
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ before do
+ set_basic_auth_header(username, token)
end
- it 'returns exception if the job is not running' do
- set_basic_auth_header(username, build.token)
- build.success!
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
- end
+ it_behaves_like 'find user from job token', :user
end
end
describe '#validate_access_token!' do
subject { validate_access_token! }
- let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
-
context 'with a job token' do
- let_it_be(:job) { create(:ci_build, user: user, status: :running) }
let(:route_authentication_setting) { { job_token_allowed: true } }
before do
@@ -684,6 +703,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'token is not valid' do
+ let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
+
before do
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
end
@@ -706,7 +727,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with impersonation token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :impersonation, user: user) }
context 'when impersonation is disabled' do
before do
@@ -722,96 +743,30 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_job_token' do
- let_it_be(:job) { create(:ci_build, user: user, status: :running) }
- let(:route_authentication_setting) { { job_token_allowed: true } }
-
subject { find_user_from_job_token }
- context 'when the job token is in the headers' do
- it 'returns the user if valid job token' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
-
- is_expected.to eq(user)
- expect(@current_authenticated_job).to eq(job)
- end
-
- it 'returns nil without job token' do
- set_header(described_class::JOB_TOKEN_HEADER, '')
-
- is_expected.to be_nil
- end
-
- it 'returns exception if invalid job token' do
- set_header(described_class::JOB_TOKEN_HEADER, 'invalid token')
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ context 'when the token is in the headers' do
+ before do
+ set_header(described_class::JOB_TOKEN_HEADER, token)
end
- it 'returns exception if the job is not running' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
- job.success!
+ it_behaves_like 'find user from job token'
+ end
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ context 'when the token is in the job_token param' do
+ before do
+ set_param(described_class::JOB_TOKEN_PARAM, token)
end
- context 'when route is not allowed to be authenticated' do
- let(:route_authentication_setting) { { job_token_allowed: false } }
-
- it 'sets current_user to nil' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
-
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
-
- is_expected.to be_nil
- end
- end
+ it_behaves_like 'find user from job token'
end
- context 'when the job token is in the params' do
- shared_examples 'job token params' do |token_key_name|
- before do
- set_param(token_key_name, token)
- end
-
- context 'with valid job token' do
- let(:token) { job.token }
-
- it 'returns the user' do
- is_expected.to eq(user)
- expect(@current_authenticated_job).to eq(job)
- end
- end
-
- context 'with empty job token' do
- let(:token) { '' }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
-
- context 'with invalid job token' do
- let(:token) { 'invalid token' }
-
- it 'returns exception' do
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
- end
- end
-
- context 'when route is not allowed to be authenticated' do
- let(:route_authentication_setting) { { job_token_allowed: false } }
- let(:token) { job.token }
-
- it 'sets current_user to nil' do
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
-
- is_expected.to be_nil
- end
- end
+ context 'when the token is in the token param' do
+ before do
+ set_param(described_class::RUNNER_JOB_TOKEN_PARAM, token)
end
- it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM
- it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM
+ it_behaves_like 'find user from job token'
end
context 'when the job token is provided via basic auth' do
@@ -834,7 +789,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#cluster_agent_token_from_authorization_token' do
- let_it_be(:agent_token) { create(:cluster_agent_token) }
+ let_it_be(:agent_token, freeze: true) { create(:cluster_agent_token) }
context 'when route_setting is empty' do
it 'returns nil' do
@@ -884,7 +839,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_runner_from_token' do
- let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner, freeze: true) { create(:ci_runner) }
context 'with API requests' do
before do
diff --git a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
index 3d782272d7e..f23fdd3fbcb 100644
--- a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
@@ -19,6 +19,9 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin
before do
stub_rack_attack_setting(options)
+ Rack::Attack.reset!
+ Rack::Attack.clear_configuration
+ Gitlab::RackAttack.configure(Rack::Attack)
end
after do
diff --git a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
deleted file mode 100644
index 928aade4008..00000000000
--- a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# 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/u2f_webauthn_converter_spec.rb b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb
new file mode 100644
index 00000000000..deddc7f5294
--- /dev/null
+++ b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::U2fWebauthnConverter do
+ let_it_be(:u2f_registration) do
+ device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
+ create(:u2f_registration, name: 'u2f_device',
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw))
+ end
+
+ it 'converts u2f registration' do
+ webauthn_credential = WebAuthn::U2fMigrator.new(
+ app_id: Gitlab.config.gitlab.url,
+ certificate: u2f_registration.certificate,
+ key_handle: u2f_registration.key_handle,
+ public_key: u2f_registration.public_key,
+ counter: u2f_registration.counter
+ ).credential
+
+ converted_webauthn = described_class.new(u2f_registration).convert
+
+ expect(converted_webauthn).to(
+ include(user_id: u2f_registration.user_id,
+ credential_xid: Base64.strict_encode64(webauthn_credential.id)))
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..708e5e21dbe
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210210093901 do
+ let(:projects) { table(:projects) }
+ let(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'updates project updated_at column if they were moved to a different repository storage' do
+ freeze_time do
+ project_1 = projects.create!(id: 1, namespace_id: namespace.id, updated_at: 1.day.ago)
+ project_2 = projects.create!(id: 2, namespace_id: namespace.id, updated_at: Time.current)
+ original_project_3_updated_at = 2.minutes.from_now
+ project_3 = projects.create!(id: 3, namespace_id: namespace.id, updated_at: original_project_3_updated_at)
+ original_project_4_updated_at = 10.days.ago
+ project_4 = projects.create!(id: 4, namespace_id: namespace.id, updated_at: original_project_4_updated_at)
+
+ repository_storage_move_1 = project_repository_storage_moves.create!(project_id: project_1.id, updated_at: 2.hours.ago, source_storage_name: 'default', destination_storage_name: 'default')
+ repository_storage_move_2 = project_repository_storage_moves.create!(project_id: project_2.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(project_id: project_3.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
+
+ subject.perform([1, 2, 3, 4, non_existing_record_id])
+
+ expect(project_1.reload.updated_at).to eq(repository_storage_move_1.updated_at + 1.second)
+ expect(project_2.reload.updated_at).to eq(repository_storage_move_2.updated_at + 1.second)
+ expect(project_3.reload.updated_at).to eq(original_project_3_updated_at)
+ expect(project_4.reload.updated_at).to eq(original_project_4_updated_at)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
new file mode 100644
index 00000000000..f724b007e01
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20201128210234 do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
+ let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") }
+ let!(:issue2) { table(:issues).create!(id: 2, project_id: project.id, service_desk_reply_to: "b@gitlab.com") }
+ let(:issue_email_participants) { table(:issue_email_participants) }
+
+ describe '#perform' do
+ it 'migrates email addresses from service desk issues', :aggregate_failures do
+ expect { subject.perform(1, 2) }.to change { issue_email_participants.count }.by(2)
+
+ expect(issue_email_participants.find_by(issue_id: 1).email).to eq("a@gitlab.com")
+ expect(issue_email_participants.find_by(issue_id: 2).email).to eq("b@gitlab.com")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
new file mode 100644
index 00000000000..47e1d4620cd
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindings do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
+ let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'vulnerability-identifier',
+ external_id: 'vulnerability-identifier',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'vulnerability identifier')
+ end
+
+ let!(:first_finding) do
+ create_finding!(
+ uuid: "test1",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:first_duplicate) do
+ create_finding!(
+ uuid: "test2",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:second_duplicate) do
+ create_finding!(
+ uuid: "test3",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner3.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:unrelated_finding) do
+ create_finding!(
+ uuid: "unreleated_finding",
+ vulnerability_id: nil,
+ report_type: 1,
+ location_fingerprint: 'random_location_fingerprint',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: unrelated_scanner.id,
+ project_id: project.id
+ )
+ end
+
+ subject { described_class.new.perform(first_finding.id, unrelated_finding.id) }
+
+ before do
+ stub_const("#{described_class}::DELETE_BATCH_SIZE", 1)
+ end
+
+ it "removes entries which would result in duplicate UUIDv5" do
+ expect(vulnerability_findings.count).to eq(4)
+
+ expect { subject }.to change { vulnerability_findings.count }.from(4).to(2)
+
+ expect(vulnerability_findings.pluck(:id)).to eq([second_duplicate.id, unrelated_finding.id])
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ vulnerability_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ user_type: user_type,
+ confirmed_at: confirmed_at
+ )
+ end
+end
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 052a01a8dd8..5b20572578c 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::BackgroundMigration do
expect(described_class).to receive(:perform)
.with('Foo', [10, 20])
- described_class.steal('Foo') { |(arg1, arg2)| arg1 == 10 && arg2 == 20 }
+ described_class.steal('Foo') { |job| job.args.second.first == 10 && job.args.second.second == 20 }
end
it 'does not steal jobs that do not match the predicate' do
diff --git a/spec/lib/gitlab/changelog/ast_spec.rb b/spec/lib/gitlab/changelog/ast_spec.rb
new file mode 100644
index 00000000000..fa15ac979fe
--- /dev/null
+++ b/spec/lib/gitlab/changelog/ast_spec.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::AST::Identifier do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ data = { 'number' => 10 }
+
+ expect(described_class.new('number').evaluate(state, data)).to eq(10)
+ end
+
+ it 'returns nil if the key is not set' do
+ expect(described_class.new('number').evaluate(state, {})).to be_nil
+ end
+
+ it 'returns nil if the input is not a Hash' do
+ expect(described_class.new('number').evaluate(state, 45)).to be_nil
+ end
+
+ it 'returns the current data when using the special identifier "it"' do
+ expect(described_class.new('it').evaluate(state, 45)).to eq(45)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Integer do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ expect(described_class.new(0).evaluate(state, [10])).to eq(10)
+ end
+
+ it 'returns nil if the index is not set' do
+ expect(described_class.new(1).evaluate(state, [10])).to be_nil
+ end
+
+ it 'returns nil if the input is not an Array' do
+ expect(described_class.new(0).evaluate(state, {})).to be_nil
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Selector do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+ let(:data) { { 'numbers' => [10] } }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ ident = Gitlab::Changelog::AST::Identifier.new('numbers')
+ int = Gitlab::Changelog::AST::Integer.new(0)
+
+ expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10)
+ end
+
+ it 'evaluates a selector that returns nil' do
+ int = Gitlab::Changelog::AST::Integer.new(0)
+
+ expect(described_class.new([int]).evaluate(state, data)).to be_nil
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Variable do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+ let(:data) { { 'numbers' => [10] } }
+
+ describe '#evaluate' do
+ it 'evaluates a variable' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{{numbers.0}}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('10')
+ end
+
+ it 'evaluates an undefined variable' do
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform('{{foobar}}').nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('')
+ end
+
+ it 'evaluates the special variable "it"' do
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform('{{it}}').nodes[0]
+
+ expect(node.evaluate(state, data)).to eq(data.to_s)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Expressions do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates all expressions' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{{number}}foo')
+
+ expect(node.evaluate(state, { 'number' => 10 })).to eq('10foo')
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Text do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'returns the text' do
+ expect(described_class.new('foo').evaluate(state, {})).to eq('foo')
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::If do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a truthy if expression without an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => true })).to eq('foo')
+ end
+
+ it 'evaluates a falsy if expression without an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => false })).to eq('')
+ end
+
+ it 'evaluates a falsy if expression with an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% else %}bar{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => false })).to eq('bar')
+ end
+ end
+
+ describe '#truthy?' do
+ it 'returns true for a non-empty String' do
+ expect(described_class.new.truthy?('foo')).to eq(true)
+ end
+
+ it 'returns true for a non-empty Array' do
+ expect(described_class.new.truthy?([10])).to eq(true)
+ end
+
+ it 'returns true for a Boolean true' do
+ expect(described_class.new.truthy?(true)).to eq(true)
+ end
+
+ it 'returns false for an empty String' do
+ expect(described_class.new.truthy?('')).to eq(false)
+ end
+
+ it 'returns true for an empty Array' do
+ expect(described_class.new.truthy?([])).to eq(false)
+ end
+
+ it 'returns false for a Boolean false' do
+ expect(described_class.new.truthy?(false)).to eq(false)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Each do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates the expression' do
+ data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] }
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% each animals %}{{name}}{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('CatDog')
+ end
+
+ it 'returns an empty string when the input is not a collection' do
+ data = { 'animals' => 10 }
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% each animals %}{{name}}{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('')
+ end
+
+ it 'disallows too many nested loops' do
+ data = {
+ 'foo' => [
+ {
+ 'bar' => [
+ {
+ 'baz' => [
+ {
+ 'quix' => [
+ {
+ 'foo' => [{ 'name' => 'Alice' }]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+ template = <<~TPL
+ {% each foo %}
+ {% each bar %}
+ {% each baz %}
+ {% each quix %}
+ {% each foo %}
+ {{name}}
+ {% end %}
+ {% end %}
+ {% end %}
+ {% end %}
+ {% end %}
+ TPL
+
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform(template).nodes[0]
+
+ expect { node.evaluate(state, data) }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/committer_spec.rb b/spec/lib/gitlab/changelog/committer_spec.rb
new file mode 100644
index 00000000000..1e04fe346cb
--- /dev/null
+++ b/spec/lib/gitlab/changelog/committer_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Committer do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:committer) { described_class.new(project, user) }
+ let(:config) { Gitlab::Changelog::Config.new(project) }
+
+ describe '#commit' do
+ context "when the release isn't in the changelog" do
+ it 'commits the changes' do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+
+ content = project.repository.blob_at('master', 'CHANGELOG.md').data
+
+ expect(content).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2020-01-01)
+
+ No changes.
+ MARKDOWN
+ end
+ end
+
+ context 'when the release is already in the changelog' do
+ it "doesn't commit the changes" do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ 2.times do
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end
+
+ content = project.repository.blob_at('master', 'CHANGELOG.md').data
+
+ expect(content).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2020-01-01)
+
+ No changes.
+ MARKDOWN
+ end
+ end
+
+ context 'when committing the changes fails' do
+ it 'retries the operation' do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ service = instance_spy(Files::MultiService)
+ errored = false
+
+ allow(Files::MultiService)
+ .to receive(:new)
+ .and_return(service)
+
+ allow(service).to receive(:execute) do
+ if errored
+ { status: :success }
+ else
+ errored = true
+ { status: :error }
+ end
+ end
+
+ expect do
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end.not_to raise_error
+ end
+ end
+
+ context "when the changelog changes before saving the changes" do
+ it 'raises a Error' do
+ release1 = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ release2 = Gitlab::Changelog::Release
+ .new(version: '2.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ # This creates the initial commit we'll later use to see if the
+ # changelog changed before saving our changes.
+ committer.commit(
+ release: release1,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Initial commit'
+ )
+
+ allow(Gitlab::Git::Commit)
+ .to receive(:last_for_path)
+ .with(
+ project.repository,
+ 'master',
+ 'CHANGELOG.md',
+ literal_pathspec: true
+ )
+ .and_return(double(:commit, sha: 'foo'))
+
+ expect do
+ committer.commit(
+ release: release2,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end.to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb
new file mode 100644
index 00000000000..51988acf3d1
--- /dev/null
+++ b/spec/lib/gitlab/changelog/config_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Config do
+ let(:project) { build_stubbed(:project) }
+
+ describe '.from_git' do
+ it 'retrieves the configuration from Git' do
+ allow(project.repository)
+ .to receive(:changelog_config)
+ .and_return("---\ndate_format: '%Y'")
+
+ expect(described_class)
+ .to receive(:from_hash)
+ .with(project, 'date_format' => '%Y')
+
+ described_class.from_git(project)
+ end
+
+ it 'returns the default configuration when no YAML file exists in Git' do
+ allow(project.repository)
+ .to receive(:changelog_config)
+ .and_return(nil)
+
+ expect(described_class)
+ .to receive(:new)
+ .with(project)
+
+ described_class.from_git(project)
+ end
+ end
+
+ describe '.from_hash' do
+ it 'sets the configuration according to a Hash' do
+ config = described_class.from_hash(
+ project,
+ 'date_format' => 'foo',
+ 'template' => 'bar',
+ 'categories' => { 'foo' => 'bar' }
+ )
+
+ expect(config.date_format).to eq('foo')
+ expect(config.template)
+ .to be_instance_of(Gitlab::Changelog::AST::Expressions)
+
+ expect(config.categories).to eq({ 'foo' => 'bar' })
+ end
+
+ it 'raises Error when the categories are not a Hash' do
+ expect { described_class.from_hash(project, 'categories' => 10) }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+
+ describe '#contributor?' do
+ it 'returns true if a user is a contributor' do
+ user = build_stubbed(:author)
+
+ allow(project.team).to receive(:contributor?).with(user).and_return(true)
+
+ expect(described_class.new(project).contributor?(user)).to eq(true)
+ end
+
+ it "returns true if a user isn't a contributor" do
+ user = build_stubbed(:author)
+
+ allow(project.team).to receive(:contributor?).with(user).and_return(false)
+
+ expect(described_class.new(project).contributor?(user)).to eq(false)
+ end
+ end
+
+ describe '#category' do
+ it 'returns the name of a category' do
+ config = described_class.new(project)
+
+ config.categories['foo'] = 'Foo'
+
+ expect(config.category('foo')).to eq('Foo')
+ end
+
+ it 'returns the raw category name when no alternative name is configured' do
+ config = described_class.new(project)
+
+ expect(config.category('bla')).to eq('bla')
+ end
+ end
+
+ describe '#format_date' do
+ it 'formats a date according to the configured date format' do
+ config = described_class.new(project)
+ time = Time.utc(2021, 1, 5)
+
+ expect(config.format_date(time)).to eq('2021-01-05')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/generator_spec.rb b/spec/lib/gitlab/changelog/generator_spec.rb
new file mode 100644
index 00000000000..bc4a7c5dd6b
--- /dev/null
+++ b/spec/lib/gitlab/changelog/generator_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Generator do
+ describe '#add' do
+ let(:project) { build_stubbed(:project) }
+ let(:author) { build_stubbed(:user) }
+ let(:commit) { build_stubbed(:commit) }
+ let(:config) { Gitlab::Changelog::Config.new(project) }
+
+ it 'generates the Markdown for the first release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '1.0.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new('')
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for a newer release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '2.0.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for a patch release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '1.1.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.1.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for an old release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '0.5.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+
+ ## 0.5.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+ MARKDOWN
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/parser_spec.rb b/spec/lib/gitlab/changelog/parser_spec.rb
new file mode 100644
index 00000000000..1d353f5eb35
--- /dev/null
+++ b/spec/lib/gitlab/changelog/parser_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Parser do
+ let(:parser) { described_class.new }
+
+ describe '#root' do
+ it 'parses an empty template' do
+ expect(parser.root).to parse('')
+ end
+
+ it 'parses a variable with a single identifier step' do
+ expect(parser.root).to parse('{{foo}}')
+ end
+
+ it 'parses a variable with a single integer step' do
+ expect(parser.root).to parse('{{0}}')
+ end
+
+ it 'parses a variable with multiple selector steps' do
+ expect(parser.root).to parse('{{foo.bar}}')
+ end
+
+ it 'parses a variable with an integer selector step' do
+ expect(parser.root).to parse('{{foo.bar.0}}')
+ end
+
+ it 'parses the special "it" variable' do
+ expect(parser.root).to parse('{{it}}')
+ end
+
+ it 'parses a text node' do
+ expect(parser.root).to parse('foo')
+ end
+
+ it 'parses an if expression' do
+ expect(parser.root).to parse('{% if foo %}bar{% end %}')
+ end
+
+ it 'parses an if-else expression' do
+ expect(parser.root).to parse('{% if foo %}bar{% else %}baz{% end %}')
+ end
+
+ it 'parses an each expression' do
+ expect(parser.root).to parse('{% each foo %}foo{% end %}')
+ end
+
+ it 'parses an escaped newline' do
+ expect(parser.root).to parse("foo\\\nbar")
+ end
+
+ it 'parses a regular newline' do
+ expect(parser.root).to parse("foo\nbar")
+ end
+
+ it 'parses the default changelog template' do
+ expect(parser.root).to parse(Gitlab::Changelog::Config::DEFAULT_TEMPLATE)
+ end
+
+ it 'raises an error when parsing an integer selector that is too large' do
+ expect(parser.root).not_to parse('{{100000000000}}')
+ end
+ end
+
+ describe '#parse_and_transform' do
+ it 'parses and transforms a template' do
+ node = parser.parse_and_transform('foo')
+
+ expect(node).to be_instance_of(Gitlab::Changelog::AST::Expressions)
+ end
+
+ it 'raises parsing errors using a custom error class' do
+ expect { parser.parse_and_transform('{% each') }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb
new file mode 100644
index 00000000000..f95244d6750
--- /dev/null
+++ b/spec/lib/gitlab/changelog/release_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Release do
+ describe '#to_markdown' do
+ let(:config) { Gitlab::Changelog::Config.new(build_stubbed(:project)) }
+ let(:commit) { build_stubbed(:commit) }
+ let(:author) { build_stubbed(:user) }
+ let(:mr) { build_stubbed(:merge_request) }
+ let(:release) do
+ described_class
+ .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
+ end
+
+ context 'when there are no entries' do
+ it 'includes a notice about the lack of entries' do
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ No changes.
+
+ OUT
+ end
+ end
+
+ context 'when all data is present' do
+ it 'includes all data' do
+ allow(config).to receive(:contributor?).with(author).and_return(true)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author,
+ merge_request: mr
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)}) \
+ by #{author.to_reference(full: true)} \
+ ([merge request](#{mr.to_reference(full: true)}))
+
+ OUT
+ end
+ end
+
+ context 'when no merge request is present' do
+ it "doesn't include a merge request link" do
+ allow(config).to receive(:contributor?).with(author).and_return(true)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)}) \
+ by #{author.to_reference(full: true)}
+
+ OUT
+ end
+ end
+
+ context 'when the author is not a contributor' do
+ it "doesn't include the author" do
+ allow(config).to receive(:contributor?).with(author).and_return(false)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)})
+
+ OUT
+ end
+ end
+
+ context 'when a category has no entries' do
+ it "isn't included in the output" do
+ config.categories['kittens'] = 'Kittens'
+ config.categories['fixed'] = 'Bug fixes'
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed'
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### Bug fixes (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)})
+
+ OUT
+ end
+ end
+ end
+
+ describe '#header_start_position' do
+ it 'returns a regular expression for finding the start of a release section' do
+ config = Gitlab::Changelog::Config.new(build_stubbed(:project))
+ release = described_class
+ .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
+
+ expect(release.header_start_pattern).to eq(/^##\s*1\.0\.0/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb
index 725ae03ad74..6d272f060ab 100644
--- a/spec/lib/gitlab/badge/coverage/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'lib/gitlab/badge/shared/metadata'
+require 'lib/gitlab/ci/badge/shared/metadata'
-RSpec.describe Gitlab::Badge::Coverage::Metadata do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Metadata do
let(:badge) do
double(project: create(:project), ref: 'feature', job: 'test')
end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb
index 3b5ea3291e4..13696d815aa 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Coverage::Report do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Report do
let_it_be(:project) { create(:project) }
let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:running_pipeline) { create(:ci_pipeline, :running, project: project) }
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
index ba5c1b2ce6e..f010d1bce50 100644
--- a/spec/lib/gitlab/badge/coverage/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Coverage::Template do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) }
let(:template) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb
index c8ed0c8ea29..2f677237fad 100644
--- a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'lib/gitlab/badge/shared/metadata'
+require 'lib/gitlab/ci/badge/shared/metadata'
-RSpec.describe Gitlab::Badge::Pipeline::Metadata do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Metadata do
let(:badge) { double(project: create(:project), ref: 'feature') }
let(:metadata) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
index b5dabca0477..45d0d781090 100644
--- a/spec/lib/gitlab/badge/pipeline/status_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Pipeline::Status do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do
let(:project) { create(:project, :repository) }
let(:sha) { project.commit.sha }
let(:branch) { 'master' }
diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
index c78e95852f3..696bb62b4d6 100644
--- a/spec/lib/gitlab/badge/pipeline/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Pipeline::Template do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) }
let(:template) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/ci/badge/shared/metadata.rb
index c99a65bb2f4..c99a65bb2f4 100644
--- a/spec/lib/gitlab/badge/shared/metadata.rb
+++ b/spec/lib/gitlab/ci/badge/shared/metadata.rb
diff --git a/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..f50c6e99e99
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Build::Credentials::Registry::DependencyProxy do
+ let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let(:gitlab_url) { 'gitlab.example.com:443' }
+
+ subject { described_class.new(build) }
+
+ before do
+ stub_config_setting(host: 'gitlab.example.com', port: 443)
+ end
+
+ it 'contains valid dependency proxy credentials' do
+ expect(subject).to be_kind_of(described_class)
+
+ expect(subject.username).to eq 'gitlab-ci-token'
+ expect(subject.password).to eq build.token
+ expect(subject.url).to eq gitlab_url
+ expect(subject.type).to eq 'registry'
+ end
+
+ describe '.valid?' do
+ subject { described_class.new(build).valid? }
+
+ context 'when dependency proxy is enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when dependency proxy is disabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: false })
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb
index c0a76973f60..43913e91085 100644
--- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
+++ b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Credentials::Registry do
+RSpec.describe Gitlab::Ci::Build::Credentials::Registry::GitlabRegistry do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:registry_url) { 'registry.example.com:5005' }
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index a1af5b75f87..0b50def05d4 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -201,40 +201,13 @@ RSpec.describe Gitlab::Ci::Build::Rules do
end
describe '#build_attributes' do
- let(:seed_attributes) { {} }
-
subject(:build_attributes) do
- result.build_attributes(seed_attributes)
+ result.build_attributes
end
it 'compacts nil values' do
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
describe '#pass?' do
diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb
index cfc2019a89b..46d7d4a58f0 100644
--- a/spec/lib/gitlab/ci/charts_spec.rb
+++ b/spec/lib/gitlab/ci/charts_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'goes until the end of the current month (including the whole last day of the month)' do
is_expected.to eq(Date.today.end_of_month.end_of_day)
end
@@ -20,6 +24,10 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %B %Y as labels format' do
expect(chart.labels).to include(chart.from.strftime('%B %Y'))
end
+
+ it 'returns count of pipelines run each day in the current year' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'monthchart' do
@@ -28,6 +36,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
@@ -39,6 +51,10 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %d %B as labels format' do
expect(chart.labels).to include(chart.from.strftime('%d %B'))
end
+
+ it 'returns count of pipelines run each day in the current month' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'weekchart' do
@@ -47,6 +63,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
@@ -58,6 +78,68 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %d %B as labels format' do
expect(chart.labels).to include(chart.from.strftime('%d %B'))
end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
+ end
+
+ context 'weekchart_utc' do
+ today = Date.today
+ end_of_today = Time.use_zone(Time.find_zone('UTC')) { today.end_of_day }
+
+ let(:project) { create(:project) }
+ let(:chart) do
+ allow(Date).to receive(:today).and_return(today)
+ allow(today).to receive(:end_of_day).and_return(end_of_today)
+ Gitlab::Ci::Charts::WeekChart.new(project)
+ end
+
+ subject { chart.total }
+
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
+ it 'uses a utc time zone for range times' do
+ expect(chart.to.zone).to eq(end_of_today.zone)
+ expect(chart.from.zone).to eq(end_of_today.zone)
+ end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
+ end
+
+ context 'weekchart_non_utc' do
+ today = Date.today
+ end_of_today = Time.use_zone(Time.find_zone('Asia/Dubai')) { today.end_of_day }
+
+ let(:project) { create(:project) }
+ let(:chart) do
+ allow(Date).to receive(:today).and_return(today)
+ allow(today).to receive(:end_of_day).and_return(end_of_today)
+ Gitlab::Ci::Charts::WeekChart.new(project)
+ end
+
+ subject { chart.total }
+
+ before do
+ # The DB uses UTC always, so our use of a Time Zone in the application
+ # can cause the creation date of the pipeline to go unmatched depending
+ # on the offset. We can work around this by requesting the pipeline be
+ # created a with the `created_at` field set to a day ago in the same week.
+ create(:ci_empty_pipeline, project: project, duration: 120, created_at: today - 1.day)
+ end
+
+ it 'uses a non-utc time zone for range times' do
+ expect(chart.to.zone).to eq(end_of_today.zone)
+ expect(chart.from.zone).to eq(end_of_today.zone)
+ end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'pipeline_times' do
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 80427eaa6ee..247f4b63910 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Cache do
+ using RSpec::Parameterized::TableSyntax
+
subject(:entry) { described_class.new(config) }
describe 'validations' do
@@ -56,8 +58,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `policy`' do
- using RSpec::Parameterized::TableSyntax
-
where(:policy, :result) do
'pull-push' | 'pull-push'
'push' | 'push'
@@ -77,8 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `when`' do
- using RSpec::Parameterized::TableSyntax
-
where(:when_config, :result) do
'on_success' | 'on_success'
'on_failure' | 'on_failure'
@@ -109,8 +107,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `policy`' do
- using RSpec::Parameterized::TableSyntax
-
where(:policy, :valid) do
'pull-push' | true
'push' | true
@@ -126,8 +122,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `when`' do
- using RSpec::Parameterized::TableSyntax
-
where(:when_config, :valid) do
'on_success' | true
'on_failure' | true
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 439799fe973..1b8dfae692a 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -87,18 +87,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
end
end
end
context 'when entry value is multi-level nested array' do
- let(:config) { [['ls', ['echo 1']], 'pwd'] }
+ let(:config) do
+ ['ls 0', ['ls 1', ['ls 2', ['ls 3', ['ls 4', ['ls 5', ['ls 6', ['ls 7', ['ls 8', ['ls 9', ['ls 10']]]]]]]]]]]
+ end
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 7834a1a94f2..a3b5f32b9f9 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -763,16 +763,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
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/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index aadf94365c6..04e80450263 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -73,6 +73,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
+ context 'when resource_group key is not a string' do
+ let(:config) { { resource_group: 123 } }
+
+ it 'returns error about wrong value type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include "job resource group should be a string"
+ end
+ end
+
context 'when it uses both "when:" and "rules:"' do
let(:config) do
{
@@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
+ context 'with resource group' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:resource_group, :result) do
+ 'iOS' | 'iOS'
+ 'review/$CI_COMMIT_REF_NAME' | 'review/$CI_COMMIT_REF_NAME'
+ nil | nil
+ end
+
+ with_them do
+ let(:config) { { script: 'ls', resource_group: resource_group }.compact }
+
+ it do
+ entry.compose!(deps)
+
+ expect(entry.resource_group).to eq(result)
+ end
+ end
+ end
+
context 'with inheritance' do
context 'of variables' do
let(:config) do
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 4fdaaca8316..99f546ceb37 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -323,20 +323,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
expect { subject }.to raise_error(described_class::AmbigiousSpecificationError)
end
end
-
- context 'when feature flag is turned off' do
- let(:values) do
- { include: full_local_file_path }
- end
-
- before do
- stub_feature_flags(variables_in_include_section_ci: false)
- end
-
- it 'does not expand the variables' do
- expect(subject[0].location).to eq('$CI_PROJECT_PATH' + local_file)
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb
new file mode 100644
index 00000000000..c68dccd3455
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference do
+ let(:config) do
+ Gitlab::Ci::Config::Yaml.load!(yaml)
+ end
+
+ describe '.tag' do
+ it 'implements the tag method' do
+ expect(described_class.tag).to eq('!reference')
+ end
+ end
+
+ describe '#resolve' do
+ subject { Gitlab::Ci::Config::Yaml::Tags::Resolver.new(config).to_hash }
+
+ context 'with circular references' do
+ let(:yaml) do
+ <<~YML
+ a: !reference [b]
+ b: !reference [a]
+ YML
+ end
+
+ it 'raises CircularReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] is part of a circular chain'
+ end
+ end
+
+ context 'with nested circular references' do
+ let(:yaml) do
+ <<~YML
+ a: !reference [b, c]
+ b: { c: !reference [d, e, f] }
+ d: { e: { f: !reference [a] } }
+ YML
+ end
+
+ it 'raises CircularReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b", "c"] is part of a circular chain'
+ end
+ end
+
+ context 'with missing references' do
+ let(:yaml) { 'a: !reference [b]' }
+
+ it 'raises MissingReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] could not be found'
+ end
+ end
+
+ context 'with invalid references' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:yaml, :error_message) do
+ 'a: !reference' | '!reference [] is not valid'
+ 'a: !reference str' | '!reference "str" is not valid'
+ 'a: !reference 1' | '!reference "1" is not valid'
+ 'a: !reference [1]' | '!reference [1] is not valid'
+ 'a: !reference { b: c }' | '!reference {"b"=>"c"} is not valid'
+ end
+
+ with_them do
+ it 'raises an error' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, error_message
+ end
+ end
+ end
+
+ context 'with arrays' do
+ let(:yaml) do
+ <<~YML
+ a: { b: [1, 2] }
+ c: { d: { e: [3, 4] } }
+ f: { g: [ !reference [a, b], 5, !reference [c, d, e]] }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ f: { g: [[1, 2], 5, [3, 4]] } })) }
+ end
+
+ context 'with hashes' do
+ context 'when referencing an entire hash' do
+ let(:yaml) do
+ <<~YML
+ a: { b: { c: 'c', d: 'd' } }
+ e: { f: !reference [a, b] }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ e: { f: { c: 'c', d: 'd' } } })) }
+ end
+
+ context 'when referencing only a hash value' do
+ let(:yaml) do
+ <<~YML
+ a: { b: { c: 'c', d: 'd' } }
+ e: { f: { g: !reference [a, b, c], h: 'h' } }
+ i: !reference [e, f]
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ i: { g: 'c', h: 'h' } })) }
+ end
+
+ context 'when referencing a value before its definition' do
+ let(:yaml) do
+ <<~YML
+ a: { b: !reference [c, d] }
+ g: { h: { i: 'i', j: 1 } }
+ c: { d: { e: !reference [g, h, j], f: 'f' } }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ a: { b: { e: 1, f: 'f' } } })) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb
new file mode 100644
index 00000000000..594242c33cc
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver do
+ let(:config) do
+ Gitlab::Ci::Config::Yaml.load!(yaml)
+ end
+
+ describe '#to_hash' do
+ subject { described_class.new(config).to_hash }
+
+ context 'when referencing deeply nested arrays' do
+ let(:yaml_templates) do
+ <<~YML
+ .job-1:
+ script:
+ - echo doing step 1 of job 1
+ - echo doing step 2 of job 1
+
+ .job-2:
+ script:
+ - echo doing step 1 of job 2
+ - !reference [.job-1, script]
+ - echo doing step 2 of job 2
+
+ .job-3:
+ script:
+ - echo doing step 1 of job 3
+ - !reference [.job-2, script]
+ - echo doing step 2 of job 3
+ YML
+ end
+
+ let(:job_yaml) do
+ <<~YML
+ test:
+ script:
+ - echo preparing to test
+ - !reference [.job-3, script]
+ - echo test finished
+ YML
+ end
+
+ shared_examples 'expands references' do
+ it 'expands the references' do
+ is_expected.to match({
+ '.job-1': {
+ script: [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ]
+ },
+ '.job-2': {
+ script: [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ]
+ },
+ '.job-3': {
+ script: [
+ 'echo doing step 1 of job 3',
+ [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ],
+ 'echo doing step 2 of job 3'
+ ]
+ },
+ test: {
+ script: [
+ 'echo preparing to test',
+ [
+ 'echo doing step 1 of job 3',
+ [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ],
+ 'echo doing step 2 of job 3'
+ ],
+ 'echo test finished'
+ ]
+ }
+ })
+ end
+ end
+
+ context 'when templates are defined before the job' do
+ let(:yaml) do
+ <<~YML
+ #{yaml_templates}
+ #{job_yaml}
+ YML
+ end
+
+ it_behaves_like 'expands references'
+ end
+
+ context 'when templates are defined after the job' do
+ let(:yaml) do
+ <<~YML
+ #{job_yaml}
+ #{yaml_templates}
+ YML
+ end
+
+ it_behaves_like 'expands references'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index dc03d2f80fe..45ce4cac6c4 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -263,6 +263,26 @@ RSpec.describe Gitlab::Ci::Config do
end
end
end
+
+ context 'when yaml uses circular !reference' do
+ let(:yml) do
+ <<~YAML
+ job-1:
+ script:
+ - !reference [job-2, before_script]
+
+ job-2:
+ before_script: !reference [job-1, script]
+ YAML
+ end
+
+ it 'raises error' do
+ expect { config }.to raise_error(
+ described_class::ConfigError,
+ /\!reference \["job-2", "before_script"\] is part of a circular chain/
+ )
+ end
+ end
end
context "when using 'include' directive" do
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index dd27b4045c9..15293429354 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -63,6 +63,17 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
+ context 'when range and slash used' do
+ let(:cron) { '3-59/10 * * * *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it_behaves_like returns_time_for_epoch
+
+ it 'returns specific time' do
+ expect(subject.min).to be_in([3, 13, 23, 33, 43, 53])
+ end
+ end
+
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb
index 3130c0c0c41..342ca6b8b75 100644
--- a/spec/lib/gitlab/ci/jwt_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_spec.rb
@@ -44,6 +44,9 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(payload[:pipeline_id]).to eq(pipeline.id.to_s)
expect(payload[:job_id]).to eq(build.id.to_s)
expect(payload[:ref]).to eq(pipeline.source_ref)
+ expect(payload[:ref_protected]).to eq(build.protected.to_s)
+ expect(payload[:environment]).to be_nil
+ expect(payload[:environment_protected]).to be_nil
end
end
@@ -90,6 +93,39 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(payload[:ref_protected]).to eq('true')
end
end
+
+ describe 'environment' do
+ let(:environment) { build_stubbed(:environment, project: project, name: 'production') }
+ let(:build) do
+ build_stubbed(
+ :ci_build,
+ project: project,
+ user: user,
+ pipeline: pipeline,
+ environment: environment.name
+ )
+ end
+
+ before do
+ allow(build).to receive(:persisted_environment).and_return(environment)
+ end
+
+ it 'has correct values for environment attributes' do
+ expect(payload[:environment]).to eq('production')
+ expect(payload[:environment_protected]).to eq('false')
+ end
+
+ context ':ci_jwt_include_environment feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_jwt_include_environment: false)
+ end
+
+ it 'does not include environment attributes' do
+ expect(payload).not_to have_key(:environment)
+ expect(payload).not_to have_key(:environment_protected)
+ end
+ end
+ end
end
describe '.for_build' do
diff --git a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
new file mode 100644
index 00000000000..30bcce21be2
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Instrumentation do
+ describe '#parse!' do
+ let(:parser_class) do
+ Class.new do
+ prepend Gitlab::Ci::Parsers::Instrumentation
+
+ def parse!(arg1, arg2)
+ "parse #{arg1} #{arg2}"
+ end
+ end
+ end
+
+ it 'sets metrics for duration of parsing' do
+ result = parser_class.new.parse!('hello', 'world')
+
+ expect(result).to eq('parse hello world')
+
+ metrics = Gitlab::Metrics.registry.get(:ci_report_parser_duration_seconds).get({ parser: parser_class.name })
+
+ expect(metrics.keys).to match_array(described_class::BUCKETS)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index b932cd81272..c9891c06507 100644
--- a/spec/lib/gitlab/ci/parsers_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -54,4 +54,12 @@ RSpec.describe Gitlab::Ci::Parsers do
end
end
end
+
+ describe '.instrument!' do
+ it 'prepends the Instrumentation module into each parser' do
+ expect(described_class.parsers.values).to all( receive(:prepend).with(Gitlab::Ci::Parsers::Instrumentation) )
+
+ described_class.instrument!
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 20406acb658..53dea1d0d19 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -235,7 +235,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
with_them do
before do
- project.update!(ci_keep_latest_artifact: keep_latest_artifact)
+ project.update!(keep_latest_artifact: keep_latest_artifact)
end
it 'builds a pipeline with appropriate locked value' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
index 3eaecb11ae0..1d17244e519 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
@@ -58,20 +58,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do
expect(build_statuses(child_pipeline)).to contain_exactly('canceled')
end
-
- context 'when FF ci_auto_cancel_all_pipelines is disabled' do
- before do
- stub_feature_flags(ci_auto_cancel_all_pipelines: false)
- end
-
- it 'does not cancel interruptible builds of child pipeline' do
- expect(build_statuses(child_pipeline)).to contain_exactly('running')
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('running')
- end
- end
end
context 'when the child pipeline has not an interruptible job' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
index 3616461d94f..cd868a57bbc 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do
%w(Template-1 Template-2).each do |expected_template|
expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to(
receive(:track_unique_project_event)
- .with(project_id: project.id, template: expected_template)
+ .with(project_id: project.id, template: expected_template, config_source: pipeline.config_source)
)
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index cf020fc343c..0efc7484699 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -383,14 +383,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
context 'when job is a bridge' do
- let(:attributes) do
+ let(:base_attributes) do
{
name: 'rspec', ref: 'master', options: { trigger: 'my/project' }, scheduling_type: :stage
}
end
+ let(:attributes) { base_attributes }
+
it { is_expected.to be_a(::Ci::Bridge) }
it { is_expected.to be_valid }
+
+ context 'when job belongs to a resource group' do
+ let(:attributes) { base_attributes.merge(resource_group_key: 'iOS') }
+
+ it 'returns a job with resource group' do
+ expect(subject.resource_group).not_to be_nil
+ expect(subject.resource_group.key).to eq('iOS')
+ end
+ end
end
it 'memoizes a resource object' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb
index 8fcc242ba5f..b7260599de2 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::ResourceGroup do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Processable::ResourceGroup do
let_it_be(:project) { create(:project) }
let(:job) { build(:ci_build, project: project) }
let(:seed) { described_class.new(job, resource_group_key) }
diff --git a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
new file mode 100644
index 00000000000..8b177fa7fc1
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do
+ let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
+ let(:degradation_3) { build(:codequality_degradation_3) }
+
+ describe '#initialize!' do
+ subject(:report) { described_class.new(codequality_report) }
+
+ context 'when quality has degradations' do
+ context 'with several degradations on the same line' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'generates quality report for mr diff' do
+ expect(report.files).to match(
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" }
+ ]
+ )
+ end
+ end
+
+ context 'with several degradations on several files' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(degradation_3)
+ end
+
+ it 'returns quality report for mr diff' do
+ expect(report.files).to match(
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" }
+ ],
+ "file_b.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "minor" }
+ ]
+ )
+ end
+ end
+ end
+
+ context 'when quality has no degradation' do
+ it 'returns an empty hash' do
+ expect(report.files).to match({})
+ end
+ 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
index 7053d54381b..90188b56f5a 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
@@ -6,62 +6,8 @@ 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
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
describe '#status' do
subject(:report_status) { comparer.status }
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
index 44e67259369..ae9b2f2c62b 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
@@ -4,62 +4,8 @@ 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
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
it { expect(codequality_report.degradations).to eq({}) }
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index d27bb98ba9a..6081f104e42 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -117,14 +117,31 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
end
+ context 'when bridge is waiting for resource' do
+ let(:bridge) { create_bridge(:waiting_for_resource, :resource_group) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::WaitingForResource
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'waiting'
+ expect(status.group).to eq 'waiting-for-resource'
+ expect(status.icon).to eq 'status_pending'
+ expect(status.favicon).to eq 'favicon_pending'
+ expect(status.illustration).to include(:image, :size, :title)
+ expect(status).not_to have_details
+ end
+ end
+
private
- def create_bridge(trait)
+ def create_bridge(*traits)
upstream_project = create(:project, :repository)
downstream_project = create(:project, :repository)
upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project)
trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } }
- create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline)
+ create(:ci_bridge, *traits, options: trigger, pipeline: upstream_pipeline)
end
end
diff --git a/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..3e19df28d83
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Bridge::WaitingForResource do
+ it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource }
+end
diff --git a/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..44bd5a8611a
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::WaitingForResource do
+ it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource }
+end
diff --git a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..91a9724d043
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource do
+ let(:user) { create(:user) }
+
+ subject do
+ processable = create(:ci_build, :waiting_for_resource, :resource_group)
+ described_class.new(Gitlab::Ci::Status::Core.new(processable, user))
+ end
+
+ describe '#illustration' do
+ it { expect(subject.illustration).to include(:image, :size, :title) }
+ end
+
+ describe '.matches?' do
+ subject {described_class.matches?(processable, user) }
+
+ context 'when processable is waiting for resource' do
+ let(:processable) { create(:ci_build, :waiting_for_resource) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when processable is not waiting for resource' do
+ let(:processable) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
index a2903391c6f..f09e03b4d55 100644
--- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
+++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do
let(:chunked_io) { described_class.new(build) }
before do
- stub_feature_flags(ci_enable_live_trace: true)
+ stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true)
end
describe "#initialize" do
diff --git a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
index d85bf29f77f..954273fd41e 100644
--- a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
@@ -5,8 +5,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
describe '#errors' do
context 'when FF :variable_inside_variable is disabled' do
+ let_it_be(:project_with_flag_disabled) { create(:project) }
+ let_it_be(:project_with_flag_enabled) { create(:project) }
+
before do
- stub_feature_flags(variable_inside_variable: false)
+ stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
@@ -53,7 +56,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_disabled) }
it 'does not report error' do
expect(subject.errors).to eq(nil)
@@ -67,8 +70,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
context 'when FF :variable_inside_variable is enabled' do
+ let_it_be(:project_with_flag_disabled) { create(:project) }
+ let_it_be(:project_with_flag_enabled) { create(:project) }
+
before do
- stub_feature_flags(variable_inside_variable: true)
+ stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
@@ -100,7 +106,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_enabled) }
it 'errors matches expected validation result' do
expect(subject.errors).to eq(validation_result)
@@ -164,7 +170,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ let_it_be(:project) { create(:project) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) }
it 'does not expand variables' do
expect(subject.sort).to eq(variables)
@@ -239,7 +246,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ let_it_be(:project) { create(:project) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) }
it 'sort returns correctly sorted variables' do
expect(subject.sort.map { |var| var[:key] }).to eq(result)
diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb
new file mode 100644
index 00000000000..b45abf8c0e1
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Helpers do
+ describe '.merge_variables' do
+ let(:current_variables) do
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' }]
+ end
+
+ let(:new_variables) do
+ [{ key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
+ end
+
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value22', public: true },
+ { key: 'key3', value: 'value3', public: true }]
+ end
+
+ subject { described_class.merge_variables(current_variables, new_variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when new variables is a hash' do
+ let(:new_variables) do
+ { 'key2' => 'value22', 'key3' => 'value3' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ context 'when new variables is a hash with symbol keys' do
+ let(:new_variables) do
+ { key2: 'value22', key3: 'value3' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ context 'when new variables is nil' do
+ let(:new_variables) {}
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ describe '.transform_to_yaml_variables' do
+ let(:variables) do
+ { 'key1' => 'value1', 'key2' => 'value2' }
+ end
+
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ subject { described_class.transform_to_yaml_variables(variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when variables is nil' do
+ let(:variables) {}
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ describe '.transform_from_yaml_variables' do
+ let(:variables) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ let(:result) do
+ { 'key1' => 'value1', 'key2' => 'value2' }
+ end
+
+ subject { described_class.transform_from_yaml_variables(variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when variables is nil' do
+ let(:variables) {}
+
+ it { is_expected.to eq({}) }
+ end
+
+ context 'when variables is a hash' do
+ let(:variables) do
+ { key1: 'value1', 'key2' => 'value2' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
index 8a7425a4156..b5adb603dab 100644
--- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
@@ -42,7 +42,8 @@ RSpec.describe Gitlab::Cleanup::OrphanJobArtifactFiles do
end
it 'stops when limit is reached' do
- cleanup = described_class.new(limit: 1)
+ stub_env('LIMIT', 1)
+ cleanup = described_class.new
mock_artifacts_found(cleanup, 'tmp/foo/bar/1', 'tmp/foo/bar/2')
diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
new file mode 100644
index 00000000000..4ed68d54680
--- /dev/null
+++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+RSpec.describe Gitlab::Cluster::LifecycleEvents do
+ # we create a new instance to ensure that we do not touch existing hooks
+ let(:replica) { Class.new(described_class) }
+
+ context 'hooks execution' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:method, :hook_names) do
+ :do_worker_start | %i[worker_start_hooks]
+ :do_before_fork | %i[before_fork_hooks]
+ :do_before_graceful_shutdown | %i[master_blackout_period master_graceful_shutdown]
+ :do_before_master_restart | %i[master_restart_hooks]
+ end
+
+ before do
+ # disable blackout period to speed-up tests
+ stub_config(shutdown: { blackout_seconds: 0 })
+ end
+
+ with_them do
+ subject { replica.public_send(method) }
+
+ it 'executes all hooks' do
+ hook_names.each do |hook_name|
+ hook = double
+ replica.instance_variable_set(:"@#{hook_name}", [hook])
+
+ # ensure that proper hooks are called
+ expect(hook).to receive(:call)
+ expect(replica).to receive(:call).with(hook_name, anything).and_call_original
+ end
+
+ subject
+ end
+ end
+ end
+
+ describe '#call' do
+ let(:name) { :my_hooks }
+
+ subject { replica.send(:call, name, hooks) }
+
+ context 'when many hooks raise exception' do
+ let(:hooks) do
+ [
+ -> { raise 'Exception A' },
+ -> { raise 'Exception B' }
+ ]
+ end
+
+ context 'USE_FATAL_LIFECYCLE_EVENTS is set to default' do
+ it 'only first hook is executed and is fatal' do
+ expect(hooks[0]).to receive(:call).and_call_original
+ expect(hooks[1]).not_to receive(:call)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
+ expect(replica).to receive(:warn).with('ERROR: The hook my_hooks failed with exception (RuntimeError) "Exception A".')
+
+ expect { subject }.to raise_error(described_class::FatalError, 'Exception A')
+ end
+ end
+
+ context 'when USE_FATAL_LIFECYCLE_EVENTS is disabled' do
+ before do
+ stub_const('Gitlab::Cluster::LifecycleEvents::USE_FATAL_LIFECYCLE_EVENTS', false)
+ end
+
+ it 'many hooks are executed and all exceptions are logged' do
+ expect(hooks[0]).to receive(:call).and_call_original
+ expect(hooks[1]).to receive(:call).and_call_original
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).twice.and_call_original
+ expect(replica).to receive(:warn).twice.and_call_original
+
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/composer/cache_spec.rb b/spec/lib/gitlab/composer/cache_spec.rb
new file mode 100644
index 00000000000..00318ac14f9
--- /dev/null
+++ b/spec/lib/gitlab/composer/cache_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Composer::Cache do
+ let_it_be(:package_name) { 'sample-project' }
+ let_it_be(:json) { { 'name' => package_name } }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) }
+ let(:branch) { project.repository.find_branch('master') }
+ let(:sha_regex) { /^[A-Fa-f0-9]{64}$/ }
+
+ shared_examples 'Composer create cache page' do
+ let(:expected_json) { ::Gitlab::Composer::VersionIndex.new(packages).to_json }
+
+ before do
+ stub_composer_cache_object_storage
+ end
+
+ it 'creates the cached page' do
+ expect { subject }.to change { Packages::Composer::CacheFile.count }.by(1)
+ cache_file = Packages::Composer::CacheFile.last
+ expect(cache_file.file_sha256).to eq package.reload.composer_metadatum.version_cache_sha
+ expect(cache_file.file.read).to eq expected_json
+ end
+ end
+
+ shared_examples 'Composer marks cache page for deletion' do
+ it 'marks the page for deletion' do
+ cache_file = Packages::Composer::CacheFile.last
+
+ freeze_time do
+ expect { subject }.to change { cache_file.reload.delete_at}.from(nil).to(1.day.from_now)
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project: project, name: package_name).execute }
+
+ context 'creating packages' do
+ context 'with a pre-existing package' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package, package2] }
+
+ before do
+ package
+ described_class.new(project: project, name: package_name).execute
+ package.reload
+ package2
+ end
+
+ it 'updates the sha and creates the cache page' do
+ expect { subject }.to change { package2.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex)
+ .and change { package.reload.composer_metadatum.version_cache_sha }.to(sha_regex)
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'first package' do
+ let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:packages) { [package] }
+
+ it 'updates the sha and creates the cache page' do
+ expect { subject }.to change { package.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex)
+ end
+
+ it_behaves_like 'Composer create cache page'
+ end
+ end
+
+ context 'updating packages' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package, package2] }
+
+ before do
+ packages
+
+ described_class.new(project: project, name: package_name).execute
+
+ package.update!(version: '1.2.0')
+ package.reload
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'deleting packages' do
+ context 'when it is not the last package' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package] }
+
+ before do
+ package
+ package2
+
+ described_class.new(project: project, name: package_name).execute
+
+ package2.destroy!
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'when it is the last package' do
+ let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let!(:last_sha) do
+ described_class.new(project: project, name: package_name).execute
+ package.reload.composer_metadatum.version_cache_sha
+ end
+
+ before do
+ package.destroy!
+ end
+
+ subject { described_class.new(project: project, name: package_name, last_page_sha: last_sha).execute }
+
+ it_behaves_like 'Composer marks cache page for deletion'
+
+ it 'does not create a new page' do
+ expect { subject }.not_to change { Packages::Composer::CacheFile.count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb
index 4c4742d9f59..7b0ed703f42 100644
--- a/spec/lib/gitlab/composer/version_index_spec.rb
+++ b/spec/lib/gitlab/composer/version_index_spec.rb
@@ -15,7 +15,9 @@ RSpec.describe Gitlab::Composer::VersionIndex do
let(:packages) { [package1, package2] }
describe '#as_json' do
- subject(:index) { described_class.new(packages).as_json }
+ subject(:package_index) { index['packages'][package_name] }
+
+ let(:index) { described_class.new(packages).as_json }
def expected_json(package)
{
@@ -32,10 +34,16 @@ RSpec.describe Gitlab::Composer::VersionIndex do
end
it 'returns the packages json' do
- packages = index['packages'][package_name]
+ expect(package_index['1.0.0']).to eq(expected_json(package1))
+ expect(package_index['2.0.0']).to eq(expected_json(package2))
+ end
+
+ context 'with an unordered list of packages' do
+ let(:packages) { [package2, package1] }
- expect(packages['1.0.0']).to eq(expected_json(package1))
- expect(packages['2.0.0']).to eq(expected_json(package2))
+ it 'returns the packages sorted by version' do
+ expect(package_index.keys).to eq ['1.0.0', '2.0.0']
+ end
end
end
diff --git a/spec/lib/gitlab/conan_token_spec.rb b/spec/lib/gitlab/conan_token_spec.rb
index be1d3e757f5..00683cf6e47 100644
--- a/spec/lib/gitlab/conan_token_spec.rb
+++ b/spec/lib/gitlab/conan_token_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::ConanToken do
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
- OpenSSL::Digest::SHA256.new,
+ OpenSSL::Digest.new('SHA256'),
base_secret,
described_class::HMAC_KEY
)
diff --git a/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
new file mode 100644
index 00000000000..cd68307e71f
--- /dev/null
+++ b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Entry::Validators::NestedArrayHelpers do
+ let(:config_struct) do
+ Struct.new(:value, keyword_init: true) do
+ include ActiveModel::Validations
+ extend Gitlab::Config::Entry::Validators::NestedArrayHelpers
+
+ validates_each :value do |record, attr, value|
+ unless validate_nested_array(value, 2) { |v| v.is_a?(Integer) }
+ record.errors.add(attr, "is invalid")
+ end
+ end
+ end
+ end
+
+ describe '#validate_nested_array' do
+ let(:config) { config_struct.new(value: value) }
+
+ subject(:errors) { config.errors }
+
+ before do
+ config.valid?
+ end
+
+ context 'with valid values' do
+ context 'with arrays of integers' do
+ let(:value) { [10, 11] }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with nested arrays of integers' do
+ let(:value) { [10, [11, 12]] }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with invalid values' do
+ subject(:error_messages) { errors.messages }
+
+ context 'with single integers' do
+ let(:value) { 10 }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when it is nested over the limit' do
+ let(:value) { [10, [11, [12]]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the array is not valid' do
+ let(:value) { [10, 11.5] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the nested array is not valid' do
+ let(:value) { [10, [11, 12.5]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb
index c07089d8ef0..024564ea213 100644
--- a/spec/lib/gitlab/crypto_helper_spec.rb
+++ b/spec/lib/gitlab/crypto_helper_spec.rb
@@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do
expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z}
expect(encrypted).not_to include "\n"
end
+
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count }
+ end
+
+ it 'encrypts using static iv' do
+ expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value')
+
+ described_class.aes256_gcm_encrypt('some-value')
+ end
end
describe '.aes256_gcm_decrypt' do
- let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') }
+ before do
+ stub_feature_flags(dynamic_nonce_creation: false)
+ end
+
+ context 'when token was encrypted using static nonce' do
+ let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) }
+
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq 'some-value'
+ end
+
+ it 'decrypts a value when it ends with a new line character' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
- it 'correctly decrypts encrypted string' do
- decrypted = described_class.aes256_gcm_decrypt(encrypted)
+ expect(decrypted).to eq 'some-value'
+ end
- expect(decrypted).to eq 'some-value'
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
+ end
+
+ context 'with feature flag switched on' do
+ before do
+ stub_feature_flags(dynamic_nonce_creation: true)
+ end
+
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq 'some-value'
+ end
+ end
end
- it 'decrypts a value when it ends with a new line character' do
- decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
+ context 'when token was encrypted using random nonce' do
+ let(:value) { 'random-value' }
+
+ # for compatibility with tokens encrypted using dynamic nonce
+ let!(:encrypted) do
+ iv = create_nonce
+ encrypted_token = described_class.create_encrypted_token(value, iv)
+ TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv)
+ encrypted_token
+ end
+
+ before do
+ stub_feature_flags(dynamic_nonce_creation: true)
+ end
- expect(decrypted).to eq 'some-value'
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq value
+ end
+
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
+ end
end
end
+
+ def create_nonce
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
+ cipher.encrypt # Required before '#random_iv' can be called
+ cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used.
+ end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 786db23ffc4..01aceec12c5 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do
end
end
end
+
+ describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original
+ end
+
+ it 'returns true when settings exist' do
+ create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+
+ expect(described_class.current_application_settings?).to eq(true)
+ end
+
+ it 'returns false when settings do not exist' do
+ expect(described_class.current_application_settings?).to eq(false)
+ end
+
+ context 'with cache', :request_store do
+ include_context 'with settings in cache'
+
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+
+ expect(described_class.current_application_settings?).to eq(true)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 21503dc1501..76578340f7b 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -218,7 +218,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
context 'when `to` is given' do
before do
- Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project, finished_at: Time.zone.now) }
end
it 'finds records created between `from` and `to` range' do
@@ -230,12 +230,34 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
context 'when `from` and `to` are within a day' do
- it 'returns the number of deployments made on that day' do
- freeze_time do
- create(:deployment, :success, project: project)
- options[:from] = options[:to] = Time.now
+ context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do
+ before do
+ stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: false)
+ end
+
+ it 'returns the number of deployments made on that day' do
+ freeze_time do
+ create(:deployment, :success, project: project)
+ options[:from] = options[:to] = Time.zone.now
+
+ expect(subject).to eq('1')
+ end
+ end
+ end
+
+ context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do
+ before do
+ stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: true)
+ end
+
+ it 'returns the number of deployments made on that day' do
+ freeze_time do
+ create(:deployment, :success, project: project, finished_at: Time.zone.now)
+ options[:from] = Time.zone.now.at_beginning_of_day
+ options[:to] = Time.zone.now.at_end_of_day
- expect(subject).to eq('1')
+ expect(subject).to eq('1')
+ end
end
end
end
diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/lib/gitlab/danger/base_linter_spec.rb
deleted file mode 100644
index 0136a0278ae..00000000000
--- a/spec/lib/gitlab/danger/base_linter_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-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 ignoring length issues for subject having not-ready wording' do
- using RSpec::Parameterized::TableSyntax
-
- let(:final_message) { 'A B C' }
-
- context 'when used as prefix' do
- where(prefix: [
- 'WIP: ',
- 'WIP:',
- 'wIp:',
- '[WIP] ',
- '[WIP]',
- '[draft]',
- '[draft] ',
- '(draft)',
- '(draft) ',
- 'draft - ',
- 'draft: ',
- 'draft:',
- 'DRAFT:'
- ])
-
- with_them do
- it 'does not have any problems' do
- commit_message = prefix + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size)
- commit = commit_class.new(commit_message, anything, anything)
-
- linter = described_class.new(commit).lint_subject
-
- expect(linter.problems).to be_empty
- end
- end
- end
-
- context 'when used as suffix' do
- where(suffix: %w[WIP draft])
-
- with_them do
- it 'does not have any problems' do
- commit_message = final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) + suffix
- commit = commit_class.new(commit_message, anything, anything)
-
- linter = described_class.new(commit).lint_subject
-
- expect(linter.problems).to be_empty
- end
- end
- end
- end
-
- context 'when subject does not have enough words and is 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/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb
deleted file mode 100644
index 04c515f1205..00000000000
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ /dev/null
@@ -1,229 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require_relative 'danger_spec_helper'
-
-require 'gitlab/danger/changelog'
-
-RSpec.describe Gitlab::Danger::Changelog do
- include DangerSpecHelper
-
- let(:added_files) { nil }
- let(:fake_git) { double('fake-git', added_files: added_files) }
-
- let(:mr_labels) { nil }
- let(:mr_json) { nil }
- let(:fake_gitlab) { double('fake-gitlab', mr_labels: mr_labels, mr_json: mr_json) }
-
- let(:changes_by_category) { nil }
- let(:sanitize_mr_title) { nil }
- let(:ee?) { false }
- let(:fake_helper) { double('fake-helper', changes_by_category: changes_by_category, sanitize_mr_title: sanitize_mr_title, ee?: ee?) }
-
- let(:fake_danger) { new_fake_danger.include(described_class) }
-
- subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) }
-
- describe '#required?' do
- subject { changelog.required? }
-
- context 'added files contain a migration' do
- [
- 'db/migrate/20200000000000_new_migration.rb',
- 'db/post_migrate/20200000000000_new_migration.rb'
- ].each do |file_path|
- let(:added_files) { [file_path] }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'added files do not contain a migration' do
- [
- 'app/models/model.rb',
- 'app/assets/javascripts/file.js'
- ].each do |file_path|
- let(:added_files) { [file_path] }
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#optional?' do
- let(:category_with_changelog) { :backend }
- let(:label_with_changelog) { 'frontend' }
- let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first }
- let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first }
-
- subject { changelog.optional? }
-
- context 'when MR contains only categories requiring no changelog' do
- let(:changes_by_category) { { category_without_changelog => nil } }
- let(:mr_labels) { [] }
-
- it 'is falsey' do
- is_expected.to be_falsy
- end
- end
-
- context 'when MR contains a label that require no changelog' do
- let(:changes_by_category) { { category_with_changelog => nil } }
- let(:mr_labels) { [label_with_changelog, label_without_changelog] }
-
- it 'is falsey' do
- is_expected.to be_falsy
- end
- end
-
- context 'when MR contains a category that require changelog and a category that require no changelog' do
- let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } }
- let(:mr_labels) { [] }
-
- it 'is truthy' do
- is_expected.to be_truthy
- end
- end
-
- context 'when MR contains a category that require changelog and a category that require no changelog with changelog label' do
- let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } }
- let(:mr_labels) { ['feature'] }
-
- it 'is truthy' do
- is_expected.to be_truthy
- end
- end
-
- context 'when MR contains a category that require changelog and a category that require no changelog with no changelog label' do
- let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } }
- let(:mr_labels) { ['tooling'] }
-
- it 'is truthy' do
- is_expected.to be_falsey
- end
- end
- end
-
- describe '#found' do
- subject { changelog.found }
-
- context 'added files contain a changelog' do
- [
- 'changelogs/unreleased/entry.yml',
- 'ee/changelogs/unreleased/entry.yml'
- ].each do |file_path|
- let(:added_files) { [file_path] }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'added files do not contain a changelog' do
- [
- 'app/models/model.rb',
- 'app/assets/javascripts/file.js'
- ].each do |file_path|
- let(:added_files) { [file_path] }
- it { is_expected.to eq(nil) }
- end
- end
- end
-
- describe '#ee_changelog?' do
- subject { changelog.ee_changelog? }
-
- before do
- allow(changelog).to receive(:found).and_return(file_path)
- end
-
- context 'is ee changelog' do
- let(:file_path) { 'ee/changelogs/unreleased/entry.yml' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'is not ee changelog' do
- let(:file_path) { 'changelogs/unreleased/entry.yml' }
-
- it { is_expected.to be_falsy }
- end
- end
-
- describe '#modified_text' do
- let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
-
- subject { changelog.modified_text }
-
- context "when title is not changed from sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG.md was edited')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
- end
- end
-
- context "when title needs sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG.md was edited')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
- end
- end
- end
-
- describe '#required_text' do
- let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
-
- subject { changelog.required_text }
-
- context "when title is not changed from sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG missing')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).not_to include('--ee')
- end
- end
-
- context "when title needs sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG missing')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).not_to include('--ee')
- end
- end
- end
-
- describe '#optional_text' do
- let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
-
- subject { changelog.optional_text }
-
- context "when title is not changed from sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG missing')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
- end
- end
-
- context "when title needs sanitization", :aggregate_failures do
- let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
-
- specify do
- expect(subject).to include('CHANGELOG missing')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb
deleted file mode 100644
index d3d86037a53..00000000000
--- a/spec/lib/gitlab/danger/commit_linter_spec.rb
+++ /dev/null
@@ -1,242 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-require_relative 'danger_spec_helper'
-
-require 'gitlab/danger/commit_linter'
-
-RSpec.describe Gitlab::Danger::CommitLinter do
- using RSpec::Parameterized::TableSyntax
-
- let(:total_files_changed) { 2 }
- let(:total_lines_changed) { 10 }
- let(:stats) { { total: { files: total_files_changed, lines: total_lines_changed } } }
- let(:diff_parent) { Struct.new(:stats).new(stats) }
- let(:commit_class) do
- Struct.new(:message, :sha, :diff_parent)
- end
-
- let(:commit_message) { 'A commit message' }
- let(:commit_sha) { 'abcd1234' }
- let(:commit) { commit_class.new(commit_message, commit_sha, diff_parent) }
-
- subject(:commit_linter) { described_class.new(commit) }
-
- describe '#fixup?' do
- where(:commit_message, :is_fixup) do
- 'A commit message' | false
- 'fixup!' | true
- 'fixup! A commit message' | true
- 'squash!' | true
- 'squash! A commit message' | true
- end
-
- with_them do
- it 'is true when commit message starts with "fixup!" or "squash!"' do
- expect(commit_linter.fixup?).to be(is_fixup)
- end
- end
- end
-
- describe '#suggestion?' do
- where(:commit_message, :is_suggestion) do
- 'A commit message' | false
- 'Apply suggestion to' | true
- 'Apply suggestion to "A commit message"' | true
- end
-
- with_them do
- it 'is true when commit message starts with "Apply suggestion to"' do
- expect(commit_linter.suggestion?).to be(is_suggestion)
- end
- end
- end
-
- describe '#merge?' do
- where(:commit_message, :is_merge) do
- 'A commit message' | false
- 'Merge branch' | true
- 'Merge branch "A commit message"' | true
- end
-
- with_them do
- it 'is true when commit message starts with "Merge branch"' do
- expect(commit_linter.merge?).to be(is_merge)
- end
- end
- end
-
- describe '#revert?' do
- where(:commit_message, :is_revert) do
- 'A commit message' | false
- 'Revert' | false
- 'Revert "' | true
- 'Revert "A commit message"' | true
- end
-
- with_them do
- it 'is true when commit message starts with "Revert \""' do
- expect(commit_linter.revert?).to be(is_revert)
- end
- end
- end
-
- describe '#multi_line?' do
- where(:commit_message, :is_multi_line) do
- "A commit message" | false
- "A commit message\n" | false
- "A commit message\n\n" | false
- "A commit message\n\nSigned-off-by: User Name <user@name.me>" | false
- "A commit message\n\nWith details" | true
- end
-
- with_them do
- it 'is true when commit message contains details' do
- expect(commit_linter.multi_line?).to be(is_multi_line)
- end
- end
- end
-
- shared_examples 'a valid commit' do
- it 'does not have any problem' do
- commit_linter.lint
-
- expect(commit_linter.problems).to be_empty
- end
- end
-
- describe '#lint' do
- describe 'separator' do
- context 'when separator is missing' do
- let(:commit_message) { "A B C\n" }
-
- it_behaves_like 'a valid commit'
- end
-
- context 'when separator is a blank line' do
- let(:commit_message) { "A B C\n\nMore details." }
-
- it_behaves_like 'a valid commit'
- end
-
- context 'when separator is missing' do
- let(:commit_message) { "A B C\nMore details." }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:separator_missing)
-
- commit_linter.lint
- end
- end
- end
-
- describe 'details' do
- context 'when details are valid' do
- let(:commit_message) { "A B C\n\nMore details." }
-
- it_behaves_like 'a valid commit'
- end
-
- context 'when no details are given and many files are changed' do
- let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 }
-
- it_behaves_like 'a valid commit'
- end
-
- context 'when no details are given and many lines are changed' do
- let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 }
-
- it_behaves_like 'a valid commit'
- end
-
- context 'when no details are given and many files and lines are changed' do
- let(:total_files_changed) { described_class::MAX_CHANGED_FILES_IN_COMMIT + 1 }
- let(:total_lines_changed) { described_class::MAX_CHANGED_LINES_IN_COMMIT + 1 }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:details_too_many_changes)
-
- commit_linter.lint
- end
- end
-
- context 'when details exceeds the max line length' do
- let(:commit_message) { "A B C\n\n" + 'D' * (described_class::MAX_LINE_LENGTH + 1) }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:details_line_too_long)
-
- commit_linter.lint
- end
- end
-
- 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
- end
-
- describe 'message' do
- context 'when message includes a text emoji' do
- let(:commit_message) { "A commit message :+1:" }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:message_contains_text_emoji)
-
- commit_linter.lint
- end
- end
-
- context 'when message includes a unicode emoji' do
- let(:commit_message) { "A commit message 🚀" }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:message_contains_unicode_emoji)
-
- commit_linter.lint
- end
- end
-
- context 'when message includes a value that is surrounded by backticks' do
- let(:commit_message) { "A commit message `%20`" }
-
- it 'does not add a problem' do
- expect(commit_linter).not_to receive(:add_problem)
-
- commit_linter.lint
- end
- end
-
- context 'when message includes a short reference' do
- [
- 'A commit message to fix #1234',
- 'A commit message to fix !1234',
- 'A commit message to fix &1234',
- 'A commit message to fix %1234',
- 'A commit message to fix gitlab#1234',
- 'A commit message to fix gitlab!1234',
- 'A commit message to fix gitlab&1234',
- 'A commit message to fix gitlab%1234',
- 'A commit message to fix gitlab-org/gitlab#1234',
- 'A commit message to fix gitlab-org/gitlab!1234',
- 'A commit message to fix gitlab-org/gitlab&1234',
- 'A commit message to fix gitlab-org/gitlab%1234',
- 'A commit message to fix "gitlab-org/gitlab%1234"',
- 'A commit message to fix `gitlab-org/gitlab%1234'
- ].each do |message|
- let(:commit_message) { message }
-
- it 'adds a problem' do
- expect(commit_linter).to receive(:add_problem).with(:message_contains_short_reference)
-
- commit_linter.lint
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/danger_spec_helper.rb b/spec/lib/gitlab/danger/danger_spec_helper.rb
deleted file mode 100644
index b1e84b3c13d..00000000000
--- a/spec/lib/gitlab/danger/danger_spec_helper.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module DangerSpecHelper
- def new_fake_danger
- Class.new do
- attr_reader :git, :gitlab, :helper
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def initialize(git: nil, gitlab: nil, helper: nil)
- @git = git
- @gitlab = gitlab
- @helper = helper
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/emoji_checker_spec.rb b/spec/lib/gitlab/danger/emoji_checker_spec.rb
deleted file mode 100644
index 6092c751e1c..00000000000
--- a/spec/lib/gitlab/danger/emoji_checker_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-require 'gitlab/danger/emoji_checker'
-
-RSpec.describe Gitlab::Danger::EmojiChecker do
- using RSpec::Parameterized::TableSyntax
-
- describe '#includes_text_emoji?' do
- where(:text, :includes_emoji) do
- 'Hello World!' | false
- ':+1:' | true
- 'Hello World! :+1:' | true
- end
-
- with_them do
- it 'is true when text includes a text emoji' do
- expect(subject.includes_text_emoji?(text)).to be(includes_emoji)
- end
- end
- end
-
- describe '#includes_unicode_emoji?' do
- where(:text, :includes_emoji) do
- 'Hello World!' | false
- '🚀' | true
- 'Hello World! 🚀' | true
- end
-
- with_them do
- it 'is true when text includes a text emoji' do
- expect(subject.includes_unicode_emoji?(text)).to be(includes_emoji)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
deleted file mode 100644
index bd5c746dd54..00000000000
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ /dev/null
@@ -1,602 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-require_relative 'danger_spec_helper'
-
-require 'gitlab/danger/helper'
-
-RSpec.describe Gitlab::Danger::Helper do
- using RSpec::Parameterized::TableSyntax
- include DangerSpecHelper
-
- let(:fake_git) { double('fake-git') }
-
- let(:mr_author) { nil }
- let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) }
-
- let(:fake_danger) { new_fake_danger.include(described_class) }
-
- subject(:helper) { fake_danger.new(git: fake_git, gitlab: fake_gitlab) }
-
- describe '#gitlab_helper' do
- context 'when gitlab helper is not available' do
- let(:fake_gitlab) { nil }
-
- it 'returns nil' do
- expect(helper.gitlab_helper).to be_nil
- end
- end
-
- context 'when gitlab helper is available' do
- it 'returns the gitlab 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
- context 'when gitlab helper is not available' do
- it 'returns false' do
- expect(helper.release_automation?).to be_falsey
- end
- end
-
- context 'when gitlab helper is available' do
- context "but the MR author isn't the RELEASE_TOOLS_BOT" do
- let(:mr_author) { 'johnmarston' }
-
- it 'returns false' do
- expect(helper.release_automation?).to be_falsey
- end
- end
-
- context 'and the MR author is the RELEASE_TOOLS_BOT' do
- let(:mr_author) { described_class::RELEASE_TOOLS_BOT }
-
- it 'returns true' do
- expect(helper.release_automation?).to be_truthy
- end
- end
- end
- end
-
- describe '#all_changed_files' do
- subject { helper.all_changed_files }
-
- it 'interprets a list of changes from the danger git plugin' do
- expect(fake_git).to receive(:added_files) { %w[a b c.old] }
- expect(fake_git).to receive(:modified_files) { %w[d e] }
- expect(fake_git)
- .to receive(:renamed_files)
- .at_least(:once)
- .and_return([{ before: 'c.old', after: 'c.new' }])
-
- is_expected.to contain_exactly('a', 'b', 'c.new', 'd', 'e')
- end
- end
-
- describe '#changed_lines' do
- subject { helper.changed_lines('changed_file.rb') }
-
- before do
- allow(fake_git).to receive(:diff_for_file).with('changed_file.rb').and_return(diff)
- end
-
- context 'when file has diff' do
- let(:diff) { double(:diff, patch: "+ # New change here\n+ # New change there") }
-
- it 'returns file changes' do
- is_expected.to eq(['+ # New change here', '+ # New change there'])
- end
- end
-
- context 'when file has no diff (renamed without changes)' do
- let(:diff) { nil }
-
- it 'returns a blank array' do
- is_expected.to eq([])
- end
- end
- end
-
- describe "changed_files" do
- it 'returns list of changed files matching given regex' do
- expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb usage_data.rb])
-
- expect(helper.changed_files(/usage_data/)).to contain_exactly('usage_data.rb')
- end
- end
-
- describe '#all_ee_changes' do
- subject { helper.all_ee_changes }
-
- it 'returns all changed files starting with ee/' do
- expect(helper).to receive(:all_changed_files).and_return(%w[fr/ee/beer.rb ee/wine.rb ee/lib/ido.rb ee.k])
-
- is_expected.to match_array(%w[ee/wine.rb ee/lib/ido.rb])
- end
- end
-
- describe '#ee?' do
- subject { helper.ee? }
-
- it 'returns true if CI_PROJECT_NAME if set to gitlab' do
- stub_env('CI_PROJECT_NAME', 'gitlab')
- expect(Dir).not_to receive(:exist?)
-
- is_expected.to be_truthy
- end
-
- it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do
- stub_env('CI_PROJECT_NAME', 'something else')
- expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
-
- is_expected.to be_truthy
- end
-
- it 'returns true if ee exists' do
- stub_env('CI_PROJECT_NAME', nil)
- expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
-
- is_expected.to be_truthy
- end
-
- it "returns false if ee doesn't exist" do
- stub_env('CI_PROJECT_NAME', nil)
- expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { false }
-
- is_expected.to be_falsy
- end
- end
-
- describe '#project_name' do
- subject { helper.project_name }
-
- it 'returns gitlab if ee? returns true' do
- expect(helper).to receive(:ee?) { true }
-
- is_expected.to eq('gitlab')
- end
-
- it 'returns gitlab-ce if ee? returns false' do
- expect(helper).to receive(:ee?) { false }
-
- is_expected.to eq('gitlab-foss')
- end
- end
-
- describe '#markdown_list' do
- it 'creates a markdown list of items' do
- items = %w[a b]
-
- expect(helper.markdown_list(items)).to eq("* `a`\n* `b`")
- end
-
- it 'wraps items in <details> when there are more than 10 items' do
- items = ('a'..'k').to_a
-
- expect(helper.markdown_list(items)).to match(%r{<details>[^<]+</details>})
- end
- end
-
- describe '#changes_by_category' do
- it 'categorizes changed files' do
- expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] }
- allow(fake_git).to receive(:modified_files) { [] }
- allow(fake_git).to receive(:renamed_files) { [] }
-
- expect(helper.changes_by_category).to eq(
- backend: %w[foo.rb],
- database: %w[db/migrate/foo lib/gitlab/database/foo.rb],
- frontend: %w[foo.js],
- none: %w[ee/changelogs/foo.yml foo.md],
- qa: %w[qa/foo],
- unknown: %w[foo]
- )
- end
- end
-
- describe '#categories_for_file' do
- before do
- allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ count(User.active)") }
- end
-
- where(:path, :expected_categories) do
- 'usage_data.rb' | [:database, :backend]
- 'doc/foo.md' | [:docs]
- 'CONTRIBUTING.md' | [:docs]
- 'LICENSE' | [:docs]
- 'MAINTENANCE.md' | [:docs]
- 'PHILOSOPHY.md' | [:docs]
- 'PROCESS.md' | [:docs]
- 'README.md' | [:docs]
-
- 'ee/doc/foo' | [:unknown]
- 'ee/README' | [:unknown]
-
- 'app/assets/foo' | [:frontend]
- 'app/views/foo' | [:frontend]
- 'public/foo' | [:frontend]
- 'scripts/frontend/foo' | [:frontend]
- 'spec/javascripts/foo' | [:frontend]
- 'spec/frontend/bar' | [:frontend]
- 'vendor/assets/foo' | [:frontend]
- 'babel.config.js' | [:frontend]
- 'jest.config.js' | [:frontend]
- 'package.json' | [:frontend]
- 'yarn.lock' | [:frontend]
- 'config/foo.js' | [:frontend]
- 'config/deep/foo.js' | [:frontend]
-
- 'ee/app/assets/foo' | [:frontend]
- 'ee/app/views/foo' | [:frontend]
- 'ee/spec/javascripts/foo' | [:frontend]
- 'ee/spec/frontend/bar' | [:frontend]
-
- '.gitlab/ci/frontend.gitlab-ci.yml' | %i[frontend engineering_productivity]
-
- 'app/models/foo' | [:backend]
- 'bin/foo' | [:backend]
- 'config/foo' | [:backend]
- 'lib/foo' | [:backend]
- 'rubocop/foo' | [:backend]
- '.rubocop.yml' | [:backend]
- '.rubocop_todo.yml' | [:backend]
- '.rubocop_manual_todo.yml' | [:backend]
- 'spec/foo' | [:backend]
- 'spec/foo/bar' | [:backend]
-
- 'ee/app/foo' | [:backend]
- 'ee/bin/foo' | [:backend]
- 'ee/spec/foo' | [:backend]
- 'ee/spec/foo/bar' | [:backend]
-
- 'spec/features/foo' | [:test]
- 'ee/spec/features/foo' | [:test]
- 'spec/support/shared_examples/features/foo' | [:test]
- 'ee/spec/support/shared_examples/features/foo' | [:test]
- 'spec/support/shared_contexts/features/foo' | [:test]
- 'ee/spec/support/shared_contexts/features/foo' | [:test]
- 'spec/support/helpers/features/foo' | [:test]
- 'ee/spec/support/helpers/features/foo' | [:test]
-
- 'generator_templates/foo' | [:backend]
- 'vendor/languages.yml' | [:backend]
- 'file_hooks/examples/' | [:backend]
-
- 'Gemfile' | [:backend]
- 'Gemfile.lock' | [:backend]
- 'Rakefile' | [:backend]
- 'FOO_VERSION' | [:backend]
-
- 'Dangerfile' | [:engineering_productivity]
- 'danger/commit_messages/Dangerfile' | [:engineering_productivity]
- 'ee/danger/commit_messages/Dangerfile' | [:engineering_productivity]
- 'danger/commit_messages/' | [:engineering_productivity]
- 'ee/danger/commit_messages/' | [:engineering_productivity]
- '.gitlab-ci.yml' | [:engineering_productivity]
- '.gitlab/ci/cng.gitlab-ci.yml' | [:engineering_productivity]
- '.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | [:engineering_productivity]
- 'scripts/foo' | [:engineering_productivity]
- 'lib/gitlab/danger/foo' | [:engineering_productivity]
- 'ee/lib/gitlab/danger/foo' | [:engineering_productivity]
- 'lefthook.yml' | [:engineering_productivity]
- '.editorconfig' | [:engineering_productivity]
- 'tooling/bin/find_foss_tests' | [:engineering_productivity]
- '.codeclimate.yml' | [:engineering_productivity]
- '.gitlab/CODEOWNERS' | [:engineering_productivity]
-
- 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template]
- 'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template]
-
- 'ee/FOO_VERSION' | [:unknown]
-
- 'db/schema.rb' | [:database]
- 'db/structure.sql' | [:database]
- 'db/migrate/foo' | [:database]
- 'db/post_migrate/foo' | [:database]
- 'ee/db/migrate/foo' | [:database]
- 'ee/db/post_migrate/foo' | [:database]
- 'ee/db/geo/migrate/foo' | [:database]
- 'ee/db/geo/post_migrate/foo' | [:database]
- 'app/models/project_authorization.rb' | [:database]
- 'app/services/users/refresh_authorized_projects_service.rb' | [:database]
- 'lib/gitlab/background_migration.rb' | [:database]
- 'lib/gitlab/background_migration/foo' | [:database]
- 'ee/lib/gitlab/background_migration/foo' | [:database]
- 'lib/gitlab/database.rb' | [:database]
- 'lib/gitlab/database/foo' | [:database]
- 'ee/lib/gitlab/database/foo' | [:database]
- 'lib/gitlab/github_import.rb' | [:database]
- 'lib/gitlab/github_import/foo' | [:database]
- 'lib/gitlab/sql/foo' | [:database]
- 'rubocop/cop/migration/foo' | [:database]
-
- 'db/fixtures/foo.rb' | [:backend]
- 'ee/db/fixtures/foo.rb' | [:backend]
- 'doc/api/graphql/reference/gitlab_schema.graphql' | [:backend]
- 'doc/api/graphql/reference/gitlab_schema.json' | [:backend]
-
- 'qa/foo' | [:qa]
- 'ee/qa/foo' | [:qa]
-
- 'changelogs/foo' | [:none]
- 'ee/changelogs/foo' | [:none]
- 'locale/gitlab.pot' | [:none]
-
- 'FOO' | [:unknown]
- 'foo' | [:unknown]
-
- 'foo/bar.rb' | [:backend]
- 'foo/bar.js' | [:frontend]
- 'foo/bar.txt' | [:none]
- 'foo/bar.md' | [:none]
- end
-
- with_them do
- subject { helper.categories_for_file(path) }
-
- it { is_expected.to eq(expected_categories) }
- end
-
- context 'having specific changes' do
- where(:expected_categories, :patch, :changed_files) do
- [:database, :backend] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb']
- [:database, :backend] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb']
- [:backend] | '+ alt_usage_data(User.active)' | ['usage_data.rb']
- [:backend] | '+ count(User.active)' | ['user.rb']
- [:backend] | '+ count(User.active)' | ['usage_data/topology.rb']
- [:backend] | '+ foo_count(User.active)' | ['usage_data.rb']
- end
-
- with_them do
- it 'has the correct categories' do
- changed_files.each do |file|
- allow(fake_git).to receive(:diff_for_file).with(file) { double(:diff, patch: patch) }
-
- expect(helper.categories_for_file(file)).to eq(expected_categories)
- end
- end
- end
- end
- end
-
- describe '#label_for_category' do
- where(:category, :expected_label) do
- :backend | '~backend'
- :database | '~database'
- :docs | '~documentation'
- :foo | '~foo'
- :frontend | '~frontend'
- :none | ''
- :qa | '~QA'
- :engineering_productivity | '~"Engineering Productivity" for CI, Danger'
- :ci_template | '~"ci::templates"'
- end
-
- with_them do
- subject { helper.label_for_category(category) }
-
- it { is_expected.to eq(expected_label) }
- end
- end
-
- describe '#new_teammates' do
- it 'returns an array of Teammate' do
- usernames = %w[filipa iamphil]
-
- teammates = helper.new_teammates(usernames)
-
- expect(teammates.map(&:username)).to eq(usernames)
- end
- end
-
- describe '#security_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_security_mr
- end
-
- it 'returns false when on a normal merge request' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('web_url' => 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1')
-
- expect(helper).not_to be_security_mr
- end
-
- it 'returns true when on a security merge request' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('web_url' => 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1')
-
- expect(helper).to be_security_mr
- end
- end
-
- describe '#draft_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_draft_mr
- end
-
- it 'returns true for a draft MR' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('title' => 'Draft: My MR title')
-
- expect(helper).to be_draft_mr
- end
-
- it 'returns false for non draft MR' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('title' => 'My MR title')
-
- expect(helper).not_to be_draft_mr
- end
- end
-
- describe '#cherry_pick_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_cherry_pick_mr
- end
-
- context 'when MR title does not mention a cherry-pick' do
- it 'returns false' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('title' => 'Add feature xyz')
-
- expect(helper).not_to be_cherry_pick_mr
- end
- end
-
- context 'when MR title mentions a cherry-pick' do
- [
- 'Cherry Pick !1234',
- 'cherry-pick !1234',
- 'CherryPick !1234'
- ].each do |mr_title|
- it 'returns true' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('title' => mr_title)
-
- expect(helper).to be_cherry_pick_mr
- end
- end
- end
- end
-
- describe '#stable_branch?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_stable_branch
- end
-
- context 'when MR target branch is not a stable branch' do
- it 'returns false' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('target_branch' => 'my-feature-branch')
-
- expect(helper).not_to be_stable_branch
- end
- end
-
- context 'when MR target branch is a stable branch' do
- %w[
- 13-1-stable-ee
- 13-1-stable-ee-patch-1
- ].each do |target_branch|
- it 'returns true' do
- expect(fake_gitlab).to receive(:mr_json)
- .and_return('target_branch' => target_branch)
-
- expect(helper).to be_stable_branch
- end
- end
- end
- end
-
- describe '#mr_has_label?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper.mr_has_labels?('telemetry')).to be_falsey
- end
-
- context 'when mr has labels' do
- before do
- mr_labels = ['telemetry', 'telemetry::reviewed']
- expect(fake_gitlab).to receive(:mr_labels).and_return(mr_labels)
- end
-
- it 'returns true with a matched label' do
- expect(helper.mr_has_labels?('telemetry')).to be_truthy
- end
-
- it 'returns false with unmatched label' do
- expect(helper.mr_has_labels?('database')).to be_falsey
- end
-
- it 'returns true with an array of labels' do
- expect(helper.mr_has_labels?(['telemetry', 'telemetry::reviewed'])).to be_truthy
- end
-
- it 'returns true with multi arguments with matched labels' do
- expect(helper.mr_has_labels?('telemetry', 'telemetry::reviewed')).to be_truthy
- end
-
- it 'returns false with multi arguments with unmatched labels' do
- expect(helper.mr_has_labels?('telemetry', 'telemetry::non existing')).to be_falsey
- end
- end
- end
-
- describe '#labels_list' do
- let(:labels) { ['telemetry', 'telemetry::reviewed'] }
-
- it 'composes the labels string' do
- expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"')
- end
-
- context 'when passing a separator' do
- it 'composes the labels string with the given separator' do
- expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"')
- end
- end
-
- it 'returns empty string for empty array' do
- expect(helper.labels_list([])).to eq('')
- end
- end
-
- describe '#prepare_labels_for_mr' do
- it 'composes the labels string' do
- mr_labels = ['telemetry', 'telemetry::reviewed']
-
- expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"')
- end
-
- it 'returns empty string for empty array' 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
deleted file mode 100644
index 29facc9fdd6..00000000000
--- a/spec/lib/gitlab/danger/merge_request_linter_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# 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
deleted file mode 100644
index 59ac3b12b6b..00000000000
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ /dev/null
@@ -1,413 +0,0 @@
-# frozen_string_literal: true
-
-require 'webmock/rspec'
-require 'timecop'
-
-require 'gitlab/danger/roulette'
-require 'active_support/testing/time_helpers'
-
-RSpec.describe Gitlab::Danger::Roulette do
- include ActiveSupport::Testing::TimeHelpers
-
- around do |example|
- travel_to(Time.utc(2020, 06, 22, 10)) { example.run }
- end
-
- let(:backend_available) { true }
- let(:backend_tz_offset_hours) { 2.0 }
- let(:backend_maintainer) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'backend-maintainer',
- 'name' => 'Backend maintainer',
- 'role' => 'Backend engineer',
- 'projects' => { 'gitlab' => 'maintainer backend' },
- 'available' => backend_available,
- 'tz_offset_hours' => backend_tz_offset_hours
- )
- end
-
- let(:frontend_reviewer) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'frontend-reviewer',
- 'name' => 'Frontend reviewer',
- 'role' => 'Frontend engineer',
- 'projects' => { 'gitlab' => 'reviewer frontend' },
- 'available' => true,
- 'tz_offset_hours' => 2.0
- )
- end
-
- let(:frontend_maintainer) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'frontend-maintainer',
- 'name' => 'Frontend maintainer',
- 'role' => 'Frontend engineer',
- 'projects' => { 'gitlab' => "maintainer frontend" },
- 'available' => true,
- 'tz_offset_hours' => 2.0
- )
- end
-
- let(:software_engineer_in_test) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'software-engineer-in-test',
- 'name' => 'Software Engineer in Test',
- 'role' => 'Software Engineer in Test, Create:Source Code',
- 'projects' => { 'gitlab' => 'reviewer qa', 'gitlab-qa' => 'maintainer' },
- 'available' => true,
- 'tz_offset_hours' => 2.0
- )
- end
-
- let(:engineering_productivity_reviewer) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'eng-prod-reviewer',
- 'name' => 'EP engineer',
- 'role' => 'Engineering Productivity',
- 'projects' => { 'gitlab' => 'reviewer backend' },
- 'available' => true,
- 'tz_offset_hours' => 2.0
- )
- end
-
- let(:ci_template_reviewer) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'ci-template-maintainer',
- 'name' => 'CI Template engineer',
- 'role' => '~"ci::templates"',
- 'projects' => { 'gitlab' => 'reviewer ci_template' },
- 'available' => true,
- 'tz_offset_hours' => 2.0
- )
- end
-
- let(:teammates) do
- [
- backend_maintainer.to_h,
- frontend_maintainer.to_h,
- frontend_reviewer.to_h,
- software_engineer_in_test.to_h,
- engineering_productivity_reviewer.to_h,
- ci_template_reviewer.to_h
- ]
- end
-
- let(:teammate_json) do
- teammates.to_json
- end
-
- subject(:roulette) { Object.new.extend(described_class) }
-
- describe 'Spin#==' do
- it 'compares Spin attributes' do
- spin1 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
- spin2 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
- spin3 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, true)
- spin4 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, true, false)
- spin5 = described_class::Spin.new(:backend, frontend_reviewer, backend_maintainer, false, false)
- spin6 = described_class::Spin.new(:backend, backend_maintainer, frontend_maintainer, false, false)
- spin7 = described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)
-
- expect(spin1).to eq(spin2)
- expect(spin1).not_to eq(spin3)
- expect(spin1).not_to eq(spin4)
- expect(spin1).not_to eq(spin5)
- expect(spin1).not_to eq(spin6)
- expect(spin1).not_to eq(spin7)
- end
- end
-
- describe '#spin' do
- let!(:project) { 'gitlab' }
- let!(:mr_source_branch) { 'a-branch' }
- let!(:mr_labels) { ['backend', 'devops::create'] }
- let!(:author) { Gitlab::Danger::Teammate.new('username' => 'johndoe') }
- let(:timezone_experiment) { false }
- let(:spins) do
- # Stub the request at the latest time so that we can modify the raw data, e.g. available fields.
- WebMock
- .stub_request(:get, described_class::ROULETTE_DATA_URL)
- .to_return(body: teammate_json)
-
- subject.spin(project, categories, timezone_experiment: timezone_experiment)
- end
-
- before do
- allow(subject).to receive(:mr_author_username).and_return(author.username)
- allow(subject).to receive(:mr_labels).and_return(mr_labels)
- allow(subject).to receive(:mr_source_branch).and_return(mr_source_branch)
- end
-
- context 'when timezone_experiment == false' do
- context 'when change contains backend category' do
- let(:categories) { [:backend] }
-
- it 'assigns backend reviewer and maintainer' do
- expect(spins[0].reviewer).to eq(engineering_productivity_reviewer)
- expect(spins[0].maintainer).to eq(backend_maintainer)
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
- end
-
- context 'when teammate is not available' do
- let(:backend_available) { false }
-
- it 'assigns backend reviewer and no maintainer' do
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, false)])
- end
- end
- end
-
- context 'when change contains frontend category' do
- let(:categories) { [:frontend] }
-
- it 'assigns frontend reviewer and maintainer' do
- expect(spins).to eq([described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)])
- 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] }
-
- it 'assigns QA reviewer' do
- expect(spins).to eq([described_class::Spin.new(:qa, software_engineer_in_test, nil, false, false)])
- end
- end
-
- context 'when change contains Engineering Productivity category' do
- let(:categories) { [:engineering_productivity] }
-
- it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do
- expect(spins).to eq([described_class::Spin.new(:engineering_productivity, engineering_productivity_reviewer, backend_maintainer, false, false)])
- end
- end
-
- context 'when change contains CI/CD Template category' do
- let(:categories) { [:ci_template] }
-
- it 'assigns CI/CD Template reviewer and fallback to backend maintainer' do
- expect(spins).to eq([described_class::Spin.new(:ci_template, ci_template_reviewer, backend_maintainer, false, false)])
- end
- end
-
- context 'when change contains test category' do
- let(:categories) { [:test] }
-
- it 'assigns corresponding SET' do
- expect(spins).to eq([described_class::Spin.new(:test, software_engineer_in_test, nil, :maintainer, false)])
- end
- end
- end
-
- context 'when timezone_experiment == true' do
- let(:timezone_experiment) { true }
-
- context 'when change contains backend category' do
- let(:categories) { [:backend] }
-
- it 'assigns backend reviewer and maintainer' do
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, true)])
- end
-
- context 'when teammate is not in a good timezone' do
- let(:backend_tz_offset_hours) { 5.0 }
-
- it 'assigns backend reviewer and no maintainer' do
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, true)])
- end
- end
- end
-
- context 'when change includes a category with timezone disabled' do
- let(:categories) { [:backend] }
-
- before do
- stub_const("#{described_class}::INCLUDE_TIMEZONE_FOR_CATEGORY", backend: false)
- end
-
- it 'assigns backend reviewer and maintainer' do
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
- end
-
- context 'when teammate is not in a good timezone' do
- let(:backend_tz_offset_hours) { 5.0 }
-
- it 'assigns backend reviewer and maintainer' do
- expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
- end
- end
- end
- end
- end
-
- RSpec::Matchers.define :match_teammates do |expected|
- match do |actual|
- expected.each do |expected_person|
- actual_person_found = actual.find { |actual_person| actual_person.name == expected_person.username }
-
- actual_person_found &&
- actual_person_found.name == expected_person.name &&
- actual_person_found.role == expected_person.role &&
- actual_person_found.projects == expected_person.projects
- end
- end
- end
-
- describe '#team' do
- subject(:team) { roulette.team }
-
- context 'HTTP failure' do
- before do
- WebMock
- .stub_request(:get, described_class::ROULETTE_DATA_URL)
- .to_return(status: 404)
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to read/)
- end
- end
-
- context 'JSON failure' do
- before do
- WebMock
- .stub_request(:get, described_class::ROULETTE_DATA_URL)
- .to_return(body: 'INVALID JSON')
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to parse/)
- end
- end
-
- context 'success' do
- before do
- WebMock
- .stub_request(:get, described_class::ROULETTE_DATA_URL)
- .to_return(body: teammate_json)
- end
-
- it 'returns an array of teammates' do
- is_expected.to match_teammates([
- backend_maintainer,
- frontend_reviewer,
- frontend_maintainer,
- software_engineer_in_test,
- engineering_productivity_reviewer,
- ci_template_reviewer
- ])
- end
-
- it 'memoizes the result' do
- expect(team.object_id).to eq(roulette.team.object_id)
- end
- end
- end
-
- describe '#project_team' do
- subject { roulette.project_team('gitlab-qa') }
-
- before do
- WebMock
- .stub_request(:get, described_class::ROULETTE_DATA_URL)
- .to_return(body: teammate_json)
- end
-
- it 'filters team by project_name' do
- is_expected.to match_teammates([
- software_engineer_in_test
- ])
- end
- end
-
- describe '#spin_for_person' do
- let(:person_tz_offset_hours) { 0.0 }
- let(:person1) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'user1',
- 'available' => true,
- 'tz_offset_hours' => person_tz_offset_hours
- )
- end
-
- let(:person2) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'user2',
- 'available' => true,
- 'tz_offset_hours' => person_tz_offset_hours)
- end
-
- let(:author) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'johndoe',
- 'available' => true,
- 'tz_offset_hours' => 0.0)
- end
-
- let(:unavailable) do
- Gitlab::Danger::Teammate.new(
- 'username' => 'janedoe',
- 'available' => false,
- 'tz_offset_hours' => 0.0)
- end
-
- before do
- allow(subject).to receive(:mr_author_username).and_return(author.username)
- end
-
- (-4..4).each do |utc_offset|
- context "when local hour for person is #{10 + utc_offset} (offset: #{utc_offset})" do
- let(:person_tz_offset_hours) { utc_offset }
-
- [false, true].each do |timezone_experiment|
- context "with timezone_experiment == #{timezone_experiment}" do
- it 'returns a random person' do
- persons = [person1, person2]
-
- selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment)
-
- expect(persons.map(&:username)).to include(selected.username)
- end
- end
- end
- end
- end
-
- ((-12..-5).to_a + (5..12).to_a).each do |utc_offset|
- context "when local hour for person is #{10 + utc_offset} (offset: #{utc_offset})" do
- let(:person_tz_offset_hours) { utc_offset }
-
- [false, true].each do |timezone_experiment|
- context "with timezone_experiment == #{timezone_experiment}" do
- it 'returns a random person or nil' do
- persons = [person1, person2]
-
- selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment)
-
- if timezone_experiment
- expect(selected).to be_nil
- else
- expect(persons.map(&:username)).to include(selected.username)
- end
- end
- end
- end
- end
- end
-
- it 'excludes unavailable persons' do
- expect(subject.spin_for_person([unavailable], random: Random.new)).to be_nil
- end
-
- it 'excludes mr.author' do
- expect(subject.spin_for_person([author], random: Random.new)).to be_nil
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb b/spec/lib/gitlab/danger/sidekiq_queues_spec.rb
deleted file mode 100644
index 7dd1a2e6924..00000000000
--- a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-require_relative 'danger_spec_helper'
-
-require 'gitlab/danger/sidekiq_queues'
-
-RSpec.describe Gitlab::Danger::SidekiqQueues do
- using RSpec::Parameterized::TableSyntax
- include DangerSpecHelper
-
- let(:fake_git) { double('fake-git') }
- let(:fake_danger) { new_fake_danger.include(described_class) }
-
- subject(:sidekiq_queues) { fake_danger.new(git: fake_git) }
-
- describe '#changed_queue_files' do
- where(:modified_files, :changed_queue_files) do
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml) | %w(app/workers/all_queues.yml ee/app/workers/all_queues.yml)
- %w(app/workers/all_queues.yml foo) | %w(app/workers/all_queues.yml)
- %w(ee/app/workers/all_queues.yml foo) | %w(ee/app/workers/all_queues.yml)
- %w(foo) | %w()
- %w() | %w()
- end
-
- with_them do
- it do
- allow(fake_git).to receive(:modified_files).and_return(modified_files)
-
- expect(sidekiq_queues.changed_queue_files).to match_array(changed_queue_files)
- end
- end
- end
-
- describe '#added_queue_names' do
- it 'returns queue names added by this change' do
- old_queues = { post_receive: nil }
-
- allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues)
- allow(sidekiq_queues).to receive(:new_queues).and_return(old_queues.merge(merge: nil, process_commit: nil))
-
- expect(sidekiq_queues.added_queue_names).to contain_exactly(:merge, :process_commit)
- end
- end
-
- describe '#changed_queue_names' do
- it 'returns names for queues whose attributes were changed' do
- old_queues = {
- merge: { name: :merge, urgency: :low },
- post_receive: { name: :post_receive, urgency: :high },
- process_commit: { name: :process_commit, urgency: :high }
- }
-
- new_queues = old_queues.merge(mailers: { name: :mailers, urgency: :high },
- post_receive: { name: :post_receive, urgency: :low },
- process_commit: { name: :process_commit, urgency: :low })
-
- allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues)
- allow(sidekiq_queues).to receive(:new_queues).and_return(new_queues)
-
- expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive, :process_commit)
- end
-
- it 'ignores removed queues' do
- old_queues = {
- merge: { name: :merge, urgency: :low },
- post_receive: { name: :post_receive, urgency: :high }
- }
-
- new_queues = {
- post_receive: { name: :post_receive, urgency: :low }
- }
-
- allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues)
- allow(sidekiq_queues).to receive(:new_queues).and_return(new_queues)
-
- expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive)
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
deleted file mode 100644
index 9c066ba4c1b..00000000000
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ /dev/null
@@ -1,220 +0,0 @@
-# frozen_string_literal: true
-
-require 'timecop'
-require 'rspec-parameterized'
-
-require 'gitlab/danger/teammate'
-require 'active_support/testing/time_helpers'
-
-RSpec.describe Gitlab::Danger::Teammate do
- using RSpec::Parameterized::TableSyntax
-
- subject { described_class.new(options) }
-
- let(:tz_offset_hours) { 2.0 }
- let(:options) do
- {
- 'username' => 'luigi',
- 'projects' => projects,
- 'role' => role,
- 'markdown_name' => '[Luigi](https://gitlab.com/luigi) (`@luigi`)',
- 'tz_offset_hours' => tz_offset_hours
- }
- end
-
- let(:capabilities) { ['reviewer backend'] }
- let(:projects) { { project => capabilities } }
- let(:role) { 'Engineer, Manage' }
- let(:labels) { [] }
- let(:project) { double }
-
- describe '#==' do
- it 'compares Teammate username' do
- joe1 = described_class.new('username' => 'joe', 'projects' => projects)
- joe2 = described_class.new('username' => 'joe', 'projects' => [])
- jane1 = described_class.new('username' => 'jane', 'projects' => projects)
- jane2 = described_class.new('username' => 'jane', 'projects' => [])
-
- expect(joe1).to eq(joe2)
- expect(jane1).to eq(jane2)
- expect(jane1).not_to eq(nil)
- expect(described_class.new('username' => nil)).not_to eq(nil)
- end
- end
-
- describe '#to_h' do
- it 'returns the given options' do
- expect(subject.to_h).to eq(options)
- end
- end
-
- context 'when having multiple capabilities' do
- let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
-
- it '#reviewer? supports multiple roles per project' do
- expect(subject.reviewer?(project, :backend, labels)).to be_truthy
- end
-
- it '#traintainer? supports multiple roles per project' do
- expect(subject.traintainer?(project, :qa, labels)).to be_truthy
- end
-
- it '#maintainer? supports multiple roles per project' do
- expect(subject.maintainer?(project, :frontend, labels)).to be_truthy
- end
-
- context 'when labels contain devops::create and the category is test' do
- let(:labels) { ['devops::create'] }
-
- context 'when role is Software Engineer in Test, Create' do
- let(:role) { 'Software Engineer in Test, Create' }
-
- it '#reviewer? returns true' do
- expect(subject.reviewer?(project, :test, labels)).to be_truthy
- end
-
- it '#maintainer? returns false' do
- expect(subject.maintainer?(project, :test, labels)).to be_falsey
- end
-
- context 'when hyperlink is mangled in the role' do
- let(:role) { '<a href="#">Software Engineer in Test</a>, Create' }
-
- it '#reviewer? returns true' do
- expect(subject.reviewer?(project, :test, labels)).to be_truthy
- end
- end
- end
-
- context 'when role is Software Engineer in Test' do
- let(:role) { 'Software Engineer in Test' }
-
- it '#reviewer? returns false' do
- expect(subject.reviewer?(project, :test, labels)).to be_falsey
- end
- end
-
- context 'when role is Software Engineer in Test, Manage' do
- let(:role) { 'Software Engineer in Test, Manage' }
-
- it '#reviewer? returns false' do
- expect(subject.reviewer?(project, :test, labels)).to be_falsey
- end
- end
-
- context 'when role is Backend Engineer, Engineering Productivity' do
- let(:role) { 'Backend Engineer, Engineering Productivity' }
-
- it '#reviewer? returns true' do
- expect(subject.reviewer?(project, :engineering_productivity, labels)).to be_truthy
- end
-
- it '#maintainer? returns false' do
- expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_falsey
- end
-
- context 'when capabilities include maintainer backend' do
- let(:capabilities) { ['maintainer backend'] }
-
- it '#maintainer? returns true' do
- expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy
- end
- end
-
- context 'when capabilities include maintainer engineering productivity' do
- let(:capabilities) { ['maintainer engineering_productivity'] }
-
- it '#maintainer? returns true' do
- expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy
- end
- end
-
- context 'when capabilities include trainee_maintainer backend' do
- let(:capabilities) { ['trainee_maintainer backend'] }
-
- it '#traintainer? returns true' do
- expect(subject.traintainer?(project, :engineering_productivity, labels)).to be_truthy
- end
- end
- end
- end
- end
-
- context 'when having single capability' do
- let(:capabilities) { 'reviewer backend' }
-
- it '#reviewer? supports one role per project' do
- expect(subject.reviewer?(project, :backend, labels)).to be_truthy
- end
-
- it '#traintainer? supports one role per project' do
- expect(subject.traintainer?(project, :database, labels)).to be_falsey
- end
-
- it '#maintainer? supports one role per project' do
- expect(subject.maintainer?(project, :frontend, labels)).to be_falsey
- end
- end
-
- describe '#local_hour' do
- include ActiveSupport::Testing::TimeHelpers
-
- around do |example|
- travel_to(Time.utc(2020, 6, 23, 10)) { example.run }
- end
-
- context 'when author is given' do
- where(:tz_offset_hours, :expected_local_hour) do
- -12 | 22
- -10 | 0
- 2 | 12
- 4 | 14
- 12 | 22
- end
-
- with_them do
- it 'returns the correct local_hour' do
- expect(subject.local_hour).to eq(expected_local_hour)
- end
- end
- end
- end
-
- describe '#markdown_name' do
- it 'returns markdown name with timezone info' do
- expect(subject.markdown_name).to eq("#{options['markdown_name']} (UTC+2)")
- end
-
- context 'when offset is 1.5' do
- let(:tz_offset_hours) { 1.5 }
-
- it 'returns markdown name with timezone info, not truncated' do
- expect(subject.markdown_name).to eq("#{options['markdown_name']} (UTC+1.5)")
- end
- end
-
- context 'when author is given' do
- where(:tz_offset_hours, :author_offset, :diff_text) do
- -12 | -10 | "2 hours behind `@mario`"
- -10 | -12 | "2 hours ahead of `@mario`"
- -10 | 2 | "12 hours behind `@mario`"
- 2 | 4 | "2 hours behind `@mario`"
- 4 | 2 | "2 hours ahead of `@mario`"
- 2 | 3 | "1 hour behind `@mario`"
- 3 | 2 | "1 hour ahead of `@mario`"
- 2 | 2 | "same timezone as `@mario`"
- end
-
- with_them do
- it 'returns markdown name with timezone info' do
- author = described_class.new(options.merge('username' => 'mario', 'tz_offset_hours' => author_offset))
-
- floored_offset_hours = subject.__send__(:floored_offset_hours)
- utc_offset = floored_offset_hours >= 0 ? "+#{floored_offset_hours}" : floored_offset_hours
-
- expect(subject.markdown_name(author: author)).to eq("#{options['markdown_name']} (UTC#{utc_offset}, #{diff_text})")
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/title_linting_spec.rb b/spec/lib/gitlab/danger/title_linting_spec.rb
deleted file mode 100644
index b48d2c5e53d..00000000000
--- a/spec/lib/gitlab/danger/title_linting_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-require 'gitlab/danger/title_linting'
-
-RSpec.describe Gitlab::Danger::TitleLinting do
- using RSpec::Parameterized::TableSyntax
-
- describe '#sanitize_mr_title' do
- where(:mr_title, :expected_mr_title) do
- '`My MR title`' | "\\`My MR title\\`"
- 'WIP: My MR title' | 'My MR title'
- 'Draft: My MR title' | 'My MR title'
- '(Draft) My MR title' | 'My MR title'
- '[Draft] My MR title' | 'My MR title'
- '[DRAFT] My MR title' | 'My MR title'
- 'DRAFT: My MR title' | 'My MR title'
- 'DRAFT: `My MR title`' | "\\`My MR title\\`"
- end
-
- with_them do
- subject { described_class.sanitize_mr_title(mr_title) }
-
- it { is_expected.to eq(expected_mr_title) }
- end
- end
-
- describe '#remove_draft_flag' do
- where(:mr_title, :expected_mr_title) do
- 'WIP: My MR title' | 'My MR title'
- 'Draft: My MR title' | 'My MR title'
- '(Draft) My MR title' | 'My MR title'
- '[Draft] My MR title' | 'My MR title'
- '[DRAFT] My MR title' | 'My MR title'
- 'DRAFT: My MR title' | 'My MR title'
- end
-
- with_them do
- subject { described_class.remove_draft_flag(mr_title) }
-
- it { is_expected.to eq(expected_mr_title) }
- end
- end
-
- describe '#has_draft_flag?' do
- it 'returns true for a draft title' do
- expect(described_class.has_draft_flag?('Draft: My MR title')).to be true
- end
-
- it 'returns false for non draft title' do
- expect(described_class.has_draft_flag?('My MR title')).to be false
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb
deleted file mode 100644
index 066bb487fa2..00000000000
--- a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'gitlab/danger/weightage/maintainers'
-
-RSpec.describe Gitlab::Danger::Weightage::Maintainers do
- let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER }
- let(:regular_maintainer) { double('Teammate', reduced_capacity: false) }
- let(:reduced_capacity_maintainer) { double('Teammate', reduced_capacity: true) }
- let(:maintainers) do
- [
- regular_maintainer,
- reduced_capacity_maintainer
- ]
- end
-
- let(:maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
- let(:reduced_capacity_maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT }
-
- subject(:weighted_maintainers) { described_class.new(maintainers).execute }
-
- describe '#execute' do
- it 'weights the maintainers overall' do
- expect(weighted_maintainers.count).to eq maintainer_count + reduced_capacity_maintainer_count
- end
-
- it 'has total count of regular maintainers' do
- expect(weighted_maintainers.count { |r| r.object_id == regular_maintainer.object_id }).to eq maintainer_count
- end
-
- it 'has count of reduced capacity maintainers' do
- expect(weighted_maintainers.count { |r| r.object_id == reduced_capacity_maintainer.object_id }).to eq reduced_capacity_maintainer_count
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb
deleted file mode 100644
index cca81f4d9b5..00000000000
--- a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-require 'gitlab/danger/weightage/reviewers'
-
-RSpec.describe Gitlab::Danger::Weightage::Reviewers do
- let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER }
- let(:regular_reviewer) { double('Teammate', hungry: false, reduced_capacity: false) }
- let(:hungry_reviewer) { double('Teammate', hungry: true, reduced_capacity: false) }
- let(:reduced_capacity_reviewer) { double('Teammate', hungry: false, reduced_capacity: true) }
- let(:reviewers) do
- [
- hungry_reviewer,
- regular_reviewer,
- reduced_capacity_reviewer
- ]
- end
-
- let(:regular_traintainer) { double('Teammate', hungry: false, reduced_capacity: false) }
- let(:hungry_traintainer) { double('Teammate', hungry: true, reduced_capacity: false) }
- let(:reduced_capacity_traintainer) { double('Teammate', hungry: false, reduced_capacity: true) }
- let(:traintainers) do
- [
- hungry_traintainer,
- regular_traintainer,
- reduced_capacity_traintainer
- ]
- end
-
- let(:hungry_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT }
- let(:hungry_traintainer_count) { described_class::TRAINTAINER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT }
- let(:reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
- let(:traintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier }
- let(:reduced_capacity_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT }
- let(:reduced_capacity_traintainer_count) { described_class::TRAINTAINER_WEIGHT }
-
- subject(:weighted_reviewers) { described_class.new(reviewers, traintainers).execute }
-
- describe '#execute', :aggregate_failures do
- it 'weights the reviewers overall' do
- reviewers_count = hungry_reviewer_count + reviewer_count + reduced_capacity_reviewer_count
- traintainers_count = hungry_traintainer_count + traintainer_count + reduced_capacity_traintainer_count
-
- expect(weighted_reviewers.count).to eq reviewers_count + traintainers_count
- end
-
- it 'has total count of hungry reviewers and traintainers' do
- expect(weighted_reviewers.count(&:hungry)).to eq hungry_reviewer_count + hungry_traintainer_count
- expect(weighted_reviewers.count { |r| r.object_id == hungry_reviewer.object_id }).to eq hungry_reviewer_count
- expect(weighted_reviewers.count { |r| r.object_id == hungry_traintainer.object_id }).to eq hungry_traintainer_count
- end
-
- it 'has total count of regular reviewers and traintainers' do
- expect(weighted_reviewers.count { |r| r.object_id == regular_reviewer.object_id }).to eq reviewer_count
- expect(weighted_reviewers.count { |r| r.object_id == regular_traintainer.object_id }).to eq traintainer_count
- end
-
- it 'has count of reduced capacity reviewers' do
- expect(weighted_reviewers.count(&:reduced_capacity)).to eq reduced_capacity_reviewer_count + reduced_capacity_traintainer_count
- expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_reviewer.object_id }).to eq reduced_capacity_reviewer_count
- expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_traintainer.object_id }).to eq reduced_capacity_traintainer_count
- end
- end
-end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 2f74e766a11..4242469b3db 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Build do
- let(:runner) { create(:ci_runner, :instance) }
+ let!(:tag_names) { %w(tag-1 tag-2) }
+ let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, runner: runner, user: user) }
@@ -35,6 +36,7 @@ RSpec.describe Gitlab::DataBuilder::Build do
}
it { expect(data[:commit][:id]).to eq(build.pipeline.id) }
it { expect(data[:runner][:id]).to eq(build.runner.id) }
+ it { expect(data[:runner][:tags]).to match_array(tag_names) }
it { expect(data[:runner][:description]).to eq(build.runner.description) }
context 'commit author_url' do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 297d87708d8..fd7cadeb89e 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -51,13 +51,15 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
context 'build with runner' do
let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
- let(:ci_runner) { create(:ci_runner) }
+ let!(:tag_names) { %w(tag-1 tag-2) }
+ let(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
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?)
+ expect(runner_data[:tags]).to match_array(tag_names)
end
end
@@ -102,5 +104,16 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/-/merge_requests/#{merge_request.iid}")
end
end
+
+ context 'when pipeline has retried builds' do
+ before do
+ create(:ci_build, :retried, pipeline: pipeline)
+ end
+
+ it 'does not contain retried builds in payload' do
+ expect(data[:builds].count).to eq(1)
+ expect(build_data[:id]).to eq(build.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
new file mode 100644
index 00000000000..f132ecbf13b
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
+ include Database::TriggerHelpers
+
+ let(:migration) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ before do
+ allow(migration).to receive(:puts)
+ end
+
+ shared_examples_for 'Setting up to rename a column' do
+ let(:model) { Class.new(ActiveRecord::Base) }
+
+ before do
+ model.table_name = :test_table
+ end
+
+ context 'when called inside a transaction block' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end.to raise_error("#{operation} can not be run inside a transaction")
+ end
+ end
+
+ context 'when the existing column has a default value' do
+ before do
+ migration.change_column_default :test_table, existing_column, 'default value'
+ end
+
+ it 'raises an error' do
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end.to raise_error("#{operation} does not currently support columns with default values")
+ end
+ end
+
+ context 'when passing a batch column' do
+ context 'when the batch column does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :missing)
+ end.to raise_error('Column missing does not exist on test_table')
+ end
+ end
+
+ context 'when the batch column does exist' do
+ it 'passes it when creating the column' do
+ expect(migration).to receive(:create_column_from)
+ .with(:test_table, existing_column, added_column, type: nil, batch_column_name: :status)
+ .and_call_original
+
+ migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :status)
+ end
+ end
+ end
+
+ it 'creates the renamed column, syncing existing data' do
+ existing_record_1 = model.create!(status: 0, existing_column => 'existing')
+ existing_record_2 = model.create!(status: 0, existing_column => nil)
+
+ migration.send(operation, :test_table, :original, :renamed)
+ model.reset_column_information
+
+ expect(migration.column_exists?(:test_table, added_column)).to eq(true)
+
+ expect(existing_record_1.reload).to have_attributes(status: 0, original: 'existing', renamed: 'existing')
+ expect(existing_record_2.reload).to have_attributes(status: 0, original: nil, renamed: nil)
+ end
+
+ it 'installs triggers to sync new data' do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ model.reset_column_information
+
+ new_record_1 = model.create!(status: 1, original: 'first')
+ new_record_2 = model.create!(status: 1, renamed: 'second')
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'first', renamed: 'first')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: 'second', renamed: 'second')
+
+ new_record_1.update!(original: 'updated')
+ new_record_2.update!(renamed: nil)
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'updated', renamed: 'updated')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: nil, renamed: nil)
+ end
+ end
+
+ describe '#rename_column_concurrently' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :original
+ t.text :other_column
+ end
+ end
+
+ it_behaves_like 'Setting up to rename a column' do
+ let(:operation) { :rename_column_concurrently }
+ let(:existing_column) { :original }
+ let(:added_column) { :renamed }
+ end
+
+ context 'when the column to rename does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.rename_column_concurrently :test_table, :missing_column, :renamed
+ end.to raise_error('Column missing_column does not exist on test_table')
+ end
+ end
+ end
+
+ describe '#undo_cleanup_concurrent_column_rename' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :other_column
+ t.text :renamed
+ end
+ end
+
+ it_behaves_like 'Setting up to rename a column' do
+ let(:operation) { :undo_cleanup_concurrent_column_rename }
+ let(:existing_column) { :renamed }
+ let(:added_column) { :original }
+ end
+
+ context 'when the renamed column does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.undo_cleanup_concurrent_column_rename :test_table, :original, :missing_column
+ end.to raise_error('Column missing_column does not exist on test_table')
+ end
+ end
+ end
+
+ shared_examples_for 'Cleaning up from renaming a column' do
+ let(:connection) { migration.connection }
+
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :original
+ t.text :other_column
+ end
+
+ migration.rename_column_concurrently :test_table, :original, :renamed
+ end
+
+ context 'when the helper is called repeatedly' do
+ before do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end
+
+ it 'does not make repeated attempts to cleanup' do
+ expect(migration).not_to receive(:remove_column)
+
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end.not_to raise_error
+ end
+ end
+
+ context 'when the renamed column exists' do
+ let(:triggers) do
+ [
+ ['trigger_7cc71f92fd63', 'function_for_trigger_7cc71f92fd63', before: 'insert'],
+ ['trigger_f1a1f619636a', 'function_for_trigger_f1a1f619636a', before: 'update'],
+ ['trigger_769a49938884', 'function_for_trigger_769a49938884', before: 'update']
+ ]
+ end
+
+ it 'removes the sync triggers and renamed columns' do
+ triggers.each do |(trigger_name, function_name, event)|
+ expect_function_to_exist(function_name)
+ expect_valid_function_trigger(:test_table, trigger_name, function_name, event)
+ end
+
+ expect(migration.column_exists?(:test_table, added_column)).to eq(true)
+
+ migration.public_send(operation, :test_table, :original, :renamed)
+
+ expect(migration.column_exists?(:test_table, added_column)).to eq(false)
+
+ triggers.each do |(trigger_name, function_name, _)|
+ expect_trigger_not_to_exist(:test_table, trigger_name)
+ expect_function_not_to_exist(function_name)
+ end
+ end
+ end
+ end
+
+ describe '#undo_rename_column_concurrently' do
+ it_behaves_like 'Cleaning up from renaming a column' do
+ let(:operation) { :undo_rename_column_concurrently }
+ let(:added_column) { :renamed }
+ end
+ end
+
+ describe '#cleanup_concurrent_column_rename' do
+ it_behaves_like 'Cleaning up from renaming a column' do
+ let(:operation) { :cleanup_concurrent_column_rename }
+ let(:added_column) { :original }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 6b709cba5b3..6de7fc3a50e 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1874,7 +1874,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
has_internal_id :iid,
scope: :project,
init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) },
- backfill: true,
presence: false
end
end
@@ -1928,258 +1927,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(issue_b.iid).to eq(3)
end
- context 'when the new code creates a row post deploy but before the migration runs' do
- it 'does not change the row iid' do
- project = setup
- issue = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue.reload.iid).to eq(1)
- end
-
- it 'backfills iids for rows already in the database' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- end
-
- it 'backfills iids across multiple projects' do
- project_a = setup
- project_b = setup
- issue_a = issues.create!(project_id: project_a.id)
- issue_b = issues.create!(project_id: project_b.id)
- issue_c = Issue.create!(project_id: project_a.id)
- issue_d = Issue.create!(project_id: project_b.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(1)
- expect(issue_c.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(2)
- end
-
- it 'generates iids properly for models created after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_d = Issue.create!(project_id: project.id)
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.iid).to eq(4)
- expect(issue_e.iid).to eq(5)
- end
-
- it 'backfills iids and properly generates iids for new models across multiple projects' do
- project_a = setup
- project_b = setup
- issue_a = issues.create!(project_id: project_a.id)
- issue_b = issues.create!(project_id: project_b.id)
- issue_c = Issue.create!(project_id: project_a.id)
- issue_d = Issue.create!(project_id: project_b.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project_a.id)
- issue_f = Issue.create!(project_id: project_b.id)
- issue_g = Issue.create!(project_id: project_a.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(1)
- expect(issue_c.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(2)
- expect(issue_e.iid).to eq(3)
- expect(issue_f.iid).to eq(3)
- expect(issue_g.iid).to eq(4)
- end
- end
-
- context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = Issue.create!(project_id: project.id)
- issue_c = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- end
-
- it 'generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- expect(issue_e.iid).to eq(5)
- end
- end
-
- context 'when the new code and old code alternate creating models post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = Issue.create!(project_id: project.id)
- issue_c = issues.create!(project_id: project.id)
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- end
-
- it 'generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_d = issues.create!(project_id: project.id)
- issue_e = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_f = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- expect(issue_e.reload.iid).to eq(5)
- expect(issue_f.iid).to eq(6)
- end
- end
-
- context 'when the new code creates and deletes a model post deploy but before the migration runs' do
- it 'backfills iids for rows already in the database' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- end
-
- it 'successfully creates a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
-
- model.backfill_iids('issues')
-
- issue_d = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.iid).to eq(3)
- end
- end
-
- context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- end
-
- it 'successfully creates a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- expect(issue_e.iid).to eq(4)
- end
- end
-
- context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do
- it 'successfully generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- end
-
- it 'successfully generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- expect(issue_e.iid).to eq(4)
- end
- end
-
context 'when the first model is created for a project after the migration' do
it 'generates an iid' do
project_a = setup
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
new file mode 100644
index 00000000000..3804dc52a77
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Instrumentation do
+ describe '#observe' do
+ subject { described_class.new }
+
+ let(:migration) { 1234 }
+
+ it 'executes the given block' do
+ expect { |b| subject.observe(migration, &b) }.to yield_control
+ end
+
+ context 'behavior with observers' do
+ subject { described_class.new(observers).observe(migration) {} }
+
+ let(:observers) { [observer] }
+ let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) }
+
+ it 'calls #before, #after, #record on given observers' do
+ expect(observer).to receive(:before).ordered
+ expect(observer).to receive(:after).ordered
+ expect(observer).to receive(:record).ordered do |observation|
+ expect(observation.migration).to eq(migration)
+ end
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #before' do
+ expect(observer).to receive(:before).and_raise('some error')
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #after' do
+ expect(observer).to receive(:after).and_raise('some error')
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #record' do
+ expect(observer).to receive(:record).and_raise('some error')
+
+ subject
+ end
+ end
+
+ context 'on successful execution' do
+ subject { described_class.new.observe(migration) {} }
+
+ it 'records walltime' do
+ expect(subject.walltime).not_to be_nil
+ end
+
+ it 'records success' do
+ expect(subject.success).to be_truthy
+ end
+
+ it 'records the migration version' do
+ expect(subject.migration).to eq(migration)
+ end
+ end
+
+ context 'upon failure' do
+ subject { described_class.new.observe(migration) { raise 'something went wrong' } }
+
+ it 'raises the exception' do
+ expect { subject }.to raise_error(/something went wrong/)
+ end
+
+ context 'retrieving observations' do
+ subject { instance.observations.first }
+
+ before do
+ instance.observe(migration) { raise 'something went wrong' }
+ rescue
+ # ignore
+ end
+
+ let(:instance) { described_class.new }
+
+ it 'records walltime' do
+ expect(subject.walltime).not_to be_nil
+ end
+
+ it 'records failure' do
+ expect(subject.success).to be_falsey
+ end
+
+ it 'records the migration version' do
+ expect(subject.migration).to eq(migration)
+ end
+ end
+ end
+
+ context 'sequence of migrations with failures' do
+ subject { described_class.new }
+
+ let(:migration1) { double('migration1', call: nil) }
+ let(:migration2) { double('migration2', call: nil) }
+
+ it 'records observations for all migrations' do
+ subject.observe('migration1') {}
+ subject.observe('migration2') { raise 'something went wrong' } rescue nil
+
+ expect(subject.observations.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
new file mode 100644
index 00000000000..73466471944
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do
+ subject { described_class.new }
+
+ let(:observation) { Gitlab::Database::Migrations::Observation.new }
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:query) { 'select pg_database_size(current_database())' }
+
+ it 'records the size change' do
+ expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 1024 }])
+ expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 256 }])
+
+ subject.before
+ subject.after
+ subject.record(observation)
+
+ expect(observation.total_database_size_change).to eq(256 - 1024)
+ end
+
+ context 'out of order calls' do
+ before do
+ allow(connection).to receive(:execute).with(query).and_return([{ 'pg_database_size' => 1024 }])
+ end
+
+ it 'does not record anything if before size is unknown' do
+ subject.after
+
+ expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ end
+
+ it 'does not record anything if after size is unknown' do
+ subject.before
+
+ expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index b50e02c7043..b5d741fc5e9 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -513,6 +513,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
context 'finishing pending background migration jobs' do
let(:source_table_double) { double('table name') }
let(:raw_arguments) { [1, 50_000, source_table_double, partitioned_table, source_column] }
+ let(:background_job) { double('background job', args: ['background jobs', raw_arguments]) }
before do
allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true)
@@ -528,7 +529,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
expect(Gitlab::BackgroundMigration).to receive(:steal)
.with(described_class::MIGRATION_CLASS_NAME)
- .and_yield(raw_arguments)
+ .and_yield(background_job)
expect(source_table_double).to receive(:==).with(source_table.to_s)
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 220ae705e71..563399ff0d9 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -54,6 +54,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do
lock_fiber.resume # start the transaction and lock the table
end
+ after do
+ lock_fiber.resume if lock_fiber.alive?
+ end
+
context 'lock_fiber' do
it 'acquires lock successfully' do
check_exclusive_lock_query = """
diff --git a/spec/lib/gitlab/diff/char_diff_spec.rb b/spec/lib/gitlab/diff/char_diff_spec.rb
new file mode 100644
index 00000000000..e4e2a3ba050
--- /dev/null
+++ b/spec/lib/gitlab/diff/char_diff_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'diff_match_patch'
+
+RSpec.describe Gitlab::Diff::CharDiff do
+ let(:old_string) { "Helo \n Worlld" }
+ let(:new_string) { "Hello \n World" }
+
+ subject(:diff) { described_class.new(old_string, new_string) }
+
+ describe '#generate_diff' do
+ context 'when old string is nil' do
+ let(:old_string) { nil }
+
+ it 'does not raise an error' do
+ expect { subject.generate_diff }.not_to raise_error
+ end
+
+ it 'treats nil values as blank strings' do
+ changes = subject.generate_diff
+
+ expect(changes).to eq([
+ [:insert, "Hello \n World"]
+ ])
+ end
+ end
+
+ it 'generates an array of changes' do
+ changes = subject.generate_diff
+
+ expect(changes).to eq([
+ [:equal, "Hel"],
+ [:insert, "l"],
+ [:equal, "o \n Worl"],
+ [:delete, "l"],
+ [:equal, "d"]
+ ])
+ end
+ end
+
+ describe '#changed_ranges' do
+ subject { diff.changed_ranges }
+
+ context 'when old string is nil' do
+ let(:old_string) { nil }
+
+ it 'returns lists of changes' do
+ old_diffs, new_diffs = subject
+
+ expect(old_diffs).to eq([])
+ expect(new_diffs).to eq([0..12])
+ end
+ end
+
+ it 'returns ranges of changes' do
+ old_diffs, new_diffs = subject
+
+ expect(old_diffs).to eq([11..11])
+ expect(new_diffs).to eq([3..3])
+ end
+ end
+
+ describe '#to_html' do
+ it 'returns an HTML representation of the diff' do
+ subject.generate_diff
+
+ expect(subject.to_html).to eq(
+ '<span class="idiff">Hel</span>' \
+ '<span class="idiff addition">l</span>' \
+ "<span class=\"idiff\">o \n Worl</span>" \
+ '<span class="idiff deletion">l</span>' \
+ '<span class="idiff">d</span>'
+ )
+ 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
index 8822fc55c6e..9ba9271cefc 100644
--- a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::FileCollectionSorter do
let(:diffs) do
[
+ double(new_path: 'README', old_path: 'README'),
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: '1-folder/README', old_path: '1-folder/README'),
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: '1-folder/README', old_path: '1-folder/README'),
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'),
@@ -19,7 +22,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
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')
+ double(new_path: 'Z-file.ext', old_path: 'Z-file.ext'),
+ double(new_path: 'README', old_path: 'README')
]
end
@@ -36,6 +40,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
'1-folder/nested/Z-file.ext',
'1-folder/A-file.ext',
'1-folder/M-file.ext',
+ '1-folder/README',
+ '1-folder/README',
'1-folder/Z-file.ext',
'2-folder/nested/A-file.ext',
'2-folder/A-file.ext',
@@ -44,6 +50,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
'.file',
'A-file.ext',
'M-file.ext',
+ 'README',
+ 'README',
'Z-file.ext'
])
end
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index f6810d7a966..94717152488 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -233,4 +233,22 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
cache.write_if_empty
end
end
+
+ describe '#key' do
+ subject { cache.key }
+
+ it 'returns the next version of the cache' do
+ is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:2")
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(improved_merge_diff_highlighting: false)
+ end
+
+ it 'returns the original version of the cache' do
+ is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:1")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 35284e952f7..dce655d5690 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -37,6 +37,33 @@ RSpec.describe Gitlab::Diff::InlineDiff do
it 'can handle unchanged empty lines' do
expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error
end
+
+ context 'when lines have multiple changes' do
+ let(:diff) do
+ <<~EOF
+ - Hello, how are you?
+ + Hi, how are you doing?
+ EOF
+ end
+
+ let(:subject) { described_class.for_lines(diff.lines) }
+
+ it 'finds all inline diffs' do
+ expect(subject[0]).to eq([3..6])
+ expect(subject[1]).to eq([3..3, 17..22])
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(improved_merge_diff_highlighting: false)
+ end
+
+ it 'finds all inline diffs' do
+ expect(subject[0]).to eq([3..19])
+ expect(subject[1]).to eq([3..22])
+ end
+ end
+ end
end
describe "#inline_diffs" 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 b1ffbedc7bf..eb11c051adc 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -40,6 +40,13 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
expect(new_issue.description).to eq(expected_description.strip)
end
+ it 'creates an issue_email_participant' do
+ receiver.execute
+ new_issue = Issue.last
+
+ expect(new_issue.issue_email_participants.first.email).to eq("jake@adventuretime.ooo")
+ end
+
it 'sends thank you email' do
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
end
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
index c47f71c207d..1cebe37bea5 100644
--- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -10,6 +10,10 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
use_backwards_compatible_subject_index: true
},
test_experiment: {
+ tracking_category: 'Team',
+ rollout_strategy: rollout_strategy
+ },
+ my_experiment: {
tracking_category: 'Team'
}
}
@@ -20,6 +24,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
let(:enabled_percentage) { 10 }
+ let(:rollout_strategy) { nil }
controller(ApplicationController) do
include Gitlab::Experimentation::ControllerConcern
@@ -117,6 +122,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
context 'when subject is given' do
+ let(:rollout_strategy) { :user }
let(:user) { build(:user) }
it 'uses the subject' do
@@ -244,6 +250,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it "provides the subject's hashed global_id as label" do
experiment_subject = double(:subject, to_global_id: 'abc')
+ allow(Gitlab::Experimentation).to receive(:valid_subject_for_rollout_strategy?).and_return(true)
controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject)
@@ -420,6 +427,26 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
controller.record_experiment_user(:test_experiment, context)
end
+
+ context 'with a cookie based rollout strategy' do
+ it 'calls tracking_group with a nil subject' do
+ expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: nil).and_return(:experimental)
+ allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context)
+
+ controller.record_experiment_user(:test_experiment, context)
+ end
+ end
+
+ context 'with a user based rollout strategy' do
+ let(:rollout_strategy) { :user }
+
+ it 'calls tracking_group with a user subject' do
+ expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: user).and_return(:experimental)
+ allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context)
+
+ controller.record_experiment_user(:test_experiment, context)
+ end
+ end
end
context 'the user is part of the control group' do
diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb
index 008e6699597..94dbf1d7e4b 100644
--- a/spec/lib/gitlab/experimentation/experiment_spec.rb
+++ b/spec/lib/gitlab/experimentation/experiment_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe Gitlab::Experimentation::Experiment do
let(:params) do
{
tracking_category: 'Category1',
- use_backwards_compatible_subject_index: true
+ use_backwards_compatible_subject_index: true,
+ rollout_strategy: nil
}
end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index b503960b8c7..7eeae3f3f33 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -7,7 +7,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
it 'temporarily ensures we know what experiments exist for backwards compatibility' do
expected_experiment_keys = [
- :onboarding_issues,
:ci_notification_dot,
:upgrade_link_in_user_menu_a,
:invite_members_version_a,
@@ -15,8 +14,7 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
:invite_members_empty_group_version_a,
:contact_sales_btn_in_app,
:customize_homepage,
- :group_only_trials,
- :default_to_issues_board
+ :group_only_trials
]
backwards_compatible_experiment_keys = described_class.filter { |_, v| v[:use_backwards_compatible_subject_index] }.keys
@@ -27,6 +25,8 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
end
RSpec.describe Gitlab::Experimentation do
+ using RSpec::Parameterized::TableSyntax
+
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
backwards_compatible_test_experiment: {
@@ -35,6 +35,10 @@ RSpec.describe Gitlab::Experimentation do
},
test_experiment: {
tracking_category: 'Team'
+ },
+ tabular_experiment: {
+ tracking_category: 'Team',
+ rollout_strategy: rollout_strategy
}
})
@@ -46,6 +50,7 @@ RSpec.describe Gitlab::Experimentation do
end
let(:enabled_percentage) { 10 }
+ let(:rollout_strategy) { nil }
describe '.get_experiment' do
subject { described_class.get_experiment(:test_experiment) }
@@ -175,4 +180,59 @@ RSpec.describe Gitlab::Experimentation do
end
end
end
+
+ describe '.log_invalid_rollout' do
+ subject { described_class.log_invalid_rollout(:test_experiment, 1) }
+
+ before do
+ allow(described_class).to receive(:valid_subject_for_rollout_strategy?).and_return(valid)
+ end
+
+ context 'subject is not valid for experiment' do
+ let(:valid) { false }
+
+ it 'logs a warning message' do
+ expect_next_instance_of(Gitlab::ExperimentationLogger) do |logger|
+ expect(logger)
+ .to receive(:warn)
+ .with(
+ message: 'Subject must conform to the rollout strategy',
+ experiment_key: :test_experiment,
+ subject: 'Integer',
+ rollout_strategy: :cookie
+ )
+ end
+
+ subject
+ end
+ end
+
+ context 'subject is valid for experiment' do
+ let(:valid) { true }
+
+ it 'does not log a warning message' do
+ expect(Gitlab::ExperimentationLogger).not_to receive(:build)
+
+ subject
+ end
+ end
+ end
+
+ describe '.valid_subject_for_rollout_strategy?' do
+ subject { described_class.valid_subject_for_rollout_strategy?(:tabular_experiment, experiment_subject) }
+
+ where(:rollout_strategy, :experiment_subject, :result) do
+ :cookie | nil | true
+ nil | nil | true
+ :cookie | 'string' | true
+ nil | User.new | false
+ :user | User.new | true
+ :group | User.new | false
+ :group | Group.new | true
+ end
+
+ with_them do
+ it { is_expected.to be(result) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 8d6df62b3f6..0b5303f22b4 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do
end
end
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
+
+ expect(results.count).to eq(1)
+ end
+ end
+
it 'does not cause N+1 query' do
expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 8961cdcae7d..49f1e6e994f 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -720,7 +720,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
committer_name: "Dmitriy Zaporozhets",
id: SeedRepo::Commit::ID,
message: "tree css fixes",
- parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
+ parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"],
+ trailers: {}
}
end
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 783f0a9ccf7..17bb83d0f2f 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -100,6 +100,13 @@ EOT
expect(diff.diff).to be_empty
expect(diff).to be_too_large
end
+
+ it 'logs the event' do
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:patch_hard_limit_bytes_hit)
+
+ diff
+ end
end
context 'using a collapsable diff that is too large' do
diff --git a/spec/lib/gitlab/git/push_spec.rb b/spec/lib/gitlab/git/push_spec.rb
index 0f52f10c0a6..8ba43b2967c 100644
--- a/spec/lib/gitlab/git/push_spec.rb
+++ b/spec/lib/gitlab/git/push_spec.rb
@@ -86,6 +86,16 @@ RSpec.describe Gitlab::Git::Push do
it { is_expected.to be_force_push }
end
+
+ context 'when called muiltiple times' do
+ it 'does not make make multiple calls to the force push check' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).once
+
+ 2.times do
+ subject.force_push?
+ end
+ end
+ end
end
describe '#branch_added?' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index ef9b5a30c86..cc1b1ceadcf 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1894,8 +1894,11 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'removes the remote' do
repository_rugged.remotes.create(remote_name, url)
- repository.remove_remote(remote_name)
+ expect(repository.remove_remote(remote_name)).to be true
+ # Since we deleted the remote via Gitaly, Rugged doesn't know
+ # this changed underneath it. Let's refresh the Rugged repo.
+ repository_rugged = Rugged::Repository.new(repository_path)
expect(repository_rugged.remotes[remote_name]).to be_nil
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a0cafe3d763..9a1ecfe6459 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -388,108 +388,6 @@ 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
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index ce01566b870..22707c9a36b 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -299,6 +299,11 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
let(:commit_message) { 'Squash message' }
+
+ let(:time) do
+ Time.now.utc
+ end
+
let(:request) do
Gitaly::UserSquashRequest.new(
repository: repository.gitaly_repository,
@@ -307,7 +312,8 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
start_sha: start_sha,
end_sha: end_sha,
author: gitaly_user,
- commit_message: commit_message
+ commit_message: commit_message,
+ timestamp: Google::Protobuf::Timestamp.new(seconds: time.to_i)
)
end
@@ -315,7 +321,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) }
subject do
- client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message)
+ client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message, time)
end
it 'sends a user_squash message and returns the squash sha' do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 7fcb11c4dfd..a8d42f4bccf 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -267,31 +267,63 @@ RSpec.describe Gitlab::GitalyClient do
end
describe '.request_kwargs' do
- context 'when catfile-cache feature is enabled' do
- before do
- stub_feature_flags('gitaly_catfile-cache': true)
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', timeout: 1)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
end
+ end
- it 'sets the gitaly-session-id in the metadata' do
- results = described_class.request_kwargs('default', timeout: 1)
- expect(results[:metadata]).to include('gitaly-session-id')
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
end
+ end
- context 'when RequestStore is not enabled' do
- it 'sets a different gitaly-session-id per request' do
- gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+ context 'gitlab_git_env' do
+ let(:policy) { 'gitaly-route-repository-accessor-policy' }
- expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ context 'when RequestStore is disabled' do
+ it 'does not force-route to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
end
end
- context 'when RequestStore is enabled', :request_store do
- it 'sets the same gitaly-session-id on every outgoing request metadata' do
- gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+ context 'when RequestStore is enabled without git_env', :request_store do
+ it 'does not force-orute to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
+ end
+ end
- 3.times do
- expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
- end
+ context 'when RequestStore is enabled with empty git_env', :request_store do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_env] = {}
+ end
+
+ it 'disables force-routing to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
+ end
+ end
+
+ context 'when RequestStore is enabled with populated git_env', :request_store do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_env] = {
+ "GIT_OBJECT_DIRECTORY_RELATIVE" => "foo/bar"
+ }
+ end
+
+ it 'enables force-routing to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to eq('primary-only')
end
end
end
diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
new file mode 100644
index 00000000000..e89e5c17644
--- /dev/null
+++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Tests that our connections are correctly mapped.
+RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
+ include GraphqlHelpers
+
+ before(:all) do
+ ActiveRecord::Schema.define do
+ create_table :testing_pagination_nodes, force: true do |t|
+ t.integer :value, null: false
+ end
+ end
+ end
+
+ after(:all) do
+ ActiveRecord::Schema.define do
+ drop_table :testing_pagination_nodes, force: true
+ end
+ end
+
+ let_it_be(:node_model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'testing_pagination_nodes'
+ end
+ end
+
+ let(:query_string) { 'query { items(first: 2) { nodes { value } } }' }
+ let(:user) { nil }
+
+ let(:node) { Struct.new(:value) }
+ let(:node_type) do
+ Class.new(::GraphQL::Schema::Object) do
+ graphql_name 'Node'
+ field :value, GraphQL::INT_TYPE, null: false
+ end
+ end
+
+ let(:query_type) do
+ item_values = nodes
+
+ query_factory do |t|
+ t.field :items, node_type.connection_type, null: true
+
+ t.define_method :items do
+ item_values
+ end
+ end
+ end
+
+ shared_examples 'it maps to a specific connection class' do |connection_type|
+ let(:raw_values) { [1, 7, 42] }
+
+ it "maps to #{connection_type.name}" do
+ expect(connection_type).to receive(:new).and_call_original
+
+ results = execute_query(query_type).to_h
+
+ expect(graphql_dig_at(results, :data, :items, :nodes, :value)).to eq [1, 7]
+ end
+ end
+
+ describe 'OffsetPaginatedRelation' do
+ before do
+ # Expect to be ordered by an explicit ordering.
+ raw_values.each_with_index { |value, id| node_model.create!(id: id, value: value) }
+ end
+
+ let(:nodes) { ::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(node_model.order(value: :asc)) }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
+ end
+
+ describe 'ActiveRecord::Relation' do
+ before do
+ # Expect to be ordered by ID descending
+ [3, 2, 1].zip(raw_values) { |id, value| node_model.create!(id: id, value: value) }
+ end
+
+ let(:nodes) { node_model.all }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::Keyset::Connection
+ end
+
+ describe 'ExternallyPaginatedArray' do
+ let(:nodes) { ::Gitlab::Graphql::ExternallyPaginatedArray.new(nil, nil, node.new(1), node.new(7)) }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection
+ end
+
+ describe 'Array' do
+ let(:nodes) { raw_values.map { |x| node.new(x) } }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ArrayConnection
+ end
+end
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
index 6e08a87523f..a140a283c1b 100644
--- a/spec/lib/gitlab/graphql/queries_spec.rb
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -151,6 +151,10 @@ RSpec.describe Gitlab::Graphql::Queries do
let(:path) { 'post_by_slug.graphql' }
it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'has a complexity' do
+ expect(subject.complexity(schema)).to be < 10
+ end
end
context 'a query with an import' do
diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb
index 1c1efe178e2..287ebcec207 100644
--- a/spec/lib/gitlab/health_checks/master_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/master_check_spec.rb
@@ -4,47 +4,67 @@ require 'spec_helper'
require_relative './simple_check_shared'
RSpec.describe Gitlab::HealthChecks::MasterCheck do
- let(:result_class) { Gitlab::HealthChecks::Result }
-
before do
stub_const('SUCCESS_CODE', 100)
stub_const('FAILURE_CODE', 101)
- described_class.register_master
end
- after do
- described_class.finish_master
- end
+ context 'when Puma runs in Clustered mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
- describe '#readiness' do
- context 'when master is running' do
- it 'worker does return success' do
- _, child_status = run_worker
+ described_class.register_master
+ end
- expect(child_status.exitstatus).to eq(SUCCESS_CODE)
- end
+ after do
+ described_class.finish_master
end
- context 'when master finishes early' do
- before do
- described_class.send(:close_write)
+ describe '.available?' do
+ specify { expect(described_class.available?).to be true }
+ end
+
+ describe '.readiness' do
+ context 'when master is running' do
+ it 'worker does return success' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(SUCCESS_CODE)
+ end
end
- it 'worker does return failure' do
- _, child_status = run_worker
+ context 'when master finishes early' do
+ before do
+ described_class.send(:close_write)
+ end
- expect(child_status.exitstatus).to eq(FAILURE_CODE)
+ it 'worker does return failure' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(FAILURE_CODE)
+ end
end
- end
- def run_worker
- pid = fork do
- described_class.register_worker
+ def run_worker
+ pid = fork do
+ described_class.register_worker
- exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
+ exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
+ end
+
+ Process.wait2(pid)
end
+ end
+ end
+
+ # '.readiness' check is not invoked if '.available?' returns false
+ context 'when Puma runs in Single mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
+ end
- Process.wait2(pid)
+ describe '.available?' do
+ specify { expect(described_class.available?).to be false }
end
end
end
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
index 03138e936aa..69828c143db 100644
--- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -61,6 +61,35 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
expect(subject.json[:message]).to eq('Redis::CannotConnectError : Redis down')
end
end
+
+ context 'when some checks are not available' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
+ end
+
+ let(:checks) do
+ [
+ Gitlab::HealthChecks::MasterCheck
+ ]
+ end
+
+ it 'asks for check availability' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:available?)
+
+ subject
+ end
+
+ it 'does not call `readiness` on checks that are not available' do
+ expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:readiness)
+
+ subject
+ end
+
+ it 'does not fail collection check' do
+ expect(subject.http_status).to eq(200)
+ expect(subject.json[:status]).to eq('ok')
+ end
+ end
end
context 'without checks' do
diff --git a/spec/lib/gitlab/hook_data/group_builder_spec.rb b/spec/lib/gitlab/hook_data/group_builder_spec.rb
new file mode 100644
index 00000000000..d7347ff99d4
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/group_builder_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::GroupBuilder do
+ let_it_be(:group) { create(:group) }
+
+ describe '#build' do
+ let(:data) { described_class.new(group).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id
+ ]
+ 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[:name]).to eq(group.name)
+ expect(data[:path]).to eq(group.path)
+ expect(data[:full_path]).to eq(group.full_path)
+ expect(data[:group_id]).to eq(group.id)
+ expect(data[:created_at]).to eq(group.created_at.xmlschema)
+ expect(data[:updated_at]).to eq(group.updated_at.xmlschema)
+ end
+ end
+
+ shared_examples_for 'does not include old path attributes' do
+ it 'does not include old path attributes' do
+ expect(data).not_to include(:old_path, :old_full_path)
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('group_create') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include old path attributes'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('group_destroy') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include old path attributes'
+ end
+
+ context 'on rename' do
+ let(:event) { :rename }
+
+ it { expect(event_name).to eq('group_rename') }
+ it_behaves_like 'includes the required attributes'
+
+ it 'includes old path details' do
+ allow(group).to receive(:path_before_last_save).and_return('old-path')
+
+ expect(data[:old_path]).to eq(group.path_before_last_save)
+ expect(data[:old_full_path]).to eq(group.path_before_last_save)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb
new file mode 100644
index 00000000000..89e5dffd7b4
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::SubgroupBuilder do
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ describe '#build' do
+ let(:data) { described_class.new(subgroup).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id,
+ :parent_group_id, :parent_name, :parent_path, :parent_full_path
+ ]
+ 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[:name]).to eq(subgroup.name)
+ expect(data[:path]).to eq(subgroup.path)
+ expect(data[:full_path]).to eq(subgroup.full_path)
+ expect(data[:group_id]).to eq(subgroup.id)
+ expect(data[:created_at]).to eq(subgroup.created_at.xmlschema)
+ expect(data[:updated_at]).to eq(subgroup.updated_at.xmlschema)
+ expect(data[:parent_name]).to eq(parent_group.name)
+ expect(data[:parent_path]).to eq(parent_group.path)
+ expect(data[:parent_full_path]).to eq(parent_group.full_path)
+ expect(data[:parent_group_id]).to eq(parent_group.id)
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('subgroup_create') }
+ it_behaves_like 'includes the required attributes'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('subgroup_destroy') }
+ it_behaves_like 'includes the required attributes'
+ 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 825513bdfc5..d0282e14d5f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -146,6 +146,7 @@ merge_requests:
- merge_user
- merge_request_diffs
- merge_request_diff
+- merge_head_diff
- merge_request_context_commits
- merge_request_context_commit_diff_files
- events
@@ -544,7 +545,7 @@ project:
- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
-- compliance_management_frameworks
+- compliance_management_framework
- metrics_users_starred_dashboards
- alert_management_alerts
- repository_storage_moves
@@ -560,7 +561,10 @@ project:
- alert_management_http_integrations
- exported_protected_branches
- incident_management_oncall_schedules
+- incident_management_oncall_rotations
- debian_distributions
+- merge_request_metrics
+- security_orchestration_policy_configuration
award_emoji:
- awardable
- user
@@ -589,6 +593,7 @@ lfs_file_locks:
project_badges:
- project
metrics:
+- target_project
- merge_request
- latest_closed_by
- merged_by
diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
index efb271086a0..96c467e78d6 100644
--- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
+++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
@@ -27,25 +27,55 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
end
context 'when file exceeds allowed decompressed size' do
- it 'returns false' do
+ it 'logs error message returns false' do
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_upload_archive_path: filepath,
+ import_upload_archive_size: File.size(filepath),
+ message: 'Decompressed archive size limit reached'
+ )
expect(subject.valid?).to eq(false)
end
end
- context 'when something goes wrong during decompression' do
- before do
- allow(subject.archive_file).to receive(:eof?).and_raise(StandardError)
+ context 'when exception occurs during decompression' do
+ shared_examples 'logs raised exception and terminates validator process group' do
+ let(:std) { double(:std, close: nil, value: nil) }
+ let(:wait_thr) { double }
+
+ before do
+ allow(Process).to receive(:getpgid).and_return(2)
+ allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr])
+ allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
+ allow(wait_thr).to receive(:value).and_raise(exception)
+ end
+
+ it 'logs raised exception and terminates validator process group' do
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_upload_archive_path: filepath,
+ import_upload_archive_size: File.size(filepath),
+ message: error_message
+ )
+ expect(Process).to receive(:kill).with(-1, 2)
+ expect(subject.valid?).to eq(false)
+ end
end
- it 'logs and tracks raised exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(instance_of(StandardError))
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(message: 'Decompressed archive size validation failed.'))
+ context 'when timeout occurs' do
+ let(:error_message) { 'Timeout reached during archive decompression' }
+ let(:exception) { Timeout::Error }
- subject.valid?
+ include_examples 'logs raised exception and terminates validator process group'
end
- it 'returns false' do
- expect(subject.valid?).to eq(false)
+ context 'when exception occurs' do
+ let(:error_message) { 'Error!' }
+ let(:exception) { StandardError.new(error_message) }
+
+ include_examples 'logs raised exception and terminates validator process group'
end
end
end
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
index b311a02833c..6680f4e7a03 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -11,12 +11,12 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(project: project_with_design_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) }
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
- project: project)
+ importable: project)
end
before do
diff --git a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
index 2575d209db5..5501e3dee5a 100644
--- a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoSaver do
let!(:project) { create(:project, :design_repo) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:design_bundler) { described_class.new(project: project, shared: shared) }
+ let(:design_bundler) { described_class.new(exportable: project, shared: shared) }
before do
project.add_maintainer(user)
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index ef7394053b9..65c28a8b8a2 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -12,11 +12,11 @@ RSpec.describe 'forked project import' do
let(:shared) { project.import_export_shared }
let(:forked_from_project) { create(:project, :repository) }
let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
- let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:repo_restorer) do
- Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project)
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, importable: project)
end
let!(:merge_request) do
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 2794acb8980..d2153221e8f 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
group_tree_restorer = described_class.new(user: user, shared: @shared, group: @group)
expect(group_tree_restorer.restore).to be_truthy
+ expect(group_tree_restorer.groups_mapping).not_to be_empty
end
end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 75db3167ebc..20f0f6af6f3 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -69,8 +69,8 @@ RSpec.describe Gitlab::ImportExport::Importer do
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).with(path_to_bundle: repo_path, shared: shared, importable: project).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, importable: 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
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index a6b917457c2..fe43a23e242 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -27,10 +27,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
describe 'bundle a project Git repo' do
- let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::RepoSaver.new(exportable: 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) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) }
after do
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
@@ -62,10 +62,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
describe 'restore a wiki Git repo' do
- let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: 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)) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project)) }
after do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
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) }
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_without_wiki, shared: shared) }
it 'does not creates an empty wiki' do
expect(subject.restore).to be true
diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index 73d51000c67..52001e778d6 100644
--- a/spec/lib/gitlab/import_export/repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do
let!(:project) { create(:project, :repository) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:bundler) { described_class.new(project: project, shared: shared) }
+ let(:bundler) { described_class.new(exportable: project, shared: shared) }
before do
project.add_maintainer(user)
@@ -25,6 +25,14 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do
expect(bundler.save).to be true
end
+ it 'creates the directory for the repository' do
+ allow(bundler).to receive(:bundle_full_path).and_return('/foo/bar/file.tar.gz')
+
+ expect(FileUtils).to receive(:mkdir_p).with('/foo/bar', anything)
+
+ bundler.save # rubocop:disable Rails/SaveBang
+ end
+
context 'when the repo is empty' do
let!(:project) { create(:project) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index a93ee051ccf..e301be47d68 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -220,6 +220,7 @@ MergeRequestDiff:
- commits_count
- files_count
- sorted
+- diff_type
MergeRequestDiffCommit:
- merge_request_diff_id
- relative_order
@@ -231,6 +232,7 @@ MergeRequestDiffCommit:
- committer_name
- committer_email
- message
+- trailers
MergeRequestDiffFile:
- merge_request_diff_id
- relative_order
@@ -255,6 +257,7 @@ MergeRequestContextCommit:
- committer_email
- message
- merge_request_id
+- trailers
MergeRequestContextCommitDiffFile:
- sha
- relative_order
@@ -580,6 +583,7 @@ ProjectFeature:
- requirements_access_level
- analytics_access_level
- operations_access_level
+- security_and_compliance_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index 865c7e57b5a..877474dd862 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -6,7 +6,8 @@ require 'fileutils'
RSpec.describe Gitlab::ImportExport::Saver do
let!(:project) { create(:project, :public, name: 'project') }
let(:base_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:export_path) { "#{base_path}/project_tree_saver_spec/export" }
+ let(:archive_path) { "#{base_path}/archive" }
+ let(:export_path) { "#{archive_path}/export" }
let(:shared) { project.import_export_shared }
subject { described_class.new(exportable: project, shared: shared) }
@@ -35,10 +36,13 @@ RSpec.describe Gitlab::ImportExport::Saver do
.to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
end
- it 'removes tmp files' do
+ it 'removes archive path and keeps base path untouched' do
+ allow(shared).to receive(:archive_path).and_return(archive_path)
+
subject.save
- expect(FileUtils).to have_received(:rm_rf).with(base_path)
- expect(Dir.exist?(base_path)).to eq(false)
+ expect(FileUtils).not_to have_received(:rm_rf).with(base_path)
+ expect(FileUtils).to have_received(:rm_rf).with(archive_path)
+ expect(Dir.exist?(archive_path)).to eq(false)
end
end
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 778d0859bf1..540f90e7804 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do
let_it_be(:project) { create(:project, :wiki_repo) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
+ let(:wiki_bundler) { described_class.new(exportable: project, shared: shared) }
let!(:project_wiki) { ProjectWiki.new(project, user) }
before do
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index 2ca7465e775..e4af3f77d5d 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -53,6 +53,7 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
:del | [%w(foo bar)] | true # Arguments can be a nested array
:del | %w(foo foo) | false
:hset | %w(foo bar) | false # Not a multi-key command
+ :mget | [] | false # This is invalid, but not because it's a cross-slot command
end
with_them do
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index c00b0fdf043..a5c9cde4c37 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -9,12 +9,17 @@ RSpec.describe Gitlab::InstrumentationHelper do
describe '.keys' do
it 'returns all available payload keys' do
expected_keys = [
+ :cpu_s,
:gitaly_calls,
:gitaly_duration_s,
:rugged_calls,
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
+ :elasticsearch_timed_out_count,
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs,
:redis_calls,
:redis_duration_s,
:redis_read_bytes,
@@ -37,7 +42,11 @@ RSpec.describe Gitlab::InstrumentationHelper do
:redis_shared_state_write_bytes,
:db_count,
:db_write_count,
- :db_cached_count
+ :db_cached_count,
+ :external_http_count,
+ :external_http_duration_s,
+ :rack_attack_redis_count,
+ :rack_attack_redis_duration_s
]
expect(described_class.keys).to eq(expected_keys)
@@ -49,10 +58,14 @@ RSpec.describe Gitlab::InstrumentationHelper do
subject { described_class.add_instrumentation_data(payload) }
- it 'adds only DB counts by default' do
+ before do
+ described_class.init_instrumentation_data
+ end
+
+ it 'includes DB counts' do
subject
- expect(payload).to eq(db_count: 0, db_cached_count: 0, db_write_count: 0)
+ expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0)
end
context 'when Gitaly calls are made' do
@@ -110,6 +123,47 @@ RSpec.describe Gitlab::InstrumentationHelper do
expect(payload[:throttle_safelist]).to eq('foobar')
end
end
+
+ it 'logs cpu_s duration' do
+ subject
+
+ expect(payload).to include(:cpu_s)
+ end
+
+ context 'when logging memory allocations' do
+ include MemoryInstrumentationHelper
+
+ before do
+ skip_memory_instrumentation!
+ end
+
+ it 'logs memory usage metrics' do
+ subject
+
+ expect(payload).to include(
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs
+ )
+ end
+
+ context 'when trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ Gitlab::Memory::Instrumentation.ensure_feature_flag!
+ end
+
+ it 'does not log memory usage metrics' do
+ subject
+
+ expect(payload).not_to include(
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs
+ )
+ end
+ end
+ end
end
describe '.queue_duration_for_job' do
diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb
index ce22f36e9fd..01ced407883 100644
--- a/spec/lib/gitlab/kas_spec.rb
+++ b/spec/lib/gitlab/kas_spec.rb
@@ -58,4 +58,48 @@ RSpec.describe Gitlab::Kas do
end
end
end
+
+ describe '.included_in_gitlab_com_rollout?' do
+ let_it_be(:project) { create(:project) }
+
+ context 'not GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'returns true' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy
+ end
+ end
+
+ context 'GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag disabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: false)
+ end
+
+ it 'returns false' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_falsey
+ end
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag enabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: project)
+ end
+
+ it 'returns true' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy
+ end
+
+ it 'returns false for another project' do
+ expect(described_class.included_in_gitlab_com_rollout?(create(:project))).to be_falsey
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
new file mode 100644
index 00000000000..6b53550a3d0
--- /dev/null
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Instrumentation do
+ include MemoryInstrumentationHelper
+
+ before do
+ skip_memory_instrumentation!
+ end
+
+ describe '.available?' do
+ it 'returns true' do
+ expect(described_class).to be_available
+ end
+ end
+
+ describe '.start_thread_memory_allocations' do
+ subject { described_class.start_thread_memory_allocations }
+
+ context 'when feature flag trace_memory_allocations is enabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: true)
+ end
+
+ it 'a hash is returned' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'when feature flag trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature is unavailable' do
+ before do
+ allow(described_class).to receive(:available?) { false }
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '.with_memory_allocations' do
+ let(:ntimes) { 100 }
+
+ subject do
+ described_class.with_memory_allocations do
+ Array.new(1000).map { '0' * 100 }
+ end
+ end
+
+ before do
+ expect(described_class).to receive(:start_thread_memory_allocations).and_call_original
+ expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original
+ end
+
+ context 'when feature flag trace_memory_allocations is enabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: true)
+ end
+
+ it 'a hash is returned' do
+ is_expected.to include(
+ mem_objects: be > 1000,
+ mem_mallocs: be > 1000,
+ mem_bytes: be > 100_000 # 100 items * 100 bytes each
+ )
+ end
+ end
+
+ context 'when feature flag trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature is unavailable' do
+ before do
+ allow(described_class).to receive(:available?) { false }
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
new file mode 100644
index 00000000000..5bcaf8fbc47
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
@@ -0,0 +1,172 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:subscriber) { described_class.new }
+
+ let(:event_1) do
+ double(:event, payload: {
+ method: 'POST', code: "200", duration: 0.321,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true'
+ })
+ end
+
+ let(:event_2) do
+ double(:event, payload: {
+ method: 'GET', code: "301", duration: 0.12,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true'
+ })
+ end
+
+ let(:event_3) do
+ double(:event, payload: {
+ method: 'POST', duration: 5.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: Net::ReadTimeout.new
+ })
+ end
+
+ describe '.detail_store' do
+ context 'when external HTTP detail store is empty' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ end
+
+ it 'returns an empty array' do
+ expect(described_class.detail_store).to eql([])
+ end
+ end
+
+ context 'when the performance bar is not enabled' do
+ it 'returns an empty array' do
+ expect(described_class.detail_store).to eql([])
+ end
+ end
+
+ context 'when external HTTP detail store has some values' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ Gitlab::SafeRequestStore[:external_http_detail_store] = [{
+ method: 'POST', code: "200", duration: 0.321
+ }]
+ end
+
+ it 'returns the external http detailed store' do
+ expect(described_class.detail_store).to eql([{ method: 'POST', code: "200", duration: 0.321 }])
+ end
+ end
+ end
+
+ describe '.payload' do
+ context 'when SafeRequestStore does not have any item from external HTTP' do
+ it 'returns an empty array' do
+ expect(described_class.payload).to eql(external_http_count: 0, external_http_duration_s: 0.0)
+ end
+ end
+
+ context 'when external HTTP recorded some values' do
+ before do
+ Gitlab::SafeRequestStore[:external_http_count] = 7
+ Gitlab::SafeRequestStore[:external_http_duration_s] = 1.2
+ end
+
+ it 'returns the external http detailed store' do
+ expect(described_class.payload).to eql(external_http_count: 7, external_http_duration_s: 1.2)
+ end
+ end
+ end
+
+ describe '#request' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ allow(subscriber).to receive(:current_transaction).and_return(transaction)
+ end
+
+ it 'tracks external HTTP request count' do
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: "200", method: "POST" })
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: "301", method: "GET" })
+
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ end
+
+ it 'tracks external HTTP duration' do
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 0.321)
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 0.12)
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 5.3)
+
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+ end
+
+ it 'tracks external HTTP exceptions' do
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: 'undefined', method: "POST" })
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_exception_total, 1)
+
+ subscriber.request(event_3)
+ end
+
+ it 'stores per-request counters' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_count]).to eq(3)
+ expect(Gitlab::SafeRequestStore[:external_http_duration_s]).to eq(5.741) # 0.321 + 0.12 + 5.3
+ end
+
+ it 'stores a portion of events into the detail store' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store].length).to eq(3)
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][0]).to include(
+ method: 'POST', code: "200", duration: 0.321,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true', exception_object: nil,
+ backtrace: be_a(Array)
+ )
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][1]).to include(
+ method: 'GET', code: "301", duration: 0.12,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true', exception_object: nil,
+ backtrace: be_a(Array)
+ )
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][2]).to include(
+ method: 'POST', duration: 5.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: be_a(Net::ReadTimeout),
+ backtrace: be_a(Array)
+ )
+ end
+
+ context 'when the performance bar is not enabled' do
+ before do
+ Gitlab::SafeRequestStore.delete(:peek_enabled)
+ end
+
+ it 'does not capture detail store' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store]).to be(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
new file mode 100644
index 00000000000..2d595632772
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do
+ let(:subscriber) { described_class.new }
+
+ describe '.payload' do
+ context 'when the request store is empty' do
+ it 'returns empty data' do
+ expect(described_class.payload).to eql(
+ rack_attack_redis_count: 0,
+ rack_attack_redis_duration_s: 0.0
+ )
+ end
+ end
+
+ context 'when the request store already has data' do
+ before do
+ Gitlab::SafeRequestStore[:rack_attack_instrumentation] = {
+ rack_attack_redis_count: 10,
+ rack_attack_redis_duration_s: 9.0
+ }
+ end
+
+ it 'returns the accumulated data' do
+ expect(described_class.payload).to eql(
+ rack_attack_redis_count: 10,
+ rack_attack_redis_duration_s: 9.0
+ )
+ end
+ end
+ end
+
+ describe '#redis' do
+ it 'accumulates per-request RackAttack cache usage' do
+ freeze_time do
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 1.second, '1', { operation: 'fetch' }
+ )
+ )
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 2.seconds, '1', { operation: 'write' }
+ )
+ )
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 3.seconds, '1', { operation: 'read' }
+ )
+ )
+ end
+
+ expect(Gitlab::SafeRequestStore[:rack_attack_instrumentation]).to eql(
+ rack_attack_redis_count: 3,
+ rack_attack_redis_duration_s: 6.0
+ )
+ end
+ end
+
+ shared_examples 'log into auth logger' do
+ context 'when matched throttle does not require user information' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_unauthenticated'
+ }
+ )
+ )
+ end
+
+ it 'logs request information' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_unauthenticated'
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+
+ context 'when matched throttle requires user information' do
+ context 'when user not found' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_authenticated_api',
+ 'rack.attack.match_discriminator' => 'not_exist_user_id'
+ }
+ )
+ )
+ end
+
+ it 'logs request information and user id' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_authenticated_api',
+ user_id: 'not_exist_user_id'
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+
+ context 'when user found' do
+ let(:user) { create(:user) }
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_authenticated_api',
+ 'rack.attack.match_discriminator' => user.id
+ }
+ )
+ )
+ end
+
+ it 'logs request information and user meta' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_authenticated_api',
+ user_id: user.id,
+ 'meta.user' => user.username
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+ end
+ end
+
+ describe '#throttle' do
+ let(:match_type) { :throttle }
+ let(:event_name) { 'throttle.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#blocklist' do
+ let(:match_type) { :blocklist }
+ let(:event_name) { 'blocklist.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#track' do
+ let(:match_type) { :track }
+ let(:event_name) { 'track.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#safelist' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ 'safelist.rack_attack', Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ env: {
+ 'rack.attack.matched' => 'throttle_unauthenticated'
+ }
+ )
+ )
+ end
+
+ it 'adds the matched name to safe request store' do
+ subscriber.safelist(event)
+ expect(Gitlab::SafeRequestStore[:instrumentation_throttle_safelist]).to eql('throttle_unauthenticated')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/middleware/request_context_spec.rb b/spec/lib/gitlab/middleware/request_context_spec.rb
index 431f4453e37..6d5b581feaa 100644
--- a/spec/lib/gitlab/middleware/request_context_spec.rb
+++ b/spec/lib/gitlab/middleware/request_context_spec.rb
@@ -18,9 +18,11 @@ RSpec.describe Gitlab::Middleware::RequestContext do
end
describe '#call' do
- context 'setting the client ip' do
- subject { Gitlab::RequestContext.instance.client_ip }
+ let(:instance) { Gitlab::RequestContext.instance }
+
+ subject { described_class.new(app).call(env) }
+ context 'setting the client ip' do
context 'with X-Forwarded-For headers' do
let(:load_balancer_ip) { '1.2.3.4' }
let(:headers) do
@@ -33,13 +35,7 @@ RSpec.describe Gitlab::Middleware::RequestContext do
let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
it 'returns the load balancer IP' do
- endpoint = proc do
- [200, {}, ["Hello"]]
- end
-
- described_class.new(endpoint).call(env)
-
- expect(subject).to eq(load_balancer_ip)
+ expect { subject }.to change { instance.client_ip }.from(nil).to(load_balancer_ip)
end
end
@@ -47,32 +43,19 @@ RSpec.describe Gitlab::Middleware::RequestContext do
let(:ip) { '192.168.1.11' }
before do
- allow_next_instance_of(Rack::Request) do |instance|
- allow(instance).to receive(:ip).and_return(ip)
+ allow_next_instance_of(Rack::Request) do |request|
+ allow(request).to receive(:ip).and_return(ip)
end
- described_class.new(app).call(env)
end
- it { is_expected.to eq(ip) }
- end
+ it 'sets the `client_ip`' do
+ expect { subject }.to change { instance.client_ip }.from(nil).to(ip)
+ end
- context 'before RequestContext middleware run' do
- it { is_expected.to be_nil }
+ it 'sets the `request_start_time`' do
+ expect { subject }.to change { instance.request_start_time }.from(nil).to(Float)
+ end
end
end
end
-
- context 'setting the thread cpu time' do
- it 'sets the `start_thread_cpu_time`' do
- expect { described_class.new(app).call(env) }
- .to change { Gitlab::RequestContext.instance.start_thread_cpu_time }.from(nil).to(Float)
- end
- end
-
- context 'setting the request start time' do
- it 'sets the `request_start_time`' do
- expect { described_class.new(app).call(env) }
- .to change { Gitlab::RequestContext.instance.request_start_time }.from(nil).to(Float)
- end
- end
end
diff --git a/spec/lib/gitlab/pages_transfer_spec.rb b/spec/lib/gitlab/pages_transfer_spec.rb
index 4f0ee76b244..552a2e0701c 100644
--- a/spec/lib/gitlab/pages_transfer_spec.rb
+++ b/spec/lib/gitlab/pages_transfer_spec.rb
@@ -8,13 +8,24 @@ RSpec.describe Gitlab::PagesTransfer do
context 'when receiving an allowed method' do
it 'schedules a PagesTransferWorker', :aggregate_failures do
- described_class::Async::METHODS.each do |meth|
+ described_class::METHODS.each do |meth|
expect(PagesTransferWorker)
.to receive(:perform_async).with(meth, %w[foo bar])
async.public_send(meth, 'foo', 'bar')
end
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ described_class::METHODS.each do |meth|
+ expect(PagesTransferWorker)
+ .not_to receive(:perform_async)
+
+ async.public_send(meth, 'foo', 'bar')
+ end
+ end
end
context 'when receiving a private method' do
@@ -59,6 +70,15 @@ RSpec.describe Gitlab::PagesTransfer do
expect(subject.public_send(meth, *args)).to be(false)
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ subject.public_send(meth, *args)
+
+ expect(File.exist?(config_path_before)).to be(true)
+ expect(File.exist?(config_path_after)).to be(false)
+ end
end
describe '#move_namespace' do
diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb
index 8feab57a8f3..5b01bb99fc8 100644
--- a/spec/lib/gitlab/patch/prependable_spec.rb
+++ b/spec/lib/gitlab/patch/prependable_spec.rb
@@ -231,4 +231,22 @@ RSpec.describe Gitlab::Patch::Prependable do
.to raise_error(described_class::MultiplePrependedBlocks)
end
end
+
+ describe 'the extra hack for override verification' do
+ context 'when ENV["STATIC_VERIFICATION"] is not defined' do
+ it 'does not extend ClassMethods onto the defining module' do
+ expect(ee).not_to respond_to(:class_name)
+ end
+ end
+
+ context 'when ENV["STATIC_VERIFICATION"] is defined' do
+ before do
+ stub_env('STATIC_VERIFICATION', 'true')
+ end
+
+ it 'does extend ClassMethods onto the defining module' do
+ expect(ee).to respond_to(:class_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb
index c34c6f7b31f..ad11eca56d1 100644
--- a/spec/lib/gitlab/performance_bar/stats_spec.rb
+++ b/spec/lib/gitlab/performance_bar/stats_spec.rb
@@ -22,10 +22,12 @@ RSpec.describe Gitlab::PerformanceBar::Stats do
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 })
+ method_path: 'lib/gitlab/pagination/offset_pagination.rb:add_pagination_headers',
+ count: 1, 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
+ .with({ duration_ms: 1.634, filename: 'lib/api/helpers.rb',
+ method_path: 'lib/api/helpers.rb:find_project',
+ count: 2, request_id: 'foo', type: :sql })
subject
end
diff --git a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
new file mode 100644
index 00000000000..2cb31b00f39
--- /dev/null
+++ b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RackAttack::InstrumentedCacheStore do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:store) { ::ActiveSupport::Cache::NullStore.new }
+
+ subject { described_class.new(upstream_store: store)}
+
+ where(:operation, :params, :test_proc) do
+ :fetch | [:key] | ->(s) { s.fetch(:key) }
+ :read | [:key] | ->(s) { s.read(:key) }
+ :read_multi | [:key_1, :key_2, :key_3] | ->(s) { s.read_multi(:key_1, :key_2, :key_3) }
+ :write_multi | [{ key_1: 1, key_2: 2, key_3: 3 }] | ->(s) { s.write_multi(key_1: 1, key_2: 2, key_3: 3) }
+ :fetch_multi | [:key_1, :key_2, :key_3] | ->(s) { s.fetch_multi(:key_1, :key_2, :key_3) {} }
+ :write | [:key, :value, { option_1: 1 }] | ->(s) { s.write(:key, :value, option_1: 1) }
+ :delete | [:key] | ->(s) { s.delete(:key) }
+ :exist? | [:key, { option_1: 1 }] | ->(s) { s.exist?(:key, option_1: 1) }
+ :delete_matched | [/^key$/, { option_1: 1 }] | ->(s) { s.delete_matched(/^key$/, option_1: 1 ) }
+ :increment | [:key, 1] | ->(s) { s.increment(:key, 1) }
+ :decrement | [:key, 1] | ->(s) { s.decrement(:key, 1) }
+ :cleanup | [] | ->(s) { s.cleanup }
+ :clear | [] | ->(s) { s.clear }
+ end
+
+ with_them do
+ it 'publishes a notification' do
+ event = nil
+
+ begin
+ subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ test_proc.call(subject)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ expect(event).not_to be_nil
+ expect(event.name).to eq("redis.rack_attack")
+ expect(event.duration).to be_a(Float).and(be > 0.0)
+ expect(event.payload[:operation]).to eql(operation)
+ end
+
+ it 'publishes a notification even if the cache store returns an error' do
+ allow(store).to receive(operation).and_raise('Something went wrong')
+
+ event = nil
+ exception = nil
+
+ begin
+ subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ begin
+ test_proc.call(subject)
+ rescue => e
+ exception = e
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ expect(event).not_to be_nil
+ expect(event.name).to eq("redis.rack_attack")
+ expect(event.duration).to be_a(Float).and(be > 0.0)
+ expect(event.payload[:operation]).to eql(operation)
+
+ expect(exception).not_to be_nil
+ expect(exception.message).to eql('Something went wrong')
+ end
+
+ it 'delegates to the upstream store' do
+ allow(store).to receive(operation).and_call_original
+
+ if params.empty?
+ expect(store).to receive(operation).with(no_args)
+ else
+ expect(store).to receive(operation).with(*params)
+ end
+
+ test_proc.call(subject)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb
index 5748e1e49e5..788d2eac61f 100644
--- a/spec/lib/gitlab/rack_attack_spec.rb
+++ b/spec/lib/gitlab/rack_attack_spec.rb
@@ -6,6 +6,7 @@ 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(:fake_cache) { instance_double("Rack::Attack::Cache") }
let(:throttles) do
{
@@ -27,6 +28,8 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do
allow(fake_rack_attack).to receive(:track)
allow(fake_rack_attack).to receive(:safelist)
allow(fake_rack_attack).to receive(:blocklist)
+ allow(fake_rack_attack).to receive(:cache).and_return(fake_cache)
+ allow(fake_cache).to receive(:store=)
end
it 'extends the request class' do
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 4c57665b41f..625dcf11546 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
- expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:branch_names)
- expect(redis_set_cache).to receive(:expire).with(:rendered_readme, :branch_names)
- expect(redis_hash_cache).to receive(:delete).with(:rendered_readme, :branch_names)
+ expect(redis_set_cache).to receive(:expire).with(:branch_names)
+ expect(redis_hash_cache).to receive(:delete).with(:branch_names)
- repository.expire_method_caches(%i(rendered_readme branch_names))
+ repository.expire_method_caches(%i(branch_names))
end
it 'does not expire caches for non-existent methods' do
diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb
index 20996dd44b8..a7b777cf4f2 100644
--- a/spec/lib/gitlab/request_forgery_protection_spec.rb
+++ b/spec/lib/gitlab/request_forgery_protection_spec.rb
@@ -52,6 +52,11 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do
end
describe '.verified?' do
+ it 'does not modify the env' do
+ env['REQUEST_METHOD'] = "GET"
+ expect { described_class.verified?(env) }.not_to change { env }
+ end
+
context 'when the request method is GET' do
before do
env['REQUEST_METHOD'] = 'GET'
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 8ed7cc141cd..1ec14092c63 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -44,10 +44,11 @@ RSpec.describe Gitlab::Runtime do
context "puma" do
let(:puma_type) { double('::Puma') }
+ let(:max_workers) { 2 }
before do
stub_const('::Puma', puma_type)
- allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
+ allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2, workers: max_workers)
stub_env('ACTION_CABLE_IN_APP', 'false')
end
@@ -70,6 +71,20 @@ RSpec.describe Gitlab::Runtime do
it_behaves_like "valid runtime", :puma, 11
end
+
+ describe ".puma_in_clustered_mode?" do
+ context 'when Puma is set up with workers > 0' do
+ let(:max_workers) { 4 }
+
+ specify { expect(described_class.puma_in_clustered_mode?).to be true }
+ end
+
+ context 'when Puma is set up with workers = 0' do
+ let(:max_workers) { 0 }
+
+ specify { expect(described_class.puma_in_clustered_mode?).to be false }
+ end
+ end
end
context "unicorn" do
diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb
index dd2f23a7e47..234b683ba1f 100644
--- a/spec/lib/gitlab/search/query_spec.rb
+++ b/spec/lib/gitlab/search/query_spec.rb
@@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do
expect(subject.filters).to all(include(negated: true))
end
end
+
+ context 'with filter value in quotes' do
+ let(:query) { '"foo bar" name:"my test script.txt"' }
+
+ it 'does not break the filter value in quotes' do
+ expect(subject.term).to eq('"foo bar"')
+ expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST SCRIPT.TXT")
+ end
+ end
+
+ context 'with extra white spaces between the query words' do
+ let(:query) { ' foo = bar name:"my test.txt"' }
+
+ it 'removes the extra whitespace between tokens' do
+ expect(subject.term).to eq('foo = bar')
+ expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST.TXT")
+ end
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index c437b6bcceb..a1b18172a31 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::SearchResults do
include ProjectForksHelper
include SearchHelpers
+ using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'foo') }
@@ -41,8 +42,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#formatted_count' do
- using RSpec::Parameterized::TableSyntax
-
where(:scope, :count_method, :expected) do
'projects' | :limited_projects_count | max_limited_count
'issues' | :limited_issues_count | max_limited_count
@@ -61,8 +60,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#highlight_map' do
- using RSpec::Parameterized::TableSyntax
-
where(:scope, :expected) do
'projects' | {}
'issues' | {}
@@ -80,8 +77,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#formatted_limited_count' do
- using RSpec::Parameterized::TableSyntax
-
where(:count, :expected) do
23 | '23'
99 | '99'
@@ -183,12 +178,18 @@ RSpec.describe Gitlab::SearchResults do
end
context 'ordering' do
- let(:query) { 'sorted' }
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
- include_examples 'search results sorted'
+ let!(:old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-old-1', title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-new-1', title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-very-old-1', title: 'updated very old', updated_at: 1.year.ago) }
+
+ include_examples 'search results sorted' do
+ let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
+ let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
+ end
end
end
@@ -219,12 +220,18 @@ RSpec.describe Gitlab::SearchResults do
end
context 'ordering' do
- let(:query) { 'sorted' }
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
- include_examples 'search results sorted'
+ let!(:old_updated) { create(:issue, project: project, title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
+
+ include_examples 'search results sorted' do
+ let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
+ let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
deleted file mode 100644
index 94dcf6f9b9a..00000000000
--- a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SidekiqLogging::ExceptionHandler do
- describe '#call' do
- let(:job) do
- {
- "class" => "TestWorker",
- "args" => [1234, 'hello'],
- "retry" => false,
- "queue" => "cronjob:test_queue",
- "queue_namespace" => "cronjob",
- "jid" => "da883554ee4fe414012f5f42",
- "correlation_id" => 'cid'
- }
- end
-
- let(:exception_message) { 'An error was thrown' }
- let(:backtrace) { caller }
- let(:exception) { RuntimeError.new(exception_message) }
- let(:logger) { double }
-
- before do
- allow(Sidekiq).to receive(:logger).and_return(logger)
- allow(exception).to receive(:backtrace).and_return(backtrace)
- end
-
- subject { described_class.new.call(exception, { context: 'Test', job: job }) }
-
- it 'logs job data into root tree' do
- expected_data = job.merge(
- error_class: 'RuntimeError',
- error_message: exception_message,
- context: 'Test',
- error_backtrace: Rails.backtrace_cleaner.clean(backtrace)
- )
-
- expect(logger).to receive(:warn).with(expected_data)
-
- subject
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index b99a5352717..3e8e117ec71 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -3,7 +3,13 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
- describe '#call' do
+ before do
+ # We disable a memory instrumentation feature
+ # as this requires a special patched Ruby
+ allow(Gitlab::Memory::Instrumentation).to receive(:available?) { false }
+ end
+
+ describe '#call', :request_store do
let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') }
let(:created_at) { timestamp - 1.second }
let(:scheduling_latency_s) { 1.0 }
@@ -21,14 +27,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
"correlation_id" => 'cid',
"error_message" => "wrong number of arguments (2 for 3)",
"error_class" => "ArgumentError",
- "error_backtrace" => [],
- "db_count" => 1,
- "db_write_count" => 0,
- "db_cached_count" => 0
+ "error_backtrace" => []
}
end
let(:logger) { double }
+ let(:clock_realtime_start) { 0.222222299 }
+ let(:clock_realtime_end) { 1.333333799 }
let(:clock_thread_cputime_start) { 0.222222299 }
let(:clock_thread_cputime_end) { 1.333333799 }
let(:start_payload) do
@@ -38,7 +43,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'pid' => Process.pid,
'created_at' => created_at.to_f,
'enqueued_at' => created_at.to_f,
- 'scheduling_latency_s' => scheduling_latency_s
+ 'scheduling_latency_s' => scheduling_latency_s,
+ 'job_size_bytes' => be > 0
)
end
@@ -49,7 +55,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
'cpu_s' => 1.111112,
- 'db_duration_s' => 0.0
+ 'db_duration_s' => 0.0,
+ 'db_cached_count' => 0,
+ 'db_count' => 0,
+ 'db_write_count' => 0
)
end
@@ -58,7 +67,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
'job_status' => 'fail',
'error_class' => 'ArgumentError',
- 'error_message' => 'some exception'
+ 'error_message' => 'Something went wrong',
+ 'error_backtrace' => be_a(Array).and(be_present)
)
end
@@ -67,7 +77,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
allow(subject).to receive(:current_time).and_return(timestamp.to_f)
- allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID).and_return(clock_thread_cputime_start, clock_thread_cputime_end)
+ allow(Process).to receive(:clock_gettime).with(Process::CLOCK_REALTIME, :float_second)
+ .and_return(clock_realtime_start, clock_realtime_end)
+ allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
+ .and_return(clock_thread_cputime_start, clock_thread_cputime_end)
end
subject { described_class.new }
@@ -84,25 +97,97 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
+ end
+ end
+
+ it 'logs real job wrapped by active job worker' do
+ wrapped_job = job.merge(
+ "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
+ "wrapped" => "TestWorker"
+ )
+
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload).ordered
+ expect(logger).to receive(:info).with(end_payload).ordered
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(wrapped_job, 'test_queue') { }
end
end
it 'logs an exception in job' do
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload)
- expect(logger).to receive(:warn).with(hash_including(exception_payload))
+ expect(logger).to receive(:warn).with(include(exception_payload))
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
expect do
- subject.call(job, 'test_queue') do
- raise ArgumentError, 'some exception'
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
end
end.to raise_error(ArgumentError)
end
end
+ it 'logs the root cause of an Sidekiq::JobRetry::Skip exception in the job' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(include(exception_payload))
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
+ rescue
+ raise Sidekiq::JobRetry::Skip
+ end
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+
+ it 'logs the root cause of an Sidekiq::JobRetry::Handled exception in the job' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(include(exception_payload))
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
+ rescue
+ raise Sidekiq::JobRetry::Handled
+ end
+ end.to raise_error(Sidekiq::JobRetry::Handled)
+ end
+ end
+
+ it 'keeps Sidekiq::JobRetry::Handled exception if the cause does not exist' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(
+ include(
+ 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
+ 'job_status' => 'fail',
+ 'error_class' => 'Sidekiq::JobRetry::Skip',
+ 'error_message' => 'Sidekiq::JobRetry::Skip'
+ )
+ )
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise Sidekiq::JobRetry::Skip
+ end
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+
it 'does not modify the job' do
Timecop.freeze(timestamp) do
job_copy = job.deep_dup
@@ -111,11 +196,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
allow(subject).to receive(:log_job_start).and_call_original
allow(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
expect(job).to eq(job_copy)
end
end
end
+
+ it 'does not modify the wrapped job' do
+ Timecop.freeze(timestamp) do
+ wrapped_job = job.merge(
+ "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
+ "wrapped" => "TestWorker"
+ )
+ job_copy = wrapped_job.deep_dup
+
+ allow(logger).to receive(:info)
+ allow(subject).to receive(:log_job_start).and_call_original
+ allow(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(wrapped_job, 'test_queue') do
+ expect(wrapped_job).to eq(job_copy)
+ end
+ end
+ end
end
context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do
@@ -130,7 +233,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
end
end
@@ -143,7 +246,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job.except("created_at", "enqueued_at"), 'test_queue') { }
+ call_subject(job.except("created_at", "enqueued_at"), 'test_queue') { }
end
end
end
@@ -159,7 +262,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
end
end
end
@@ -177,7 +280,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
let(:expected_end_payload) do
- end_payload.merge(timing_data)
+ end_payload.merge(timing_data.stringify_keys)
end
it 'logs with Gitaly and Rugged timing data' do
@@ -185,7 +288,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
job.merge!(timing_data)
end
end
@@ -207,7 +310,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
let(:expected_end_payload_with_db) do
expected_end_payload.merge(
'db_duration_s' => a_value >= 0.1,
- 'db_count' => 1,
+ 'db_count' => a_value >= 1,
'db_cached_count' => 0,
'db_write_count' => 0
)
@@ -217,7 +320,9 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
- subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') }
+ call_subject(job, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
end
it 'prevents database time from leaking to the next job' do
@@ -226,8 +331,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') }
- subject.call(job, 'test_queue') { }
+ call_subject(job.dup, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
+
+ Gitlab::SafeRequestStore.clear!
+
+ call_subject(job.dup, 'test_queue') { }
end
end
@@ -243,7 +353,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1"] = 15
job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2"] = 16
job['key that will be ignored because it does not start with extra.'] = 17
@@ -251,13 +361,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
end
+
+ def call_subject(job, queue)
+ # This structured logger strongly depends on execution of `InstrumentationLogger`
+ subject.call(job, queue) do
+ ::Gitlab::SidekiqMiddleware::InstrumentationLogger.new.call('worker', job, queue) do
+ yield
+ end
+ end
+ end
end
describe '#add_time_keys!' do
- let(:time) { { duration: 0.1231234, cputime: 1.2342345 } }
+ let(:time) { { duration: 0.1231234 } }
let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } }
let(:current_utc_time) { Time.now.utc }
- let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration_s' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time.to_f } }
+
+ let(:payload_with_time_keys) do
+ { 'class' => 'my-class',
+ 'message' => 'my-message',
+ 'job_status' => 'my-job-status',
+ 'duration_s' => 0.123123,
+ 'completed_at' => current_utc_time.to_f }
+ end
subject { described_class.new }
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
index f7010b2001a..e2b36125b4e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -60,6 +60,27 @@ RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do
end
end
+ context "when a worker is wrapped into ActiveJob" do
+ before do
+ stub_const('TestWrappedWorker', Class.new)
+ TestWrappedWorker.class_eval do
+ include Sidekiq::Worker
+ end
+ end
+
+ it_behaves_like "a metrics client middleware" do
+ let(:job) do
+ {
+ "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
+ "wrapped" => TestWrappedWorker
+ }
+ end
+
+ let(:worker) { TestWrappedWorker.new }
+ let(:labels) { default_labels.merge(urgency: "") }
+ end
+ end
+
context "when workers are attributed" do
def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
klass = Class.new do
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 44bfaf4cc3c..e58e41d3e4f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -198,6 +198,28 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
it_behaves_like "a metrics middleware"
end
+ context "when a worker is wrapped into ActiveJob" do
+ before do
+ stub_const('TestWrappedWorker', Class.new)
+ TestWrappedWorker.class_eval do
+ include Sidekiq::Worker
+ end
+ end
+
+ let(:job) do
+ {
+ "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
+ "wrapped" => TestWrappedWorker
+ }
+ end
+
+ let(:worker) { TestWrappedWorker.new }
+ let(:worker_class) { TestWrappedWorker }
+ let(:labels) { default_labels.merge(urgency: "") }
+
+ it_behaves_like "a metrics middleware"
+ end
+
context "when workers are attributed" do
def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
Class.new do
diff --git a/spec/lib/gitlab/suggestions/commit_message_spec.rb b/spec/lib/gitlab/suggestions/commit_message_spec.rb
index 1411f64f8b7..965960f0c3e 100644
--- a/spec/lib/gitlab/suggestions/commit_message_spec.rb
+++ b/spec/lib/gitlab/suggestions/commit_message_spec.rb
@@ -72,6 +72,17 @@ RSpec.describe Gitlab::Suggestions::CommitMessage do
end
end
+ context 'when a custom commit message is specified' do
+ let(:message) { "i'm a project message. a user's custom message takes precedence over me :(" }
+ let(:custom_message) { "hello there! i'm a cool custom commit message." }
+
+ it 'shows the custom commit message' do
+ expect(Gitlab::Suggestions::CommitMessage
+ .new(user, suggestion_set, custom_message)
+ .message).to eq(custom_message)
+ end
+ end
+
context 'is specified and includes all placeholders' do
let(:message) do
'*** %{branch_name} %{files_count} %{file_paths} %{project_name} %{project_path} %{user_full_name} %{username} %{suggestions_count} ***'
diff --git a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
index e2751d194d3..38ec28c2b9a 100644
--- a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
+++ b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
@@ -15,9 +15,19 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
FileUtils.rm_rf(base_dir)
end
- subject(:finder) { described_class.new(base_dir, '', { 'General' => '', 'Bar' => 'Bar' }, excluded_patterns: excluded_patterns) }
+ subject(:finder) do
+ described_class.new(base_dir, '',
+ { 'General' => '', 'Bar' => 'Bar' },
+ include_categories_for_file,
+ excluded_patterns: excluded_patterns)
+ end
let(:excluded_patterns) { [] }
+ let(:include_categories_for_file) do
+ {
+ "SAST" => { "Security" => "Security" }
+ }
+ end
describe '.find' do
context 'with a non-prefixed General template' do
@@ -60,6 +70,7 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
context 'with a prefixed template' do
before do
create_template!('Bar/test-template')
+ create_template!('Security/SAST')
end
it 'finds the template with a prefix' do
@@ -76,6 +87,16 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
expect { finder.find('../foo') }.to raise_error(/Invalid path/)
end
+ context 'with include_categories_for_file being present' do
+ it 'finds the template with a prefix' do
+ expect(finder.find('SAST')).to be_present
+ end
+
+ it 'does not find any template which is missing in include_categories_for_file' do
+ expect(finder.find('DAST')).to be_nil
+ end
+ end
+
context 'while listed as an exclusion' do
let(:excluded_patterns) { [%r{^Bar/test-template$}] }
diff --git a/spec/lib/gitlab/terraform/state_migration_helper_spec.rb b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb
new file mode 100644
index 00000000000..36c9c060e98
--- /dev/null
+++ b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Terraform::StateMigrationHelper do
+ before do
+ stub_terraform_state_object_storage
+ end
+
+ describe '.migrate_to_remote_storage' do
+ let!(:local_version) { create(:terraform_state_version, file_store: Terraform::StateUploader::Store::LOCAL) }
+
+ subject { described_class.migrate_to_remote_storage }
+
+ it 'migrates remote files to remote storage' do
+ subject
+
+ expect(local_version.reload.file_store).to eq(Terraform::StateUploader::Store::REMOTE)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index acf7aeb303a..7a0a4f0cc46 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -9,47 +9,48 @@ RSpec.describe Gitlab::Tracking::StandardContext do
let(:snowplow_context) { subject.to_context }
describe '#to_context' do
- context 'with no arguments' do
- it 'creates a Snowplow context with no data' do
- snowplow_context.to_json[:data].each do |_, v|
- expect(v).to be_nil
+ context 'environment' do
+ shared_examples 'contains environment' do |expected_environment|
+ it 'contains environment' do
+ expect(snowplow_context.to_json.dig(:data, :environment)).to eq(expected_environment)
end
end
- end
- context 'with extra data' do
- subject { described_class.new(foo: 'bar') }
-
- it 'creates a Snowplow context with the given data' do
- expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar')
+ context 'development or test' do
+ include_examples 'contains environment', 'development'
end
- end
- context 'with namespace' do
- subject { described_class.new(namespace: namespace) }
+ context 'staging' do
+ before do
+ allow(Gitlab).to receive(:staging?).and_return(true)
+ end
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
+ include_examples 'contains environment', 'staging'
end
- end
- context 'with project' do
- subject { described_class.new(project: project) }
+ context 'production' do
+ before do
+ allow(Gitlab).to receive(:com_and_canary?).and_return(true)
+ end
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(project.namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
+ include_examples 'contains environment', 'production'
end
end
- context 'with project and namespace' do
- subject { described_class.new(namespace: namespace, project: project) }
+ it 'contains source' do
+ expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
+ end
+
+ context 'with extra data' do
+ subject { described_class.new(foo: 'bar') }
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
+ it 'creates a Snowplow context with the given data' do
+ expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar')
end
end
+
+ it 'does not contain any ids' do
+ expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
+ end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 8f1fd49f4c5..80740c8112e 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -42,36 +42,31 @@ RSpec.describe Gitlab::Tracking do
end
shared_examples 'delegates to destination' do |klass|
- context 'with standard context' do
- it "delegates to #{klass} destination" do
- expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
- expect(category).to eq('category')
- expect(action).to eq('action')
- expect(args[:label]).to eq('label')
- expect(args[:property]).to eq('property')
- expect(args[:value]).to eq(1.5)
- expect(args[:context].length).to eq(1)
- expect(args[:context].first.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
- expect(args[:context].first.to_json[:data]).to include(foo: 'bar')
- end
+ it "delegates to #{klass} destination" do
+ other_context = double(:context)
- described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
- standard_context: Gitlab::Tracking::StandardContext.new(foo: 'bar'))
- end
- end
+ project = double(:project)
+ user = double(:user)
+ namespace = double(:namespace)
- context 'without standard context' do
- it "delegates to #{klass} destination" do
- expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
- expect(category).to eq('category')
- expect(action).to eq('action')
- expect(args[:label]).to eq('label')
- expect(args[:property]).to eq('property')
- expect(args[:value]).to eq(1.5)
- end
+ expect(Gitlab::Tracking::StandardContext)
+ .to receive(:new)
+ .with(project: project, user: user, namespace: namespace)
+ .and_call_original
- described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5)
+ expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
+ expect(category).to eq('category')
+ expect(action).to eq('action')
+ expect(args[:label]).to eq('label')
+ expect(args[:property]).to eq('property')
+ expect(args[:value]).to eq(1.5)
+ expect(args[:context].length).to eq(2)
+ expect(args[:context].first).to eq(other_context)
+ expect(args[:context].last.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
end
+
+ described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
+ context: [other_context], project: project, user: user, namespace: namespace)
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 686382dc262..fa01d4e48df 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -302,36 +302,36 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'does not block urls from private networks' do
local_ips.each do |ip|
stub_domain_resolv(fake_domain, ip) do
- expect(described_class).not_to be_blocked_url("http://#{fake_domain}", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://#{fake_domain}", **url_blocker_attributes)
end
- expect(described_class).not_to be_blocked_url("http://#{ip}", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://#{ip}", **url_blocker_attributes)
end
end
it 'allows localhost endpoints' do
- expect(described_class).not_to be_blocked_url('http://0.0.0.0', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://localhost', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://127.0.0.1', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://0.0.0.0', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://localhost', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.1', **url_blocker_attributes)
end
it 'allows loopback endpoints' do
- expect(described_class).not_to be_blocked_url('http://127.0.0.2', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.2', **url_blocker_attributes)
end
it 'allows IPv4 link-local endpoints' do
- expect(described_class).not_to be_blocked_url('http://169.254.169.254', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://169.254.168.100', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://169.254.169.254', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://169.254.168.100', **url_blocker_attributes)
end
it 'allows IPv6 link-local endpoints' do
- expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', **url_blocker_attributes)
end
end
@@ -416,11 +416,11 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
attrs = url_blocker_attributes.merge(dns_rebind_protection: false)
stub_domain_resolv('example.com', '192.168.1.2') do
- expect(described_class).not_to be_blocked_url(url, attrs)
+ expect(described_class).not_to be_blocked_url(url, **attrs)
end
stub_domain_resolv('example.com', '192.168.1.3') do
- expect(described_class).to be_blocked_url(url, attrs)
+ expect(described_class).to be_blocked_url(url, **attrs)
end
end
end
@@ -442,18 +442,18 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
stub_domain_resolv(domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
stub_domain_resolv(subdomain1, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{subdomain1}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
# subdomain2 is not part of the allowlist so it should be blocked
stub_domain_resolv(subdomain2, '192.168.1.1') do
expect(described_class).to be_blocked_url("http://#{subdomain2}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
end
@@ -463,12 +463,12 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
stub_domain_resolv(unicode_domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{unicode_domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
stub_domain_resolv(idna_encoded_domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{idna_encoded_domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
end
@@ -525,7 +525,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'allows domain with port when resolved ip has port allowed' do
stub_domain_resolv("www.resolve-domain.com", '127.0.0.1') do
- expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", **url_blocker_attributes)
end
end
end
diff --git a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
index d9e44e9b85c..4c4248b143e 100644
--- a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
+++ b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
@@ -37,19 +37,19 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do
let(:allowlist) { ['example.io:3000'] }
it 'returns true if domain and ports present in allowlist' do
- parsed_allowlist = [['example.io', { port: 3000 }]]
+ parsed_allowlist = [['example.io', 3000]]
not_allowed = [
'example.io',
- ['example.io', { port: 3001 }]
+ ['example.io', 3001]
]
aggregate_failures do
- parsed_allowlist.each do |domain_and_port|
- expect(described_class).to be_domain_allowed(*domain_and_port)
+ parsed_allowlist.each do |domain, port|
+ expect(described_class).to be_domain_allowed(domain, port: port)
end
- not_allowed.each do |domain_and_port|
- expect(described_class).not_to be_domain_allowed(*domain_and_port)
+ not_allowed.each do |domain, port|
+ expect(described_class).not_to be_domain_allowed(domain, port: port)
end
end
end
@@ -139,23 +139,23 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do
it 'returns true if ip and ports present in allowlist' do
parsed_allowlist = [
- ['127.0.0.9', { port: 3000 }],
- ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 443 }]
+ ['127.0.0.9', 3000],
+ ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 443]
]
not_allowed = [
'127.0.0.9',
- ['127.0.0.9', { port: 3001 }],
+ ['127.0.0.9', 3001],
'[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
- ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 3001 }]
+ ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 3001]
]
aggregate_failures do
- parsed_allowlist.each do |ip_and_port|
- expect(described_class).to be_ip_allowed(*ip_and_port)
+ parsed_allowlist.each do |ip, port|
+ expect(described_class).to be_ip_allowed(ip, port: port)
end
- not_allowed.each do |ip_and_port|
- expect(described_class).not_to be_ip_allowed(*ip_and_port)
+ not_allowed.each do |ip, port|
+ expect(described_class).not_to be_ip_allowed(ip, port: port)
end
end
end
diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb
new file mode 100644
index 00000000000..0677aa2d9d7
--- /dev/null
+++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Docs::Renderer do
+ describe 'contents' do
+ let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH }
+ let(:items) { Gitlab::Usage::MetricDefinition.definitions }
+
+ it 'generates dictionary for given items' do
+ generated_dictionary = described_class.new(items).contents
+ generated_dictionary_keys = RDoc::Markdown
+ .parse(generated_dictionary)
+ .table_of_contents
+ .select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') }
+ .map(&:text)
+ .map { |text| text.sub('<code>', '').sub('</code>', '') }
+
+ expect(generated_dictionary_keys).to match_array(items.keys)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
new file mode 100644
index 00000000000..7002c76a7cf
--- /dev/null
+++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Docs::ValueFormatter do
+ describe '.format' do
+ using RSpec::Parameterized::TableSyntax
+ where(:key, :value, :expected_value) do
+ :product_group | 'growth::product intelligence' | '`growth::product intelligence`'
+ :data_source | 'redis' | 'Redis'
+ :data_source | 'ruby' | 'Ruby'
+ :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)'
+ :tier | %w(gold premium) | 'gold, premium'
+ :distribution | %w(ce ee) | 'ce, ee'
+ :key_path | 'key.path' | '**`key.path`**'
+ :milestone | '13.4' | '13.4'
+ :status | 'data_available' | 'data_available'
+ end
+
+ with_them do
+ subject { described_class.format(key, value) }
+
+ it { is_expected.to eq(expected_value) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index e101f837324..8b592838f5d 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -5,18 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::MetricDefinition do
let(:attributes) do
{
- name: 'uuid',
description: 'GitLab instance unique identifier',
value_type: 'string',
product_category: 'collection',
- stage: 'growth',
+ product_stage: 'growth',
status: 'data_available',
default_generation: 'generation_1',
- full_path: {
- generation_1: 'uuid',
- generation_2: 'license.uuid'
- },
- group: 'group::product analytics',
+ key_path: 'uuid',
+ product_group: 'group::product analytics',
time_frame: 'none',
data_source: 'database',
distribution: %w(ee ce),
@@ -44,13 +40,12 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
using RSpec::Parameterized::TableSyntax
where(:attribute, :value) do
- :name | nil
:description | nil
:value_type | nil
:value_type | 'test'
:status | nil
- :default_generation | nil
- :group | nil
+ :key_path | nil
+ :product_group | nil
:time_frame | nil
:time_frame | '29d'
:data_source | 'other'
@@ -70,6 +65,20 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
described_class.new(path, attributes).validate!
end
+
+ context 'with skip_validation' do
+ it 'raise exception if skip_validation: false' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+
+ described_class.new(path, attributes.merge( { skip_validation: false } )).validate!
+ end
+
+ it 'does not raise exception if has skip_validation: true' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ described_class.new(path, attributes.merge( { skip_validation: true } )).validate!
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index 40671d980d6..d4a789419a4 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -4,15 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metric do
describe '#definition' do
- it 'returns generation_1 metric definiton' do
- expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
+ it 'returns key_path metric definiton' do
+ expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
end
end
describe '#unflatten_default_path' do
using RSpec::Parameterized::TableSyntax
- where(:default_generation_path, :value, :expected_hash) do
+ where(:key_path, :value, :expected_hash) do
'uuid' | nil | { uuid: nil }
'uuid' | '1111' | { uuid: '1111' }
'counts.issues' | nil | { counts: { issues: nil } }
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Usage::Metric do
end
with_them do
- subject { described_class.new(default_generation_path: default_generation_path, value: value).unflatten_default_path }
+ subject { described_class.new(key_path: key_path, value: value).unflatten_key_path }
it { is_expected.to eq(expected_hash) }
end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
new file mode 100644
index 00000000000..5469ded18f9
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redis_shared_state do
+ let(:entity1) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' }
+ let(:entity2) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' }
+ let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' }
+ let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' }
+ let(:end_date) { Date.current }
+ let(:sources) { Gitlab::Usage::Metrics::Aggregates::Sources }
+
+ let_it_be(:recorded_at) { Time.current.to_i }
+
+ context 'aggregated_metrics_data' do
+ shared_examples 'aggregated_metrics_data' do
+ context 'no aggregated metric is defined' do
+ it 'returns empty hash' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics).and_return([])
+ end
+
+ expect(aggregated_metrics_data).to eq({})
+ end
+ end
+
+ context 'there are aggregated metrics defined' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ end
+ end
+
+ context 'with disabled database_sourced_aggregated_metrics feature flag' do
+ before do
+ stub_feature_flags(database_sourced_aggregated_metrics: false)
+ end
+
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'skips database sourced metrics', :aggregate_failures do
+ results = {
+ 'gmau_1' => 5
+ }
+
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5)
+ expect(sources::PostgresHll).not_to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3]))
+ expect(aggregated_metrics_data).to eq(results)
+ end
+ end
+
+ context 'with AND operator' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "AND" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "AND" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'returns the number of unique events recorded for every metric in aggregate', :aggregate_failures do
+ results = {
+ 'gmau_1' => 2,
+ 'gmau_2' => 1
+ }
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ # gmau_1 data is as follow
+ # |A| => 4
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(4)
+ # |B| => 6
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event5')).and_return(6)
+ # |A + B| => 8
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 2 sets
+ # |A & B| = (|A| + |B|) - |A + B| => (4 + 6) - 8 => 2
+
+ # gmau_2 data is as follow:
+ # |A| => 2
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event1')).and_return(2)
+ # |B| => 3
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event2')).and_return(3)
+ # |C| => 5
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(5)
+
+ # |A + B| => 4 therefore |A & B| = (|A| + |B|) - |A + B| => 2 + 3 - 4 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2])).and_return(4)
+ # |A + C| => 6 therefore |A & C| = (|A| + |C|) - |A + C| => 2 + 5 - 6 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event3])).and_return(6)
+ # |B + C| => 7 therefore |B & C| = (|B| + |C|) - |B + C| => 3 + 5 - 7 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event2 event3])).and_return(7)
+ # |A + B + C| => 8
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 3 sets
+ # |A & B & C| = (|A & B| + |A & C| + |B & C|) - (|A| + |B| + |C|) + |A + B + C|
+ # (1 + 1 + 1) - (2 + 3 + 5) + 8 => 1
+
+ expect(aggregated_metrics_data).to eq(results)
+ end
+ end
+
+ context 'with OR operator' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'returns the number of unique events occurred for any metric in aggregate', :aggregate_failures do
+ results = {
+ 'gmau_1' => 5,
+ 'gmau_2' => 3
+ }
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5)
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(3)
+ expect(aggregated_metrics_data).to eq(results)
+ end
+ end
+
+ context 'hidden behind feature flag' do
+ let(:enabled_feature_flag) { 'test_ff_enabled' }
+ let(:disabled_feature_flag) { 'test_ff_disabled' }
+ let(:aggregated_metrics) do
+ [
+ # represents stable aggregated metrics that has been fully released
+ { name: 'gmau_without_ff', source: 'redis', events: %w[event3_slot event5_slot], operator: "OR" },
+ # represents new aggregated metric that is under performance testing on gitlab.com
+ { name: 'gmau_enabled', source: 'redis', events: %w[event4], operator: "OR", feature_flag: enabled_feature_flag },
+ # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
+ { name: 'gmau_disabled', source: 'redis', events: %w[event4], operator: "OR", feature_flag: disabled_feature_flag }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'does not calculate data for aggregates with ff turned off' do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+ stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false)
+ allow(sources::RedisHll).to receive(:calculate_metrics_union).and_return(6)
+
+ expect(aggregated_metrics_data).to eq('gmau_without_ff' => 6, 'gmau_enabled' => 6)
+ end
+ end
+ end
+
+ context 'error handling' do
+ context 'development and test environment' do
+ it 'raises error when unknown aggregation operator is used' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "SUM" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationOperator
+ end
+
+ it 'raises error when unknown aggregation source is used' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationSource
+ end
+
+ it 're raises Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
+ error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "OR" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error error
+ end
+ end
+
+ context 'production' do
+ before do
+ stub_rails_env('production')
+ end
+
+ it 'rescues unknown aggregation operator error' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "SUM" }])
+ end
+
+ expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ end
+
+ it 'rescues unknown aggregation source error' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }])
+ end
+
+ expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ end
+
+ it 'rescues Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
+ error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "OR" }])
+ end
+
+ expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ end
+ end
+ end
+ end
+
+ it 'allows for YAML aliases in aggregated metrics configs' do
+ expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true)
+
+ described_class.new(recorded_at)
+ end
+
+ describe '.aggregated_metrics_weekly_data' do
+ subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data }
+
+ let(:start_date) { 7.days.ago.to_date }
+
+ it_behaves_like 'aggregated_metrics_data'
+ end
+
+ describe '.aggregated_metrics_monthly_data' do
+ subject(:aggregated_metrics_data) { described_class.new(recorded_at).monthly_data }
+
+ let(:start_date) { 4.weeks.ago.to_date }
+
+ it_behaves_like 'aggregated_metrics_data'
+
+ context 'metrics union calls' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_3', source: 'redis', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'caches intermediate operations', :aggregate_failures do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ end
+
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ aggregated_metrics[0][:events].each do |event|
+ expect(sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: event))
+ .once
+ .and_return(0)
+ end
+
+ 2.upto(4) do |subset_size|
+ aggregated_metrics[0][:events].combination(subset_size).each do |events|
+ expect(sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: events))
+ .once
+ .and_return(0)
+ end
+ end
+
+ aggregated_metrics_data
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
new file mode 100644
index 00000000000..7b8be8e8bc6
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_gitlab_redis_shared_state do
+ let_it_be(:start_date) { 7.days.ago }
+ let_it_be(:end_date) { Date.current }
+ let_it_be(:recorded_at) { Time.current }
+ let_it_be(:time_period) { { created_at: (start_date..end_date) } }
+ let(:metric_1) { 'metric_1' }
+ let(:metric_2) { 'metric_2' }
+ let(:metric_names) { [metric_1, metric_2] }
+
+ describe '.calculate_events_union' do
+ subject(:calculate_metrics_union) do
+ described_class.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+
+ before do
+ [
+ {
+ metric_name: metric_1,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1)
+ },
+ {
+ metric_name: metric_2,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: ::Gitlab::Database::PostgresHll::Buckets.new(10 => 1, 56 => 1)
+ }
+ ].each do |params|
+ described_class.save_aggregated_metrics(**params)
+ end
+ end
+
+ it 'returns the number of unique events in the union of all metrics' do
+ expect(calculate_metrics_union.round(2)).to eq(3.12)
+ end
+
+ context 'when there is no aggregated data saved' do
+ let(:metric_names) { [metric_1, 'i do not have any records'] }
+
+ it 'raises error when union data is missing' do
+ expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ end
+ end
+
+ context 'when there is only one metric defined as aggregated' do
+ let(:metric_names) { [metric_1] }
+
+ it 'returns the number of unique events for that metric' do
+ expect(calculate_metrics_union.round(2)).to eq(2.08)
+ end
+ end
+ end
+
+ describe '.save_aggregated_metrics' do
+ subject(:save_aggregated_metrics) do
+ described_class.save_aggregated_metrics(metric_name: metric_1,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: data)
+ end
+
+ context 'with compatible data argument' do
+ let(:data) { ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1) }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_weekly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+
+ context 'with monthly key' do
+ let_it_be(:start_date) { 4.weeks.ago }
+ let_it_be(:time_period) { { created_at: (start_date..end_date) } }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_monthly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ context 'with all_time key' do
+ let_it_be(:time_period) { nil }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_all_time-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ context 'error handling' do
+ before do
+ allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(::Redis::CommandError)
+ end
+
+ it 'rescues and reraise ::Redis::CommandError for development and test environments' do
+ expect { save_aggregated_metrics }.to raise_error ::Redis::CommandError
+ end
+
+ context 'for environment different than development' do
+ before do
+ stub_rails_env('production')
+ end
+
+ it 'rescues ::Redis::CommandError' do
+ expect { save_aggregated_metrics }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ context 'with incompatible data argument' do
+ let(:data) { 1 }
+
+ context 'for environment different than development' do
+ before do
+ stub_rails_env('production')
+ end
+
+ it 'does not persist data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ it 'raises error for development environment' do
+ expect { save_aggregated_metrics }.to raise_error /Unsupported data type/
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
new file mode 100644
index 00000000000..af2de5ea343
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
+ describe '.calculate_events_union' do
+ let(:event_names) { %w[event_a event_b] }
+ let(:start_date) { 7.days.ago }
+ let(:end_date) { Date.current }
+
+ subject(:calculate_metrics_union) do
+ described_class.calculate_metrics_union(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: nil)
+ end
+
+ it 'calls Gitlab::UsageDataCounters::HLLRedisCounter.calculate_events_union' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union)
+ .with(event_names: event_names, start_date: start_date, end_date: end_date)
+ .and_return(5)
+
+ calculate_metrics_union
+ end
+
+ it 'prevents from using fallback value as valid union result' do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_return(-1)
+
+ expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ 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 c0deb2aa00c..58f974fbe12 100644
--- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
@@ -13,18 +13,32 @@ RSpec.describe 'aggregated metrics' do
end
end
+ RSpec::Matchers.define :has_known_source do
+ match do |aggregate|
+ Gitlab::Usage::Metrics::Aggregates::SOURCES.include?(aggregate[:source])
+ end
+
+ failure_message do |aggregate|
+ "Aggregate with name: `#{aggregate[:name]}` uses not allowed source `#{aggregate[:source]}`"
+ end
+ end
+
let_it_be(:known_events) do
Gitlab::UsageDataCounters::HLLRedisCounter.known_events
end
- Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics.tap do |aggregated_metrics|
+ Gitlab::Usage::Metrics::Aggregates::Aggregate.new(Time.current).send(:aggregated_metrics).tap do |aggregated_metrics|
it 'all events has unique name' do
event_names = aggregated_metrics&.map { |event| event[:name] }
expect(event_names).to eq(event_names&.uniq)
end
- aggregated_metrics&.each do |aggregate|
+ it 'all aggregated metrics has known source' do
+ expect(aggregated_metrics).to all has_known_source
+ end
+
+ aggregated_metrics&.select { |agg| agg[:source] == Gitlab::Usage::Metrics::Aggregates::REDIS_SOURCE }&.each do |aggregate|
context "for #{aggregate[:name]} aggregate of #{aggregate[:events].join(' ')}" do
let_it_be(:events_records) { known_events.select { |event| aggregate[:events].include?(event[:name]) } }
@@ -37,7 +51,7 @@ RSpec.describe 'aggregated metrics' do
end
it "uses allowed aggregation operators" do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator]
+ expect(Gitlab::Usage::Metrics::Aggregates::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator]
end
it "uses events from the same Redis slot" do
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index ba7bfe47bc9..b1d5d106082 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -3,28 +3,88 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
- let(:project_id) { 1 }
-
describe '.track_unique_project_event' do
- described_class::TEMPLATE_TO_EVENT.keys.each do |template|
- context "when given template #{template}" do
- it_behaves_like 'tracking unique hll events', :usage_data_track_ci_templates_unique_projects do
- subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) }
+ using RSpec::Parameterized::TableSyntax
+
+ where(:template, :config_source, :expected_event) do
+ # Implicit Auto DevOps usage
+ 'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops'
+ 'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build'
+ 'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy'
+ 'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast'
+ 'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection'
+ # Explicit include:template usage
+ '5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app'
+ 'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops'
+ 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2'
+ 'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs'
+ 'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build'
+ 'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy'
+ 'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest'
+ 'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast'
+ 'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection'
+ 'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest'
+ end
+
+ with_them do
+ it_behaves_like 'tracking unique hll events' do
+ subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) }
+
+ let(:project_id) { 1 }
+ let(:target_id) { expected_event }
+ let(:expected_type) { instance_of(Integer) }
+ end
+ end
+
+ context 'known_events coverage tests' do
+ let(:project_id) { 1 }
+ let(:config_source) { :repository_source }
- let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" }
- let(:expected_type) { instance_of(Integer) }
+ # These tests help guard against missing "explicit" events in known_events/ci_templates.yml
+ context 'explicit include:template events' do
+ described_class::TEMPLATE_TO_EVENT.keys.each do |template|
+ it "does not raise error for #{template}" do
+ expect do
+ described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
+ end.not_to raise_error
+ end
+ end
+ end
+
+ # This test is to help guard against missing "implicit" events in known_events/ci_templates.yml
+ it 'does not raise error for any template in an implicit Auto DevOps pipeline' do
+ project = create(:project, :auto_devops)
+ pipeline = double(project: project)
+ command = double
+ result = Gitlab::Ci::YamlProcessor.new(
+ Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
+ project: project,
+ user: double,
+ sha: double
+ ).execute
+
+ config_source = :auto_devops_source
+
+ result.included_templates.each do |template|
+ expect do
+ described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source)
+ end.not_to raise_error
end
end
end
- it 'does not track templates outside of TEMPLATE_TO_EVENT' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(
- receive(:track_event)
- )
+ context 'templates outside of TEMPLATE_TO_EVENT' do
+ let(:project_id) { 1 }
+ let(:config_source) { :repository_source }
+
Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template|
next if described_class::TEMPLATE_TO_EVENT.key?(template)
- described_class.track_unique_project_event(project_id: 1, template: template)
+ it "does not track #{template}" do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(receive(:track_event))
+
+ described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index b8eddc0ca7f..b4894ec049f 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
@@ -27,6 +27,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'deploy_token_packages',
'user_packages',
'compliance',
+ 'ecosystem',
'analytics',
'ide_edit',
'search',
@@ -39,12 +40,16 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'snippets',
'code_review',
'terraform',
- 'ci_templates'
+ 'ci_templates',
+ 'quickactions',
+ 'pipeline_authoring'
)
end
end
describe 'known_events' do
+ let(:feature) { 'test_hll_redis_counter_ff_check' }
+
let(:weekly_event) { 'g_analytics_contribution' }
let(:daily_event) { 'g_analytics_search' }
let(:analytics_slot_event) { 'g_analytics_contribution' }
@@ -64,7 +69,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly" },
+ { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly", feature_flag: feature },
{ name: daily_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "daily" },
{ name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" },
{ name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" },
@@ -75,6 +80,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
allow(described_class).to receive(:known_events).and_return(known_events)
end
@@ -85,6 +92,32 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe '.track_event' do
+ context 'with feature flag set' do
+ it 'tracks the event when feature enabled' do
+ stub_feature_flags(feature => true)
+
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+
+ it 'does not track the event with feature flag disabled' do
+ stub_feature_flags(feature => false)
+
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+ end
+
+ context 'with no feature flag set' do
+ it 'tracks the event' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(daily_event, values: 1)
+ end
+ end
+
context 'when usage_ping is disabled' do
it 'does not track the event' do
stub_application_setting(usage_ping_enabled: false)
@@ -425,182 +458,59 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'aggregated_metrics_data' do
+ describe '.calculate_events_union' do
+ let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
let(:known_events) do
[
{ name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
{ name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" },
{ name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" },
- { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" },
+ { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "daily" },
{ name: 'event4', category: 'category2', aggregation: "weekly" }
].map(&:with_indifferent_access)
end
before do
allow(described_class).to receive(:known_events).and_return(known_events)
- end
-
- shared_examples 'aggregated_metrics_data' do
- context 'no aggregated metrics is defined' do
- it 'returns empty hash' do
- allow(described_class).to receive(:aggregated_metrics).and_return([])
-
- expect(aggregated_metrics_data).to eq({})
- end
- end
-
- context 'there are aggregated metrics defined' do
- before do
- allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
-
- context 'with AND operator' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" },
- { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" },
- { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" },
- { name: 'gmau_4', events: %w[event4], operator: "AND" }
- ].map(&:with_indifferent_access)
- end
-
- it 'returns the number of unique events for all known events' do
- results = {
- 'gmau_1' => 3,
- 'gmau_2' => 2,
- 'gmau_3' => 1,
- 'gmau_4' => 3
- }
-
- expect(aggregated_metrics_data).to eq(results)
- end
- end
-
- context 'with OR operator' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" },
- { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" },
- { name: 'gmau_3', events: %w[event4], operator: "OR" }
- ].map(&:with_indifferent_access)
- end
- it 'returns the number of unique events for all known events' do
- results = {
- 'gmau_1' => 2,
- 'gmau_2' => 3,
- 'gmau_3' => 3
- }
-
- expect(aggregated_metrics_data).to eq(results)
- end
- end
-
- context 'hidden behind feature flag' do
- let(:enabled_feature_flag) { 'test_ff_enabled' }
- let(:disabled_feature_flag) { 'test_ff_disabled' }
- let(:aggregated_metrics) do
- [
- # represents stable aggregated metrics that has been fully released
- { name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" },
- # represents new aggregated metric that is under performance testing on gitlab.com
- { name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag },
- # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
- { name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag }
- ].map(&:with_indifferent_access)
- end
-
- it 'returns the number of unique events for all known events' do
- skip_feature_flags_yaml_validation
- stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false)
+ described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
+ described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
+ described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
+ described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
+ described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
+ described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
+ described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
+ described_class.track_event('event3_slot', values: entity2, time: 3.days.ago)
+ described_class.track_event('event5_slot', values: entity2, time: 3.days.ago)
+
+ # events out of time scope
+ described_class.track_event('event2_slot', values: entity4, time: 8.days.ago)
- expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3)
- end
- end
- end
+ # events in different slots
+ described_class.track_event('event4', values: entity1, time: 2.days.ago)
+ described_class.track_event('event4', values: entity2, time: 2.days.ago)
end
- describe '.aggregated_metrics_weekly_data' do
- subject(:aggregated_metrics_data) { described_class.aggregated_metrics_weekly_data }
-
- before do
- described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event5_slot', values: entity2, time: 3.days.ago)
-
- # events out of time scope
- described_class.track_event('event2_slot', values: entity3, time: 8.days.ago)
-
- # events in different slots
- described_class.track_event('event4', values: entity1, time: 2.days.ago)
- described_class.track_event('event4', values: entity2, time: 2.days.ago)
- described_class.track_event('event4', values: entity4, time: 2.days.ago)
- end
-
- it_behaves_like 'aggregated_metrics_data'
+ it 'calculates union of given events', :aggregate_failure do
+ expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event4]))).to eq 2
+ expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event2_slot event3_slot]))).to eq 3
end
- describe '.aggregated_metrics_monthly_data' do
- subject(:aggregated_metrics_data) { described_class.aggregated_metrics_monthly_data }
-
- it_behaves_like 'aggregated_metrics_data' do
- before do
- described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity2, time: 10.days.ago)
- described_class.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1))
-
- # events out of time scope
- described_class.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1))
-
- # events in different slots
- described_class.track_event('event4', values: entity1, time: 2.days.ago)
- described_class.track_event('event4', values: entity2, time: 2.days.ago)
- described_class.track_event('event4', values: entity4, time: 2.days.ago)
- end
- end
-
- context 'Redis calls' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }
- ].map(&:with_indifferent_access)
- end
-
- let(:known_events) do
- [
- { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
- { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" },
- { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" },
- { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" }
- ].map(&:with_indifferent_access)
- end
-
- it 'caches intermediate operations' do
- allow(described_class).to receive(:known_events).and_return(known_events)
- allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ it 'validates and raise exception if events has mismatched slot or aggregation', :aggregate_failure do
+ expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event4])) }.to raise_error described_class::SlotMismatch
+ expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event5_slot event3_slot])) }.to raise_error described_class::AggregationMismatch
+ end
+ end
- 4.downto(1) do |subset_size|
- known_events.combination(subset_size).each do |events|
- keys = described_class.send(:weekly_redis_keys, events: events, start_date: 4.weeks.ago.to_date, end_date: Date.current)
- expect(Gitlab::Redis::HLL).to receive(:count).with(keys: keys).once.and_return(0)
- end
- end
+ describe '.weekly_time_range' do
+ it 'return hash with weekly time range boundaries' do
+ expect(described_class.weekly_time_range).to eq(start_date: 7.days.ago.to_date, end_date: Date.current)
+ end
+ end
- subject
- end
- end
+ describe '.monthly_time_range' do
+ it 'return hash with monthly time range boundaries' do
+ expect(described_class.monthly_time_range).to eq(start_date: 4.weeks.ago.to_date, end_date: Date.current)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
index c7b208cfb31..a604de4a61f 100644
--- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
@@ -73,6 +73,54 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
end
end
+ describe '.track_approve_mr_action' do
+ subject { described_class.track_approve_mr_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVE_ACTION }
+ end
+ end
+
+ describe '.track_unapprove_mr_action' do
+ subject { described_class.track_unapprove_mr_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNAPPROVE_ACTION }
+ end
+ end
+
+ describe '.track_resolve_thread_action' do
+ subject { described_class.track_resolve_thread_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_RESOLVE_THREAD_ACTION }
+ end
+ end
+
+ describe '.track_unresolve_thread_action' do
+ subject { described_class.track_unresolve_thread_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNRESOLVE_THREAD_ACTION }
+ end
+ end
+
+ describe '.track_title_edit_action' do
+ subject { described_class.track_title_edit_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_EDIT_MR_TITLE_ACTION }
+ end
+ end
+
+ describe '.track_description_edit_action' do
+ subject { described_class.track_description_edit_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_EDIT_MR_DESC_ACTION }
+ end
+ end
+
describe '.track_create_comment_action' do
subject { described_class.track_create_comment_action(note: note) }
@@ -148,4 +196,92 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION }
end
end
+
+ describe '.track_add_suggestion_action' do
+ subject { described_class.track_add_suggestion_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_ADD_SUGGESTION_ACTION }
+ end
+ end
+
+ describe '.track_apply_suggestion_action' do
+ subject { described_class.track_apply_suggestion_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPLY_SUGGESTION_ACTION }
+ end
+ end
+
+ describe '.track_users_assigned_to_mr' do
+ subject { described_class.track_users_assigned_to_mr(users: [user]) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_ASSIGNED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_marked_as_draft_action' do
+ subject { described_class.track_marked_as_draft_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_MARKED_AS_DRAFT_ACTION }
+ end
+ end
+
+ describe '.track_unmarked_as_draft_action' do
+ subject { described_class.track_unmarked_as_draft_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNMARKED_AS_DRAFT_ACTION }
+ end
+ end
+
+ describe '.track_task_item_status_changed' do
+ subject { described_class.track_task_item_status_changed(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_TASK_ITEM_STATUS_CHANGED_ACTION }
+ end
+ end
+
+ describe '.track_users_review_requested' do
+ subject { described_class.track_users_review_requested(users: [user]) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_REVIEW_REQUESTED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_added_action' do
+ subject { described_class.track_approval_rule_added_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_ADDED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_edited_action' do
+ subject { described_class.track_approval_rule_edited_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_EDITED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_deleted_action' do
+ subject { described_class.track_approval_rule_deleted_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_DELETED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_mr_create_from_issue' do
+ subject { described_class.track_mr_create_from_issue(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_CREATE_FROM_ISSUE_ACTION }
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..d4c423f57fe
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter, :clean_gitlab_redis_shared_state do
+ let(:user) { build(:user, id: 1) }
+ let(:note) { build(:note, author: user) }
+ let(:args) { nil }
+
+ shared_examples_for 'a tracked quick action unique event' do
+ specify do
+ expect { 3.times { subject } }
+ .to change {
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: action,
+ start_date: 2.weeks.ago,
+ end_date: 2.weeks.from_now
+ )
+ }
+ .by(1)
+ end
+ end
+
+ subject { described_class.track_unique_action(quickaction_name, args: args, user: user) }
+
+ describe '.track_unique_action' do
+ let(:quickaction_name) { 'approve' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_approve' }
+ end
+ end
+
+ context 'tracking assigns' do
+ let(:quickaction_name) { 'assign' }
+
+ context 'single assignee' do
+ let(:args) { '@one' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_single' }
+ end
+ end
+
+ context 'multiple assignees' do
+ let(:args) { '@one @two' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_multiple' }
+ end
+ end
+
+ context 'assigning "me"' do
+ let(:args) { 'me' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_self' }
+ end
+ end
+
+ context 'assigning a reviewer' do
+ let(:quickaction_name) { 'assign_reviewer' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_reviewer' }
+ end
+ end
+
+ context 'assigning a reviewer with request review alias' do
+ let(:quickaction_name) { 'request_review' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_reviewer' }
+ end
+ end
+ end
+
+ context 'tracking copy_metadata' do
+ let(:quickaction_name) { 'copy_metadata' }
+
+ context 'for issues' do
+ let(:args) { '#123' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_copy_metadata_issue' }
+ end
+ end
+
+ context 'for merge requests' do
+ let(:args) { '!123' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_copy_metadata_merge_request' }
+ end
+ end
+ end
+
+ context 'tracking spend' do
+ let(:quickaction_name) { 'spend' }
+
+ context 'adding time' do
+ let(:args) { '1d' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_spend_add' }
+ end
+ end
+
+ context 'removing time' do
+ let(:args) { '-1d' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_spend_subtract' }
+ end
+ end
+ end
+
+ context 'tracking unassign' do
+ let(:quickaction_name) { 'unassign' }
+
+ context 'unassigning everyone' do
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unassign_all' }
+ end
+ end
+
+ context 'unassigning specific users' do
+ let(:args) { '@hello' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unassign_specific' }
+ end
+ end
+ end
+
+ context 'tracking unlabel' do
+ context 'called as unlabel' do
+ let(:quickaction_name) { 'unlabel' }
+
+ context 'removing all labels' do
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_all' }
+ end
+ end
+
+ context 'removing specific labels' do
+ let(:args) { '~wow' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_specific' }
+ end
+ end
+ end
+
+ context 'called as remove_label' do
+ let(:quickaction_name) { 'remove_label' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_all' }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..7593d51fe76
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a tracked vs code unique action' do |event|
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ def count_unique(date_from:, date_to:)
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
+ end
+
+ it 'tracks when the user agent is from vs code' do
+ aggregate_failures do
+ user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
+
+ expect(track_action(user: user1, **user_agent)).to be_truthy
+ expect(track_action(user: user1, **user_agent)).to be_truthy
+ expect(track_action(user: user2, **user_agent)).to be_truthy
+
+ expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(2)
+ end
+ end
+
+ it 'does not track when the user agent is not from vs code' do
+ aggregate_failures do
+ user_agent = { user_agent: 'normal_user_agent' }
+
+ expect(track_action(user: user1, **user_agent)).to be_falsey
+ expect(track_action(user: user1, **user_agent)).to be_falsey
+ expect(track_action(user: user2, **user_agent)).to be_falsey
+
+ expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(0)
+ end
+ end
+
+ it 'does not track if user agent is not present' do
+ expect(track_action(user: nil, user_agent: nil)).to be_nil
+ end
+
+ it 'does not track if user is not present' do
+ user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
+
+ expect(track_action(user: nil, **user_agent)).to be_nil
+ end
+end
+
+RSpec.describe Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state do
+ let(:user1) { build(:user, id: 1) }
+ let(:user2) { build(:user, id: 2) }
+ let(:time) { Time.current }
+
+ context 'when tracking a vs code api request' do
+ it_behaves_like 'a tracked vs code unique action' do
+ let(:action) { described_class::VS_CODE_API_REQUEST_ACTION }
+
+ def track_action(params)
+ described_class.track_api_request_when_trackable(**params)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index fd02521622c..602f6640d72 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -228,11 +228,32 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
)
end
- it 'includes imports usage data' do
+ it 'includes import gmau usage data' do
for_defined_days_back do
user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+
+ create(:project, import_type: :github, creator_id: user.id)
+ create(:jira_import_state, :finished, project: create(:project, creator_id: user.id))
+ create(:issue_csv_import, user: user)
+ create(:group_import_state, group: group, user: user)
create(:bulk_import, user: user)
+ end
+
+ expect(described_class.usage_activity_by_stage_manage({})).to include(
+ unique_users_all_imports: 10
+ )
+
+ expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
+ unique_users_all_imports: 5
+ )
+ end
+
+ it 'includes imports usage data' do
+ for_defined_days_back do
+ user = create(:user)
%w(gitlab_project gitlab github bitbucket bitbucket_server gitea git manifest fogbugz phabricator).each do |type|
create(:project, import_type: type, creator_id: user.id)
@@ -242,72 +263,113 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
create(:jira_import_state, :finished, project: jira_project)
create(:issue_csv_import, user: user)
+
+ group = create(:group)
+ group.add_owner(user)
+ create(:group_import_state, group: group, user: user)
+
+ bulk_import = create(:bulk_import, user: user)
+ create(:bulk_import_entity, :group_entity, bulk_import: bulk_import)
+ create(:bulk_import_entity, :project_entity, bulk_import: bulk_import)
end
expect(described_class.usage_activity_by_stage_manage({})).to include(
{
bulk_imports: {
- gitlab: 2
+ gitlab_v1: 2,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE
},
- projects_imported: {
- total: 2,
- gitlab_project: 2,
- gitlab: 2,
- github: 2,
+ project_imports: {
bitbucket: 2,
bitbucket_server: 2,
- gitea: 2,
git: 2,
+ gitea: 2,
+ github: 2,
+ gitlab: 2,
+ gitlab_migration: 2,
+ gitlab_project: 2,
manifest: 2
},
- issues_imported: {
+ issue_imports: {
jira: 2,
fogbugz: 2,
phabricator: 2,
csv: 2
- }
+ },
+ group_imports: {
+ group_import: 2,
+ gitlab_migration: 2
+ },
+ projects_imported: {
+ total: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
+ github: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitea: Gitlab::UsageData::DEPRECATED_VALUE,
+ git: Gitlab::UsageData::DEPRECATED_VALUE,
+ manifest: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ issues_imported: {
+ jira: Gitlab::UsageData::DEPRECATED_VALUE,
+ fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
+ phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
+ csv: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
}
)
expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
{
bulk_imports: {
- gitlab: 1
+ gitlab_v1: 1,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE
},
- projects_imported: {
- total: 1,
- gitlab_project: 1,
- gitlab: 1,
- github: 1,
+ project_imports: {
bitbucket: 1,
bitbucket_server: 1,
- gitea: 1,
git: 1,
+ gitea: 1,
+ github: 1,
+ gitlab: 1,
+ gitlab_migration: 1,
+ gitlab_project: 1,
manifest: 1
},
- issues_imported: {
+ issue_imports: {
jira: 1,
fogbugz: 1,
phabricator: 1,
csv: 1
- }
+ },
+ group_imports: {
+ group_import: 1,
+ gitlab_migration: 1
+ },
+ projects_imported: {
+ total: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
+ github: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitea: Gitlab::UsageData::DEPRECATED_VALUE,
+ git: Gitlab::UsageData::DEPRECATED_VALUE,
+ manifest: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ issues_imported: {
+ jira: Gitlab::UsageData::DEPRECATED_VALUE,
+ fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
+ phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
+ csv: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
+
}
)
end
- it 'includes group imports usage data' do
- for_defined_days_back do
- user = create(:user)
- group = create(:group)
- group.add_owner(user)
- create(:group_import_state, group: group, user: user)
- end
-
- expect(described_class.usage_activity_by_stage_manage({}))
- .to include(groups_imported: 2)
- expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period))
- .to include(groups_imported: 1)
- end
-
def omniauth_providers
[
OpenStruct.new(name: 'google_oauth2'),
@@ -1262,7 +1324,9 @@ 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 ci_secrets_management incident_management_alerts snippets terraform] }
+ let(:ineligible_total_categories) do
+ %w[source_code ci_secrets_management incident_management_alerts snippets terraform pipeline_authoring]
+ end
it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)
@@ -1286,8 +1350,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
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_weekly_data).and_return(global_search_gmau: 123)
+ it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#weekly_data', :aggregate_failures do
+ expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance|
+ expect(instance).to receive(:weekly_data).and_return(global_search_gmau: 123)
+ end
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
@@ -1295,8 +1361,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
describe '.aggregated_metrics_monthly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
- 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)
+ it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#monthly_data', :aggregate_failures do
+ expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance|
+ expect(instance).to receive(:monthly_data).and_return(global_search_gmau: 123)
+ end
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb
index 93d91f7ed90..acc5bd47c8c 100644
--- a/spec/lib/gitlab/utils/markdown_spec.rb
+++ b/spec/lib/gitlab/utils/markdown_spec.rb
@@ -53,33 +53,23 @@ RSpec.describe Gitlab::Utils::Markdown do
end
context 'when string has a product suffix' do
- let(:string) { 'My Header (ULTIMATE)' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
-
- context 'with only modifier' do
- let(:string) { 'My Header (STARTER ONLY)' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
- end
-
- context 'with "*" around a product suffix' do
- let(:string) { 'My Header **(STARTER)**' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
- end
-
- context 'with "*" around a product suffix and only modifier' do
- let(:string) { 'My Header **(STARTER ONLY)**' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
+ %w[CORE STARTER PREMIUM ULTIMATE FREE BRONZE SILVER GOLD].each do |tier|
+ ['', ' ONLY', ' SELF', ' SASS'].each do |modifier|
+ context "#{tier}#{modifier}" do
+ let(:string) { "My Header (#{tier}#{modifier})" }
+
+ it 'ignores a product suffix' do
+ is_expected.to eq 'my-header'
+ end
+
+ context 'with "*" around a product suffix' do
+ let(:string) { "My Header **(#{tier}#{modifier})**" }
+
+ it 'ignores a product suffix' do
+ is_expected.to eq 'my-header'
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index 7ba7392df0f..a5e53c1dfc1 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -2,6 +2,9 @@
require 'fast_spec_helper'
+# Patching ActiveSupport::Concern
+require_relative '../../../../config/initializers/0_as_concern'
+
RSpec.describe Gitlab::Utils::Override do
let(:base) do
Struct.new(:good) do
@@ -164,6 +167,70 @@ RSpec.describe Gitlab::Utils::Override do
it_behaves_like 'checking as intended, nothing was overridden'
end
+
+ context 'when ActiveSupport::Concern and class_methods are used' do
+ # We need to give module names before using Override
+ let(:base) { stub_const('Base', Module.new) }
+ let(:extension) { stub_const('Extension', Module.new) }
+
+ def define_base(method_name:)
+ base.module_eval do
+ extend ActiveSupport::Concern
+
+ class_methods do
+ define_method(method_name) do
+ :f
+ end
+ end
+ end
+ end
+
+ def define_extension(method_name:)
+ extension.module_eval do
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend Gitlab::Utils::Override
+
+ override method_name
+ define_method(method_name) do
+ :g
+ end
+ end
+ end
+ end
+
+ context 'when it is defining a overriding method' do
+ before do
+ define_base(method_name: :f)
+ define_extension(method_name: :f)
+
+ base.prepend(extension)
+ end
+
+ it 'verifies' do
+ expect(base.f).to eq(:g)
+
+ described_class.verify!
+ end
+ end
+
+ context 'when it is not defining a overriding method' do
+ before do
+ define_base(method_name: :f)
+ define_extension(method_name: :g)
+
+ base.prepend(extension)
+ end
+
+ it 'raises NotImplementedError' do
+ expect(base.f).to eq(:f)
+
+ expect { described_class.verify! }
+ .to raise_error(NotImplementedError)
+ end
+ end
+ end
end
context 'when STATIC_VERIFICATION is not set' do
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index dfc381d0ef2..e964e695828 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -58,6 +58,16 @@ RSpec.describe Gitlab::Utils::UsageData do
expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5)
end
+ it 'yield provided block with PostgresHll::Buckets' do
+ buckets = Gitlab::Database::PostgresHll::Buckets.new
+
+ allow_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter) do |instance|
+ allow(instance).to receive(:execute).and_return(buckets)
+ end
+
+ expect { |block| described_class.estimate_batch_distinct_count(relation, 'column', &block) }.to yield_with_args(buckets)
+ end
+
context 'quasi integration test for different counting parameters' do
# HyperLogLog http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf algorithm
# used in estimate_batch_distinct_count produce probabilistic
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 1052d4cbacc..665eebdfd9e 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -116,8 +116,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.ms_to_round_sec' do
- using RSpec::Parameterized::TableSyntax
-
where(:original, :expected) do
1999.8999 | 1.9999
12384 | 12.384
@@ -169,8 +167,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.remove_line_breaks' do
- using RSpec::Parameterized::TableSyntax
-
where(:original, :expected) do
"foo\nbar\nbaz" | "foobarbaz"
"foo\r\nbar\r\nbaz" | "foobarbaz"
@@ -281,8 +277,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.append_path' do
- using RSpec::Parameterized::TableSyntax
-
where(:host, :path, :result) do
'http://test/' | '/foo/bar' | 'http://test/foo/bar'
'http://test/' | '//foo/bar' | 'http://test/foo/bar'
@@ -393,8 +387,6 @@ RSpec.describe Gitlab::Utils do
end
describe ".safe_downcase!" do
- using RSpec::Parameterized::TableSyntax
-
where(:str, :result) do
"test".freeze | "test"
"Test".freeze | "test"
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 9662ad13631..c22df5dd063 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -15,9 +15,7 @@ RSpec.describe Gitlab::Workhorse do
end
before do
- allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({
- 'gitaly-feature-foobar' => 'true'
- })
+ stub_feature_flags(gitaly_enforce_requests_limits: true)
end
describe ".send_git_archive" do
@@ -43,7 +41,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -73,7 +71,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -124,7 +122,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-format-patch")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -187,7 +185,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-diff")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -274,7 +272,7 @@ RSpec.describe Gitlab::Workhorse do
let(:gitaly_params) do
{
GitalyServer: {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address('default'),
token: Gitlab::GitalyClient.token('default')
}
@@ -310,6 +308,35 @@ RSpec.describe Gitlab::Workhorse do
it { is_expected.to include(ShowAllRefs: true) }
end
+
+ context 'when a feature flag is set for a single project' do
+ before do
+ stub_feature_flags(gitaly_mep_mep: project)
+ end
+
+ it 'sets the flag to true for that project' do
+ response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'true')
+ end
+
+ it 'sets the flag to false for other projects' do
+ other_project = create(:project, :public, :repository)
+ response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
+ end
+
+ it 'sets the flag to false when there is no project' do
+ snippet = create(:personal_snippet, :repository)
+ response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
+ end
+ end
end
context "when git_receive_pack action is passed" do
@@ -423,7 +450,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-blob')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -485,7 +512,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
- 'features' => { 'gitaly-feature-foobar' => 'true' },
+ 'features' => { 'gitaly-feature-enforce-requests-limits' => 'true' },
'address' => Gitlab::GitalyClient.address(project.repository_storage),
'token' => Gitlab::GitalyClient.token(project.repository_storage)
},
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb
deleted file mode 100644
index ed668c52a0e..00000000000
--- a/spec/lib/gitlab_danger_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe GitlabDanger do
- let(:gitlab_danger_helper) { nil }
-
- subject { described_class.new(gitlab_danger_helper) }
-
- 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: #{described_class::LOCAL_RULES.join(', ')}")
- end
- end
-
- describe '.success_message' do
- it 'returns an informational success message' do
- expect(described_class.success_message).to eq('==> No Danger rule violations!')
- end
- end
-
- describe '#rule_names' do
- context 'when running locally' do
- it 'returns local only rules' do
- expect(subject.rule_names).to eq(described_class::LOCAL_RULES)
- end
- end
-
- context 'when running under CI' do
- let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
-
- it 'returns all rules' do
- expect(subject.rule_names).to eq(described_class::LOCAL_RULES | described_class::CI_ONLY_RULES)
- end
- end
- end
-
- describe '#html_link' do
- context 'when running locally' do
- it 'returns the same string' do
- str = 'something'
-
- expect(subject.html_link(str)).to eq(str)
- end
- end
-
- context 'when running under CI' do
- let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
-
- it 'returns a HTML link formatted version of the string' do
- str = 'something'
- html_formatted_str = %Q{<a href="#{str}">#{str}</a>}
-
- expect(gitlab_danger_helper).to receive(:html_link).with(str).and_return(html_formatted_str)
-
- expect(subject.html_link(str)).to eq(html_formatted_str)
- end
- end
- end
-
- describe '#ci?' do
- context 'when gitlab_danger_helper is not available' do
- it 'returns false' do
- expect(subject.ci?).to be_falsey
- end
- end
-
- context 'when gitlab_danger_helper is available' do
- let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
-
- it 'returns true' do
- expect(subject.ci?).to be_truthy
- end
- end
- end
-end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index e1b8323eb8e..5f945d5b9fc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -95,6 +95,26 @@ RSpec.describe Gitlab do
end
end
+ describe '.com' do
+ subject { described_class.com { true } }
+
+ before do
+ allow(described_class).to receive(:com?).and_return(gl_com)
+ end
+
+ context 'when on GitLab.com' do
+ let(:gl_com) { true }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when not on GitLab.com' do
+ let(:gl_com) { false }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '.staging?' do
subject { described_class.staging? }
@@ -332,13 +352,13 @@ RSpec.describe Gitlab do
describe '.maintenance_mode?' do
it 'returns true when maintenance mode is enabled' do
- stub_application_setting(maintenance_mode: true)
+ stub_maintenance_mode_setting(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)
+ stub_maintenance_mode_setting(false)
expect(described_class.maintenance_mode?).to eq(false)
end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 0ead2a1d269..1361d80fe75 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
-require 'fog/core'
RSpec.describe ObjectStorage::Config do
using RSpec::Parameterized::TableSyntax
@@ -34,7 +33,9 @@ RSpec.describe ObjectStorage::Config do
}
end
- subject { described_class.new(raw_config.as_json) }
+ subject do
+ described_class.new(raw_config.as_json)
+ end
describe '#load_provider' do
before do
@@ -45,6 +46,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers AWS as a provider' do
expect(Fog.providers.keys).to include(:aws)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::AWS::Storage::Real) }
+ end
end
context 'with Google' do
@@ -59,6 +64,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers Google as a provider' do
expect(Fog.providers.keys).to include(:google)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::GoogleXML::Real) }
+ end
end
context 'with Azure' do
@@ -73,6 +82,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers AzureRM as a provider' do
expect(Fog.providers.keys).to include(:azurerm)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::AzureRM::Real) }
+ end
end
end
@@ -170,6 +183,50 @@ RSpec.describe ObjectStorage::Config do
it { expect(subject.provider).to eq('AWS') }
it { expect(subject.aws?).to be true }
it { expect(subject.google?).to be false }
+
+ it 'returns the default S3 endpoint' do
+ subject.load_provider
+
+ expect(subject.s3_endpoint).to eq("https://test-bucket.s3.amazonaws.com")
+ end
+
+ describe 'with a custom endpoint' do
+ let(:endpoint) { 'https://my.example.com' }
+
+ before do
+ credentials[:endpoint] = endpoint
+ end
+
+ it 'returns the custom endpoint' do
+ subject.load_provider
+
+ expect(subject.s3_endpoint).to eq(endpoint)
+ end
+ end
+
+ context 'with custom S3 host and port' do
+ where(:host, :port, :scheme, :expected) do
+ 's3.example.com' | 8080 | nil | 'https://test-bucket.s3.example.com:8080'
+ 's3.example.com' | 443 | nil | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | 443 | "https" | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | nil | nil | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | 80 | "http" | 'http://test-bucket.s3.example.com'
+ 's3.example.com' | "bogus" | nil | nil
+ end
+
+ with_them do
+ before do
+ credentials[:host] = host
+ credentials[:port] = port
+ credentials[:scheme] = scheme
+ subject.load_provider
+ end
+
+ it 'returns expected host' do
+ expect(subject.s3_endpoint).to eq(expected)
+ end
+ end
+ end
end
context 'with Google credentials' do
diff --git a/spec/lib/peek/views/external_http_spec.rb b/spec/lib/peek/views/external_http_spec.rb
new file mode 100644
index 00000000000..98c4f771f33
--- /dev/null
+++ b/spec/lib/peek/views/external_http_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Peek::Views::ExternalHttp, :request_store do
+ subject { described_class.new }
+
+ let(:subscriber) { Gitlab::Metrics::Subscribers::ExternalHttp.new }
+
+ before do
+ allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
+ end
+
+ let(:event_1) do
+ {
+ method: 'POST', code: "200", duration: 0.03,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true'
+ }
+ end
+
+ let(:event_2) do
+ {
+ method: 'POST', duration: 1.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: Net::ReadTimeout.new
+ }
+ end
+
+ let(:event_3) do
+ {
+ method: 'GET', code: "301", duration: 0.005,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true',
+ proxy_host: 'proxy.gitlab.com', proxy_port: 8080
+ }
+ end
+
+ it 'returns no results' do
+ expect(subject.results).to eq(
+ calls: 0, details: [], duration: "0ms", warnings: []
+ )
+ end
+
+ it 'returns aggregated results' do
+ subscriber.request(double(:event, payload: event_1))
+ subscriber.request(double(:event, payload: event_2))
+ subscriber.request(double(:event, payload: event_3))
+
+ results = subject.results
+ expect(results[:calls]).to eq(3)
+ expect(results[:duration]).to eq("1335.00ms")
+ expect(results[:details].count).to eq(3)
+
+ expected = [
+ {
+ duration: 30.0,
+ label: "POST https://gitlab.com:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ },
+ {
+ duration: 1300,
+ label: "POST http://gitlab.com:80/api/v4/projects/2/issues?current=true",
+ code: nil,
+ proxy: nil,
+ error: "Exception: Net::ReadTimeout",
+ warnings: ["1300.0 over 100"]
+ },
+ {
+ duration: 5.0,
+ label: "GET http://gitlab.com:80/api/v4/projects/2?current=true",
+ code: "Response status: 301",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ }
+ ]
+
+ expect(
+ results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) }
+ ).to match_array(expected)
+ end
+
+ context 'when the host is in IPv4 format' do
+ before do
+ event_1[:host] = '1.2.3.4'
+ end
+
+ it 'displays IPv4 in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://1.2.3.4:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the host is in IPv6 foramat' do
+ before do
+ event_1[:host] = '2606:4700:90:0:f22e:fbec:5bed:a9b9'
+ end
+
+ it 'displays IPv6 in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the query is a hash' do
+ before do
+ event_1[:query] = { current: true, 'item1' => 'string', 'item2' => [1, 2] }
+ end
+
+ it 'converts query hash into a query string' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://gitlab.com:80/api/v4/projects?current=true&item1=string&item2%5B%5D=1&item2%5B%5D=2",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the host is invalid' do
+ before do
+ event_1[:host] = '!@#%!@#%!@#%'
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when URI creation raises an URI::Error' do
+ before do
+ # This raises an URI::Error exception
+ event_1[:port] = 'invalid'
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when URI creation raises a StandardError exception' do
+ before do
+ # This raises a TypeError exception
+ event_1[:scheme] = 1234
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+end
diff --git a/spec/lib/release_highlights/validator/entry_spec.rb b/spec/lib/release_highlights/validator/entry_spec.rb
index 648356e63ba..da44938f165 100644
--- a/spec/lib/release_highlights/validator/entry_spec.rb
+++ b/spec/lib/release_highlights/validator/entry_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe ReleaseHighlights::Validator::Entry do
subject.valid?
- expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate")
+ expect(subject.errors[:packages].first).to include("must be one of", "Free", "Premium", "Ultimate")
end
end
end
diff --git a/spec/lib/release_highlights/validator_spec.rb b/spec/lib/release_highlights/validator_spec.rb
index e68d9145dcd..a423e8cc5f6 100644
--- a/spec/lib/release_highlights/validator_spec.rb
+++ b/spec/lib/release_highlights/validator_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe ReleaseHighlights::Validator do
---------------------------------------------------------
Validation failed for spec/fixtures/whats_new/invalid.yml
---------------------------------------------------------
- * Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6)
+ * Packages must be one of ["Free", "Premium", "Ultimate"] (line 6)
MESSAGE
end
diff --git a/spec/lib/security/ci_configuration/sast_build_actions_spec.rb b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb
new file mode 100644
index 00000000000..c8f9430eff9
--- /dev/null
+++ b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb
@@ -0,0 +1,539 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastBuildActions do
+ let(:default_sast_values) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'test' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 4 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec, test, tests, tmp' }
+ ] }
+ end
+
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'new_registry' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'security' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 1 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
+ ] }
+ end
+
+ let(:params_with_analyzer_info) do
+ params.merge( { 'analyzers' =>
+ [
+ {
+ 'name' => "bandit",
+ 'enabled' => false
+ },
+ {
+ 'name' => "brakeman",
+ 'enabled' => true,
+ 'variables' => [
+ { 'field' => "SAST_BRAKEMAN_LEVEL",
+ 'defaultValue' => "1",
+ 'value' => "2" }
+ ]
+ },
+ {
+ 'name' => "flawfinder",
+ 'enabled' => true,
+ 'variables' => [
+ { 'field' => "SAST_FLAWFINDER_LEVEL",
+ 'defaultValue' => "1",
+ 'value' => "1" }
+ ]
+ }
+ ] }
+ )
+ end
+
+ let(:params_with_all_analyzers_enabled) do
+ params.merge( { 'analyzers' =>
+ [
+ {
+ 'name' => "flawfinder",
+ 'enabled' => true
+ },
+ {
+ 'name' => "brakeman",
+ 'enabled' => true
+ }
+ ] }
+ )
+ end
+
+ context 'with existing .gitlab-ci.yml' do
+ let(:auto_devops_enabled) { false }
+
+ context 'sast has not been included' do
+ context 'template includes are array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_template_array_without_sast }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_two_includes)
+ end
+ end
+
+ context 'template include is not an array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_without_sast }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_two_includes)
+ end
+
+ it 'reports defaults have been overwritten' do
+ expect(result.first[:default_values_overwritten]).to eq(true)
+ end
+ end
+ end
+
+ context 'sast template include is not an array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_all_params)
+ end
+ end
+
+ context 'with default values' do
+ let(:params) { default_sast_values }
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+
+ it 'reports defaults have not been overwritten' do
+ expect(result.first[:default_values_overwritten]).to eq(false)
+ end
+
+ context 'analyzer section' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers)
+ end
+
+ context 'analyzers are disabled' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate }
+
+ it 'writes SAST_EXCLUDED_ANALYZERS' do
+ stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'bandit, brakeman, flawfinder')
+
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers)
+ end
+ end
+
+ context 'all analyzers are enabled' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_all_analyzers_enabled, gitlab_ci_content).generate }
+
+ it 'does not write SAST_DEFAULT_ANALYZERS or SAST_EXCLUDED_ANALYZERS' do
+ stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'brakeman, flawfinder')
+
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+ end
+ end
+ end
+
+ context 'with update stage and SEARCH_MAX_DEPTH and set SECURE_ANALYZERS_PREFIX to default' do
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'brand_new_stage' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 5 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
+ ] }
+ end
+
+ let(:gitlab_ci_content) { existing_gitlab_ci }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_updated_stage)
+ end
+ end
+
+ context 'with no existing variables' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_variables }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_variable_section_added)
+ end
+ end
+
+ context 'with no existing sast config' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_section }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_sast_section_added)
+ end
+ end
+
+ context 'with no existing sast variables' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_variables }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_sast_variables_section_added)
+ end
+ end
+
+ def existing_gitlab_ci_and_template_array_without_sast
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "existing.yml" }] }
+ end
+
+ def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
+ { "stages" => %w(test),
+ "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
+ "include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
+ end
+
+ def existing_gitlab_ci_and_single_template_without_sast
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => { "template" => "existing.yml" } }
+ end
+
+ def existing_gitlab_ci_with_no_variables
+ { "stages" => %w(test security),
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci_with_no_sast_section
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci_with_no_sast_variables
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+ end
+
+ context 'with no .gitlab-ci.yml' do
+ let(:gitlab_ci_content) { nil }
+
+ context 'autodevops disabled' do
+ let(:auto_devops_enabled) { false }
+
+ context 'with one empty parameter' do
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => '' }
+ ] }
+ end
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+ end
+
+ context 'with all parameters' do
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_all_params)
+ end
+ end
+ end
+
+ context 'with autodevops enabled' do
+ let(:auto_devops_enabled) { true }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ before do
+ allow_next_instance_of(described_class) do |sast_build_actions|
+ allow(sast_build_actions).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages)
+ end
+ end
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(auto_devops_with_custom_stage)
+ end
+ end
+ end
+
+ describe 'Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS' do
+ subject(:variable) {Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS}
+
+ it 'is sorted alphabetically' do
+ sorted_variable = Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS
+ .split(',')
+ .map(&:strip)
+ .sort
+ .join(', ')
+
+ expect(variable).to eq(sorted_variable)
+ end
+ end
+
+ # stubbing this method allows this spec file to use fast_spec_helper
+ def fast_auto_devops_stages
+ auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') )
+ auto_devops_template['stages']
+ end
+
+ def sast_yaml_with_no_variables_set_but_analyzers
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ sast:
+ variables:
+ SAST_EXCLUDED_ANALYZERS: bandit
+ SAST_BRAKEMAN_LEVEL: '2'
+ stage: test
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_with_no_variables_set
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ sast:
+ stage: test
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_all_params
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def auto_devops_with_custom_stage
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - build
+ - test
+ - deploy
+ - review
+ - dast
+ - staging
+ - canary
+ - production
+ - incremental rollout 10%
+ - incremental rollout 25%
+ - incremental rollout 50%
+ - incremental rollout 100%
+ - performance
+ - cleanup
+ - security
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_two_includes
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: existing.yml
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_variable_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ CI_YML
+ end
+
+ def sast_yaml_sast_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ CI_YML
+ end
+
+ def sast_yaml_sast_variables_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ stage: security
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_updated_stage
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ - brand_new_stage
+ variables:
+ RANDOM: make sure this persists
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 5
+ stage: brand_new_stage
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+end