From d298fad0c0564454271cba11e6f20c19681534ac Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 5 Feb 2021 16:20:45 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-9-stable-ee --- spec/lib/gitlab/access/branch_protection_spec.rb | 10 +- spec/lib/gitlab/auth/ip_rate_limiter_spec.rb | 3 + .../lib/gitlab/auth/u2f_webauthn_converter_spec.rb | 29 + ...move_duplicate_vulnerabilities_findings_spec.rb | 135 +++++ spec/lib/gitlab/background_migration_spec.rb | 2 +- spec/lib/gitlab/badge/coverage/metadata_spec.rb | 32 -- spec/lib/gitlab/badge/coverage/report_spec.rb | 99 ---- spec/lib/gitlab/badge/coverage/template_spec.rb | 182 ------- spec/lib/gitlab/badge/pipeline/metadata_spec.rb | 29 - spec/lib/gitlab/badge/pipeline/status_spec.rb | 127 ----- spec/lib/gitlab/badge/pipeline/template_spec.rb | 140 ----- spec/lib/gitlab/badge/shared/metadata.rb | 33 -- spec/lib/gitlab/changelog/committer_spec.rb | 128 +++++ spec/lib/gitlab/changelog/config_spec.rb | 96 ++++ spec/lib/gitlab/changelog/generator_spec.rb | 164 ++++++ spec/lib/gitlab/changelog/release_spec.rb | 107 ++++ .../lib/gitlab/changelog/template/compiler_spec.rb | 136 +++++ spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb | 32 ++ spec/lib/gitlab/ci/badge/coverage/report_spec.rb | 99 ++++ spec/lib/gitlab/ci/badge/coverage/template_spec.rb | 182 +++++++ spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb | 29 + spec/lib/gitlab/ci/badge/pipeline/status_spec.rb | 127 +++++ spec/lib/gitlab/ci/badge/pipeline/template_spec.rb | 140 +++++ spec/lib/gitlab/ci/badge/shared/metadata.rb | 33 ++ .../credentials/registry/dependency_proxy_spec.rb | 43 ++ .../credentials/registry/gitlab_registry_spec.rb | 43 ++ .../gitlab/ci/build/credentials/registry_spec.rb | 43 -- spec/lib/gitlab/ci/build/rules_spec.rb | 29 +- spec/lib/gitlab/ci/charts_spec.rb | 82 +++ spec/lib/gitlab/ci/config/entry/cache_spec.rb | 10 +- spec/lib/gitlab/ci/config/entry/job_spec.rb | 10 - .../lib/gitlab/ci/config/entry/processable_spec.rb | 29 + spec/lib/gitlab/ci/config/external/mapper_spec.rb | 14 - spec/lib/gitlab/ci/cron_parser_spec.rb | 11 + spec/lib/gitlab/ci/parsers/instrumentation_spec.rb | 27 + spec/lib/gitlab/ci/parsers_spec.rb | 8 + .../chain/cancel_pending_pipelines_spec.rb | 14 - .../gitlab/ci/reports/codequality_mr_diff_spec.rb | 58 ++ .../reports/codequality_reports_comparer_spec.rb | 58 +- .../gitlab/ci/reports/codequality_reports_spec.rb | 58 +- spec/lib/gitlab/ci/trace/chunked_io_spec.rb | 2 +- spec/lib/gitlab/ci/variables/helpers_spec.rb | 103 ++++ .../cleanup/orphan_job_artifact_files_spec.rb | 3 +- spec/lib/gitlab/cluster/lifecycle_events_spec.rb | 85 +++ spec/lib/gitlab/composer/cache_spec.rb | 133 +++++ spec/lib/gitlab/composer/version_index_spec.rb | 16 +- spec/lib/gitlab/conan_token_spec.rb | 2 +- spec/lib/gitlab/crypto_helper_spec.rb | 78 ++- spec/lib/gitlab/current_settings_spec.rb | 28 + spec/lib/gitlab/danger/base_linter_spec.rb | 193 ------- spec/lib/gitlab/danger/changelog_spec.rb | 229 -------- spec/lib/gitlab/danger/commit_linter_spec.rb | 242 --------- spec/lib/gitlab/danger/danger_spec_helper.rb | 17 - spec/lib/gitlab/danger/emoji_checker_spec.rb | 38 -- spec/lib/gitlab/danger/helper_spec.rb | 602 --------------------- .../lib/gitlab/danger/merge_request_linter_spec.rb | 55 -- spec/lib/gitlab/danger/roulette_spec.rb | 413 -------------- spec/lib/gitlab/danger/sidekiq_queues_spec.rb | 82 --- spec/lib/gitlab/danger/teammate_spec.rb | 220 -------- spec/lib/gitlab/danger/title_linting_spec.rb | 56 -- .../gitlab/danger/weightage/maintainers_spec.rb | 34 -- spec/lib/gitlab/danger/weightage/reviewers_spec.rb | 63 --- spec/lib/gitlab/data_builder/build_spec.rb | 4 +- spec/lib/gitlab/data_builder/pipeline_spec.rb | 4 +- .../gitlab/database/migration_helpers/v2_spec.rb | 221 ++++++++ spec/lib/gitlab/database/migration_helpers_spec.rb | 253 --------- .../table_management_helpers_spec.rb | 3 +- spec/lib/gitlab/database/with_lock_retries_spec.rb | 4 + spec/lib/gitlab/diff/char_diff_spec.rb | 77 +++ .../lib/gitlab/diff/file_collection_sorter_spec.rb | 10 +- spec/lib/gitlab/diff/highlight_cache_spec.rb | 18 + spec/lib/gitlab/diff/inline_diff_spec.rb | 27 + .../experimentation/controller_concern_spec.rb | 27 + spec/lib/gitlab/experimentation/experiment_spec.rb | 3 +- spec/lib/gitlab/experimentation_spec.rb | 65 ++- spec/lib/gitlab/file_finder_spec.rb | 8 + spec/lib/gitlab/git/commit_spec.rb | 3 +- spec/lib/gitlab/git/diff_spec.rb | 7 + spec/lib/gitlab/git/repository_spec.rb | 5 +- .../gitlab/graphql/pagination/connections_spec.rb | 97 ++++ spec/lib/gitlab/hook_data/group_builder_spec.rb | 68 +++ spec/lib/gitlab/hook_data/subgroup_builder_spec.rb | 52 ++ spec/lib/gitlab/import_export/all_models.yml | 5 +- .../import_export/design_repo_restorer_spec.rb | 4 +- .../gitlab/import_export/design_repo_saver_spec.rb | 2 +- spec/lib/gitlab/import_export/fork_spec.rb | 4 +- .../import_export/group/tree_restorer_spec.rb | 1 + spec/lib/gitlab/import_export/importer_spec.rb | 4 +- .../lib/gitlab/import_export/repo_restorer_spec.rb | 10 +- spec/lib/gitlab/import_export/repo_saver_spec.rb | 10 +- .../gitlab/import_export/safe_model_attributes.yml | 4 + spec/lib/gitlab/import_export/saver_spec.rb | 12 +- .../gitlab/import_export/wiki_repo_saver_spec.rb | 2 +- .../redis_cluster_validator_spec.rb | 1 + spec/lib/gitlab/instrumentation_helper_spec.rb | 6 +- spec/lib/gitlab/kas_spec.rb | 44 ++ .../metrics/subscribers/external_http_spec.rb | 172 ++++++ .../gitlab/metrics/subscribers/rack_attack_spec.rb | 203 +++++++ spec/lib/gitlab/patch/prependable_spec.rb | 18 + spec/lib/gitlab/performance_bar/stats_spec.rb | 8 +- .../rack_attack/instrumented_cache_store_spec.rb | 89 +++ spec/lib/gitlab/rack_attack_spec.rb | 3 + spec/lib/gitlab/repository_cache_adapter_spec.rb | 7 +- spec/lib/gitlab/search/query_spec.rb | 18 + spec/lib/gitlab/search_results_spec.rb | 7 +- .../sidekiq_logging/structured_logger_spec.rb | 3 +- spec/lib/gitlab/suggestions/commit_message_spec.rb | 11 + .../finders/global_template_finder_spec.rb | 23 +- .../terraform/state_migration_helper_spec.rb | 21 + spec/lib/gitlab/tracking/standard_context_spec.rb | 50 +- spec/lib/gitlab/url_blocker_spec.rb | 46 +- spec/lib/gitlab/url_blockers/url_allowlist_spec.rb | 28 +- spec/lib/gitlab/usage/docs/renderer_spec.rb | 21 + spec/lib/gitlab/usage/docs/value_formatter_spec.rb | 26 + spec/lib/gitlab/usage/metric_definition_spec.rb | 23 +- spec/lib/gitlab/usage/metric_spec.rb | 8 +- .../usage/metrics/aggregates/aggregate_spec.rb | 256 +++++++++ .../usage_data_counters/aggregated_metrics_spec.rb | 4 +- .../usage_data_counters/hll_redis_counter_spec.rb | 195 ++----- .../merge_request_activity_unique_counter_spec.rb | 48 ++ .../quick_action_activity_unique_counter_spec.rb | 163 ++++++ spec/lib/gitlab/usage_data_spec.rb | 144 +++-- spec/lib/gitlab/utils/markdown_spec.rb | 44 +- spec/lib/gitlab/utils/override_spec.rb | 67 +++ spec/lib/gitlab/utils/usage_data_spec.rb | 103 ++++ spec/lib/gitlab/utils_spec.rb | 8 - 126 files changed, 4479 insertions(+), 3728 deletions(-) create mode 100644 spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb create mode 100644 spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/metadata_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/report_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/template_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/metadata_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/status_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/template_spec.rb delete mode 100644 spec/lib/gitlab/badge/shared/metadata.rb create mode 100644 spec/lib/gitlab/changelog/committer_spec.rb create mode 100644 spec/lib/gitlab/changelog/config_spec.rb create mode 100644 spec/lib/gitlab/changelog/generator_spec.rb create mode 100644 spec/lib/gitlab/changelog/release_spec.rb create mode 100644 spec/lib/gitlab/changelog/template/compiler_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/report_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/template_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/status_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/template_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/shared/metadata.rb create mode 100644 spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb create mode 100644 spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb delete mode 100644 spec/lib/gitlab/ci/build/credentials/registry_spec.rb create mode 100644 spec/lib/gitlab/ci/parsers/instrumentation_spec.rb create mode 100644 spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb create mode 100644 spec/lib/gitlab/ci/variables/helpers_spec.rb create mode 100644 spec/lib/gitlab/cluster/lifecycle_events_spec.rb create mode 100644 spec/lib/gitlab/composer/cache_spec.rb delete mode 100644 spec/lib/gitlab/danger/base_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/changelog_spec.rb delete mode 100644 spec/lib/gitlab/danger/commit_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/danger_spec_helper.rb delete mode 100644 spec/lib/gitlab/danger/emoji_checker_spec.rb delete mode 100644 spec/lib/gitlab/danger/helper_spec.rb delete mode 100644 spec/lib/gitlab/danger/merge_request_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/roulette_spec.rb delete mode 100644 spec/lib/gitlab/danger/sidekiq_queues_spec.rb delete mode 100644 spec/lib/gitlab/danger/teammate_spec.rb delete mode 100644 spec/lib/gitlab/danger/title_linting_spec.rb delete mode 100644 spec/lib/gitlab/danger/weightage/maintainers_spec.rb delete mode 100644 spec/lib/gitlab/danger/weightage/reviewers_spec.rb create mode 100644 spec/lib/gitlab/database/migration_helpers/v2_spec.rb create mode 100644 spec/lib/gitlab/diff/char_diff_spec.rb create mode 100644 spec/lib/gitlab/graphql/pagination/connections_spec.rb create mode 100644 spec/lib/gitlab/hook_data/group_builder_spec.rb create mode 100644 spec/lib/gitlab/hook_data/subgroup_builder_spec.rb create mode 100644 spec/lib/gitlab/metrics/subscribers/external_http_spec.rb create mode 100644 spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb create mode 100644 spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb create mode 100644 spec/lib/gitlab/terraform/state_migration_helper_spec.rb create mode 100644 spec/lib/gitlab/usage/docs/renderer_spec.rb create mode 100644 spec/lib/gitlab/usage/docs/value_formatter_spec.rb create mode 100644 spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb create mode 100644 spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb (limited to 'spec/lib/gitlab') 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/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/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/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/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb deleted file mode 100644 index 725ae03ad74..00000000000 --- a/spec/lib/gitlab/badge/coverage/metadata_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'lib/gitlab/badge/shared/metadata' - -RSpec.describe Gitlab::Badge::Coverage::Metadata do - let(:badge) do - double(project: create(:project), ref: 'feature', job: 'test') - end - - let(:metadata) { described_class.new(badge) } - - it_behaves_like 'badge metadata' - - describe '#title' do - it 'returns coverage report title' do - expect(metadata.title).to eq 'coverage report' - end - end - - describe '#image_url' do - it 'returns valid url' do - expect(metadata.image_url).to include 'badges/feature/coverage.svg' - end - end - - describe '#link_url' do - it 'returns valid link' do - expect(metadata.link_url).to include 'commits/feature' - end - end -end diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb deleted file mode 100644 index 3b5ea3291e4..00000000000 --- a/spec/lib/gitlab/badge/coverage/report_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::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) } - let_it_be(:failure_pipeline) { create(:ci_pipeline, :failed, project: project) } - - let_it_be(:builds) do - [ - create(:ci_build, :success, pipeline: success_pipeline, coverage: 40, created_at: 9.seconds.ago, name: 'coverage'), - create(:ci_build, :success, pipeline: success_pipeline, coverage: 60, created_at: 8.seconds.ago) - ] - end - - let(:badge) do - described_class.new(project, 'master', opts: { job: job_name }) - end - - let(:job_name) { nil } - - describe '#entity' do - it 'describes a coverage' do - expect(badge.entity).to eq 'coverage' - end - end - - describe '#metadata' do - it 'returns correct metadata' do - expect(badge.metadata.image_url).to include 'coverage.svg' - end - end - - describe '#template' do - it 'returns correct template' do - expect(badge.template.key_text).to eq 'coverage' - end - end - - describe '#status' do - context 'with no job specified' do - it 'returns the most recent successful pipeline coverage value' do - expect(badge.status).to eq(50.00) - end - - context 'and no successful pipelines' do - before do - allow(badge).to receive(:successful_pipeline).and_return(nil) - end - - it 'returns nil' do - expect(badge.status).to eq(nil) - end - end - end - - context 'with a blank job name' do - let(:job_name) { ' ' } - - it 'returns the latest successful pipeline coverage value' do - expect(badge.status).to eq(50.00) - end - end - - context 'with an unmatching job name specified' do - let(:job_name) { 'incorrect name' } - - it 'returns nil' do - expect(badge.status).to be_nil - end - end - - context 'with a matching job name specified' do - let(:job_name) { 'coverage' } - - it 'returns the pipeline coverage value' do - expect(badge.status).to eq(40.00) - end - - context 'with a more recent running pipeline' do - let!(:another_build) { create(:ci_build, :success, pipeline: running_pipeline, coverage: 20, created_at: 7.seconds.ago, name: 'coverage') } - - it 'returns the running pipeline coverage value' do - expect(badge.status).to eq(20.00) - end - end - - context 'with a more recent failed pipeline' do - let!(:another_build) { create(:ci_build, :success, pipeline: failure_pipeline, coverage: 10, created_at: 6.seconds.ago, name: 'coverage') } - - it 'returns the failed pipeline coverage value' do - expect(badge.status).to eq(10.00) - end - end - end - end -end diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb deleted file mode 100644 index ba5c1b2ce6e..00000000000 --- a/spec/lib/gitlab/badge/coverage/template_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Badge::Coverage::Template do - let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) } - let(:template) { described_class.new(badge) } - - describe '#key_text' do - it 'says coverage by default' do - expect(template.key_text).to eq 'coverage' - end - - context 'when custom key_text is defined' do - before do - allow(badge).to receive(:customization).and_return({ key_text: "custom text" }) - end - - it 'returns custom value' do - expect(template.key_text).to eq "custom text" - end - - context 'when its size is larger than the max allowed value' do - before do - allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 }) - end - - it 'returns default value' do - expect(template.key_text).to eq 'coverage' - end - end - end - end - - describe '#value_text' do - context 'when coverage is known' do - it 'returns coverage percentage' do - expect(template.value_text).to eq '90.00%' - end - end - - context 'when coverage is known to many digits' do - before do - allow(badge).to receive(:status).and_return(92.349) - end - - it 'returns rounded coverage percentage' do - expect(template.value_text).to eq '92.35%' - end - end - - context 'when coverage is unknown' do - before do - allow(badge).to receive(:status).and_return(nil) - end - - it 'returns string that says coverage is unknown' do - expect(template.value_text).to eq 'unknown' - end - end - end - - describe '#key_width' do - it 'is fixed by default' do - expect(template.key_width).to eq 62 - end - - context 'when custom key_width is defined' do - before do - allow(badge).to receive(:customization).and_return({ key_width: 101 }) - end - - it 'returns custom value' do - expect(template.key_width).to eq 101 - end - - context 'when it is larger than the max allowed value' do - before do - allow(badge).to receive(:customization).and_return({ key_width: 513 }) - end - - it 'returns default value' do - expect(template.key_width).to eq 62 - end - end - end - end - - describe '#value_width' do - context 'when coverage is known' do - it 'is narrower when coverage is known' do - expect(template.value_width).to eq 54 - end - end - - context 'when coverage is unknown' do - before do - allow(badge).to receive(:status).and_return(nil) - end - - it 'is wider when coverage is unknown to fit text' do - expect(template.value_width).to eq 58 - end - end - end - - describe '#key_color' do - it 'always has the same color' do - expect(template.key_color).to eq '#555' - end - end - - describe '#value_color' do - context 'when coverage is good' do - before do - allow(badge).to receive(:status).and_return(98) - end - - it 'is green' do - expect(template.value_color).to eq '#4c1' - end - end - - context 'when coverage is acceptable' do - before do - allow(badge).to receive(:status).and_return(90) - end - - it 'is green-orange' do - expect(template.value_color).to eq '#a3c51c' - end - end - - context 'when coverage is medium' do - before do - allow(badge).to receive(:status).and_return(75) - end - - it 'is orange-yellow' do - expect(template.value_color).to eq '#dfb317' - end - end - - context 'when coverage is low' do - before do - allow(badge).to receive(:status).and_return(50) - end - - it 'is red' do - expect(template.value_color).to eq '#e05d44' - end - end - - context 'when coverage is unknown' do - before do - allow(badge).to receive(:status).and_return(nil) - end - - it 'is grey' do - expect(template.value_color).to eq '#9f9f9f' - end - end - end - - describe '#width' do - context 'when coverage is known' do - it 'returns the key width plus value width' do - expect(template.width).to eq 116 - end - end - - context 'when coverage is unknown' do - before do - allow(badge).to receive(:status).and_return(nil) - end - - it 'returns key width plus wider value width' do - expect(template.width).to eq 120 - end - end - end -end diff --git a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb b/spec/lib/gitlab/badge/pipeline/metadata_spec.rb deleted file mode 100644 index c8ed0c8ea29..00000000000 --- a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'lib/gitlab/badge/shared/metadata' - -RSpec.describe Gitlab::Badge::Pipeline::Metadata do - let(:badge) { double(project: create(:project), ref: 'feature') } - let(:metadata) { described_class.new(badge) } - - it_behaves_like 'badge metadata' - - describe '#title' do - it 'returns build status title' do - expect(metadata.title).to eq 'pipeline status' - end - end - - describe '#image_url' do - it 'returns valid url' do - expect(metadata.image_url).to include 'badges/feature/pipeline.svg' - end - end - - describe '#link_url' do - it 'returns valid link' do - expect(metadata.link_url).to include 'commits/feature' - end - end -end diff --git a/spec/lib/gitlab/badge/pipeline/status_spec.rb b/spec/lib/gitlab/badge/pipeline/status_spec.rb deleted file mode 100644 index b5dabca0477..00000000000 --- a/spec/lib/gitlab/badge/pipeline/status_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Badge::Pipeline::Status do - let(:project) { create(:project, :repository) } - let(:sha) { project.commit.sha } - let(:branch) { 'master' } - let(:badge) { described_class.new(project, branch) } - - describe '#entity' do - it 'always says pipeline' do - expect(badge.entity).to eq 'pipeline' - end - end - - describe '#template' do - it 'returns badge template' do - expect(badge.template.key_text).to eq 'pipeline' - end - end - - describe '#metadata' do - it 'returns badge metadata' do - expect(badge.metadata.image_url).to include 'badges/master/pipeline.svg' - end - end - - context 'pipeline exists', :sidekiq_might_not_need_inline do - let!(:pipeline) { create_pipeline(project, sha, branch) } - - context 'pipeline success' do - before do - pipeline.success! - end - - describe '#status' do - it 'is successful' do - expect(badge.status).to eq 'success' - end - end - end - - context 'pipeline failed' do - before do - pipeline.drop! - end - - describe '#status' do - it 'failed' do - expect(badge.status).to eq 'failed' - end - end - end - - context 'when outdated pipeline for given ref exists' do - before do - pipeline.success! - - old_pipeline = create_pipeline(project, '11eeffdd', branch) - old_pipeline.drop! - end - - it 'does not take outdated pipeline into account' do - expect(badge.status).to eq 'success' - end - end - - context 'when multiple pipelines exist for given sha' do - before do - pipeline.drop! - - new_pipeline = create_pipeline(project, sha, branch) - new_pipeline.success! - end - - it 'does not take outdated pipeline into account' do - expect(badge.status).to eq 'success' - end - end - - context 'when ignored_skipped is set to true' do - let(:new_badge) { described_class.new(project, branch, opts: { ignore_skipped: true }) } - - before do - pipeline.skip! - end - - describe '#status' do - it 'uses latest non-skipped status' do - expect(new_badge.status).not_to eq 'skipped' - end - end - end - - context 'when ignored_skipped is set to false' do - let(:new_badge) { described_class.new(project, branch, opts: { ignore_skipped: false }) } - - before do - pipeline.skip! - end - - describe '#status' do - it 'uses latest status' do - expect(new_badge.status).to eq 'skipped' - end - end - end - end - - context 'build does not exist' do - describe '#status' do - it 'is unknown' do - expect(badge.status).to eq 'unknown' - end - end - end - - def create_pipeline(project, sha, branch) - pipeline = create(:ci_empty_pipeline, - project: project, - sha: sha, - ref: branch) - - create(:ci_build, pipeline: pipeline, stage: 'notify') - end -end diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb deleted file mode 100644 index c78e95852f3..00000000000 --- a/spec/lib/gitlab/badge/pipeline/template_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Badge::Pipeline::Template do - let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) } - let(:template) { described_class.new(badge) } - - describe '#key_text' do - it 'says pipeline by default' do - expect(template.key_text).to eq 'pipeline' - end - - context 'when custom key_text is defined' do - before do - allow(badge).to receive(:customization).and_return({ key_text: 'custom text' }) - end - - it 'returns custom value' do - expect(template.key_text).to eq 'custom text' - end - - context 'when its size is larger than the max allowed value' do - before do - allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 }) - end - - it 'returns default value' do - expect(template.key_text).to eq 'pipeline' - end - end - end - end - - describe '#value_text' do - it 'is status value' do - expect(template.value_text).to eq 'passed' - end - end - - describe '#key_width' do - it 'is fixed by default' do - expect(template.key_width).to eq 62 - end - - context 'when custom key_width is defined' do - before do - allow(badge).to receive(:customization).and_return({ key_width: 101 }) - end - - it 'returns custom value' do - expect(template.key_width).to eq 101 - end - - context 'when it is larger than the max allowed value' do - before do - allow(badge).to receive(:customization).and_return({ key_width: 513 }) - end - - it 'returns default value' do - expect(template.key_width).to eq 62 - end - end - end - end - - describe 'widths and text anchors' do - it 'has fixed width and text anchors' do - expect(template.width).to eq 116 - expect(template.key_width).to eq 62 - expect(template.value_width).to eq 54 - expect(template.key_text_anchor).to eq 31 - expect(template.value_text_anchor).to eq 89 - end - end - - describe '#key_color' do - it 'is always the same' do - expect(template.key_color).to eq '#555' - end - end - - describe '#value_color' do - context 'when status is success' do - it 'has expected color' do - expect(template.value_color).to eq '#4c1' - end - end - - context 'when status is failed' do - before do - allow(badge).to receive(:status).and_return('failed') - end - - it 'has expected color' do - expect(template.value_color).to eq '#e05d44' - end - end - - context 'when status is running' do - before do - allow(badge).to receive(:status).and_return('running') - end - - it 'has expected color' do - expect(template.value_color).to eq '#dfb317' - end - end - - context 'when status is preparing' do - before do - allow(badge).to receive(:status).and_return('preparing') - end - - it 'has expected color' do - expect(template.value_color).to eq '#a7a7a7' - end - end - - context 'when status is unknown' do - before do - allow(badge).to receive(:status).and_return('unknown') - end - - it 'has expected color' do - expect(template.value_color).to eq '#9f9f9f' - end - end - - context 'when status does not match any known statuses' do - before do - allow(badge).to receive(:status).and_return('invalid') - end - - it 'has expected color' do - expect(template.value_color).to eq '#9f9f9f' - end - end - end -end diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb deleted file mode 100644 index c99a65bb2f4..00000000000 --- a/spec/lib/gitlab/badge/shared/metadata.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'badge metadata' do - describe '#to_html' do - let(:html) { Nokogiri::HTML.parse(metadata.to_html) } - let(:a_href) { html.at('a') } - - it 'points to link' do - expect(a_href[:href]).to eq metadata.link_url - end - - it 'contains clickable image' do - expect(a_href.children.first.name).to eq 'img' - end - end - - describe '#to_markdown' do - subject { metadata.to_markdown } - - it { is_expected.to include metadata.image_url } - it { is_expected.to include metadata.link_url } - end - - describe '#to_asciidoc' do - subject { metadata.to_asciidoc } - - it { is_expected.to include metadata.image_url } - it { is_expected.to include metadata.link_url } - it { is_expected.to include 'image:' } - it { is_expected.to include 'link=' } - it { is_expected.to include 'title=' } - 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..f0d6bc2b6b5 --- /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 CommitError' 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(described_class::CommitError) + 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..adf82fa3ac2 --- /dev/null +++ b/spec/lib/gitlab/changelog/config_spec.rb @@ -0,0 +1,96 @@ +# 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::Template::Template) + expect(config.categories).to eq({ 'foo' => 'bar' }) + end + + it 'raises ConfigError when the categories are not a Hash' do + expect { described_class.from_hash(project, 'categories' => 10) } + .to raise_error(described_class::ConfigError) + 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/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb new file mode 100644 index 00000000000..50a23d23299 --- /dev/null +++ b/spec/lib/gitlab/changelog/release_spec.rb @@ -0,0 +1,107 @@ +# 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 + 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/changelog/template/compiler_spec.rb b/spec/lib/gitlab/changelog/template/compiler_spec.rb new file mode 100644 index 00000000000..8b09bc90529 --- /dev/null +++ b/spec/lib/gitlab/changelog/template/compiler_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Changelog::Template::Compiler do + def compile(template, data = {}) + Gitlab::Changelog::Template::Compiler.new.compile(template).render(data) + end + + describe '#compile' do + it 'compiles an empty template' do + expect(compile('')).to eq('') + end + + it 'compiles a template with an undefined variable' do + expect(compile('{{number}}')).to eq('') + end + + it 'compiles a template with a defined variable' do + expect(compile('{{number}}', 'number' => 42)).to eq('42') + end + + it 'compiles a template with the special "it" variable' do + expect(compile('{{it}}', 'values' => 10)).to eq({ 'values' => 10 }.to_s) + end + + it 'compiles a template containing an if statement' do + expect(compile('{% if foo %}yes{% end %}', 'foo' => true)).to eq('yes') + end + + it 'compiles a template containing an if/else statement' do + expect(compile('{% if foo %}yes{% else %}no{% end %}', 'foo' => false)) + .to eq('no') + end + + it 'compiles a template that iterates over an Array' do + expect(compile('{% each numbers %}{{it}}{% end %}', 'numbers' => [1, 2, 3])) + .to eq('123') + end + + it 'compiles a template that iterates over a Hash' do + output = compile( + '{% each pairs %}{{0}}={{1}}{% end %}', + 'pairs' => { 'key' => 'value' } + ) + + expect(output).to eq('key=value') + end + + it 'compiles a template that iterates over a Hash of Arrays' do + output = compile( + '{% each values %}{{key}}{% end %}', + 'values' => [{ 'key' => 'value' }] + ) + + expect(output).to eq('value') + end + + it 'compiles a template with a variable path' do + output = compile('{{foo.bar}}', 'foo' => { 'bar' => 10 }) + + expect(output).to eq('10') + end + + it 'compiles a template with a variable path that uses an Array index' do + output = compile('{{foo.values.1}}', 'foo' => { 'values' => [10, 20] }) + + expect(output).to eq('20') + end + + it 'compiles a template with a variable path that uses a Hash and a numeric index' do + output = compile('{{foo.1}}', 'foo' => { 'key' => 'value' }) + + expect(output).to eq('') + end + + it 'compiles a template with a variable path that uses an Array and a String based index' do + output = compile('{{foo.numbers.bla}}', 'foo' => { 'numbers' => [10, 20] }) + + expect(output).to eq('') + end + + it 'ignores ERB tags provided by the user' do + input = '<% exit %> <%= exit %> <%= foo -%>' + + expect(compile(input)).to eq(input) + end + + it 'removes newlines introduced by end statements on their own lines' do + output = compile(<<~TPL, 'foo' => true) + {% if foo %} + foo + {% end %} + TPL + + expect(output).to eq("foo\n") + end + + it 'supports escaping of trailing newlines' do + output = compile(<<~TPL) + foo \ + bar\ + baz + TPL + + expect(output).to eq("foo barbaz\n") + end + + # rubocop: disable Lint/InterpolationCheck + it 'ignores embedded Ruby expressions' do + input = '#{exit}' + + expect(compile(input)).to eq(input) + end + # rubocop: enable Lint/InterpolationCheck + + it 'ignores ERB tags inside variable tags' do + input = '{{<%= exit %>}}' + + expect(compile(input)).to eq(input) + end + + it 'ignores malicious code that tries to escape a variable' do + input = "{{') ::Kernel.exit # '}}" + + expect(compile(input)).to eq(input) + end + + it 'ignores malicious code that makes use of whitespace' do + input = "x<\\\n%::Kernel.system(\"id\")%>" + + expect(Kernel).not_to receive(:system).with('id') + expect(compile(input)).to eq('x<%::Kernel.system("id")%>') + end + end +end diff --git a/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb new file mode 100644 index 00000000000..6d272f060ab --- /dev/null +++ b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'lib/gitlab/ci/badge/shared/metadata' + +RSpec.describe Gitlab::Ci::Badge::Coverage::Metadata do + let(:badge) do + double(project: create(:project), ref: 'feature', job: 'test') + end + + let(:metadata) { described_class.new(badge) } + + it_behaves_like 'badge metadata' + + describe '#title' do + it 'returns coverage report title' do + expect(metadata.title).to eq 'coverage report' + end + end + + describe '#image_url' do + it 'returns valid url' do + expect(metadata.image_url).to include 'badges/feature/coverage.svg' + end + end + + describe '#link_url' do + it 'returns valid link' do + expect(metadata.link_url).to include 'commits/feature' + end + end +end diff --git a/spec/lib/gitlab/ci/badge/coverage/report_spec.rb b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb new file mode 100644 index 00000000000..13696d815aa --- /dev/null +++ b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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) } + let_it_be(:failure_pipeline) { create(:ci_pipeline, :failed, project: project) } + + let_it_be(:builds) do + [ + create(:ci_build, :success, pipeline: success_pipeline, coverage: 40, created_at: 9.seconds.ago, name: 'coverage'), + create(:ci_build, :success, pipeline: success_pipeline, coverage: 60, created_at: 8.seconds.ago) + ] + end + + let(:badge) do + described_class.new(project, 'master', opts: { job: job_name }) + end + + let(:job_name) { nil } + + describe '#entity' do + it 'describes a coverage' do + expect(badge.entity).to eq 'coverage' + end + end + + describe '#metadata' do + it 'returns correct metadata' do + expect(badge.metadata.image_url).to include 'coverage.svg' + end + end + + describe '#template' do + it 'returns correct template' do + expect(badge.template.key_text).to eq 'coverage' + end + end + + describe '#status' do + context 'with no job specified' do + it 'returns the most recent successful pipeline coverage value' do + expect(badge.status).to eq(50.00) + end + + context 'and no successful pipelines' do + before do + allow(badge).to receive(:successful_pipeline).and_return(nil) + end + + it 'returns nil' do + expect(badge.status).to eq(nil) + end + end + end + + context 'with a blank job name' do + let(:job_name) { ' ' } + + it 'returns the latest successful pipeline coverage value' do + expect(badge.status).to eq(50.00) + end + end + + context 'with an unmatching job name specified' do + let(:job_name) { 'incorrect name' } + + it 'returns nil' do + expect(badge.status).to be_nil + end + end + + context 'with a matching job name specified' do + let(:job_name) { 'coverage' } + + it 'returns the pipeline coverage value' do + expect(badge.status).to eq(40.00) + end + + context 'with a more recent running pipeline' do + let!(:another_build) { create(:ci_build, :success, pipeline: running_pipeline, coverage: 20, created_at: 7.seconds.ago, name: 'coverage') } + + it 'returns the running pipeline coverage value' do + expect(badge.status).to eq(20.00) + end + end + + context 'with a more recent failed pipeline' do + let!(:another_build) { create(:ci_build, :success, pipeline: failure_pipeline, coverage: 10, created_at: 6.seconds.ago, name: 'coverage') } + + it 'returns the failed pipeline coverage value' do + expect(badge.status).to eq(10.00) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/badge/coverage/template_spec.rb b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb new file mode 100644 index 00000000000..f010d1bce50 --- /dev/null +++ b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Badge::Coverage::Template do + let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) } + let(:template) { described_class.new(badge) } + + describe '#key_text' do + it 'says coverage by default' do + expect(template.key_text).to eq 'coverage' + end + + context 'when custom key_text is defined' do + before do + allow(badge).to receive(:customization).and_return({ key_text: "custom text" }) + end + + it 'returns custom value' do + expect(template.key_text).to eq "custom text" + end + + context 'when its size is larger than the max allowed value' do + before do + allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 }) + end + + it 'returns default value' do + expect(template.key_text).to eq 'coverage' + end + end + end + end + + describe '#value_text' do + context 'when coverage is known' do + it 'returns coverage percentage' do + expect(template.value_text).to eq '90.00%' + end + end + + context 'when coverage is known to many digits' do + before do + allow(badge).to receive(:status).and_return(92.349) + end + + it 'returns rounded coverage percentage' do + expect(template.value_text).to eq '92.35%' + end + end + + context 'when coverage is unknown' do + before do + allow(badge).to receive(:status).and_return(nil) + end + + it 'returns string that says coverage is unknown' do + expect(template.value_text).to eq 'unknown' + end + end + end + + describe '#key_width' do + it 'is fixed by default' do + expect(template.key_width).to eq 62 + end + + context 'when custom key_width is defined' do + before do + allow(badge).to receive(:customization).and_return({ key_width: 101 }) + end + + it 'returns custom value' do + expect(template.key_width).to eq 101 + end + + context 'when it is larger than the max allowed value' do + before do + allow(badge).to receive(:customization).and_return({ key_width: 513 }) + end + + it 'returns default value' do + expect(template.key_width).to eq 62 + end + end + end + end + + describe '#value_width' do + context 'when coverage is known' do + it 'is narrower when coverage is known' do + expect(template.value_width).to eq 54 + end + end + + context 'when coverage is unknown' do + before do + allow(badge).to receive(:status).and_return(nil) + end + + it 'is wider when coverage is unknown to fit text' do + expect(template.value_width).to eq 58 + end + end + end + + describe '#key_color' do + it 'always has the same color' do + expect(template.key_color).to eq '#555' + end + end + + describe '#value_color' do + context 'when coverage is good' do + before do + allow(badge).to receive(:status).and_return(98) + end + + it 'is green' do + expect(template.value_color).to eq '#4c1' + end + end + + context 'when coverage is acceptable' do + before do + allow(badge).to receive(:status).and_return(90) + end + + it 'is green-orange' do + expect(template.value_color).to eq '#a3c51c' + end + end + + context 'when coverage is medium' do + before do + allow(badge).to receive(:status).and_return(75) + end + + it 'is orange-yellow' do + expect(template.value_color).to eq '#dfb317' + end + end + + context 'when coverage is low' do + before do + allow(badge).to receive(:status).and_return(50) + end + + it 'is red' do + expect(template.value_color).to eq '#e05d44' + end + end + + context 'when coverage is unknown' do + before do + allow(badge).to receive(:status).and_return(nil) + end + + it 'is grey' do + expect(template.value_color).to eq '#9f9f9f' + end + end + end + + describe '#width' do + context 'when coverage is known' do + it 'returns the key width plus value width' do + expect(template.width).to eq 116 + end + end + + context 'when coverage is unknown' do + before do + allow(badge).to receive(:status).and_return(nil) + end + + it 'returns key width plus wider value width' do + expect(template.width).to eq 120 + end + end + end +end diff --git a/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb new file mode 100644 index 00000000000..2f677237fad --- /dev/null +++ b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'lib/gitlab/ci/badge/shared/metadata' + +RSpec.describe Gitlab::Ci::Badge::Pipeline::Metadata do + let(:badge) { double(project: create(:project), ref: 'feature') } + let(:metadata) { described_class.new(badge) } + + it_behaves_like 'badge metadata' + + describe '#title' do + it 'returns build status title' do + expect(metadata.title).to eq 'pipeline status' + end + end + + describe '#image_url' do + it 'returns valid url' do + expect(metadata.image_url).to include 'badges/feature/pipeline.svg' + end + end + + describe '#link_url' do + it 'returns valid link' do + expect(metadata.link_url).to include 'commits/feature' + end + end +end diff --git a/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb new file mode 100644 index 00000000000..45d0d781090 --- /dev/null +++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do + let(:project) { create(:project, :repository) } + let(:sha) { project.commit.sha } + let(:branch) { 'master' } + let(:badge) { described_class.new(project, branch) } + + describe '#entity' do + it 'always says pipeline' do + expect(badge.entity).to eq 'pipeline' + end + end + + describe '#template' do + it 'returns badge template' do + expect(badge.template.key_text).to eq 'pipeline' + end + end + + describe '#metadata' do + it 'returns badge metadata' do + expect(badge.metadata.image_url).to include 'badges/master/pipeline.svg' + end + end + + context 'pipeline exists', :sidekiq_might_not_need_inline do + let!(:pipeline) { create_pipeline(project, sha, branch) } + + context 'pipeline success' do + before do + pipeline.success! + end + + describe '#status' do + it 'is successful' do + expect(badge.status).to eq 'success' + end + end + end + + context 'pipeline failed' do + before do + pipeline.drop! + end + + describe '#status' do + it 'failed' do + expect(badge.status).to eq 'failed' + end + end + end + + context 'when outdated pipeline for given ref exists' do + before do + pipeline.success! + + old_pipeline = create_pipeline(project, '11eeffdd', branch) + old_pipeline.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' + end + end + + context 'when multiple pipelines exist for given sha' do + before do + pipeline.drop! + + new_pipeline = create_pipeline(project, sha, branch) + new_pipeline.success! + end + + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' + end + end + + context 'when ignored_skipped is set to true' do + let(:new_badge) { described_class.new(project, branch, opts: { ignore_skipped: true }) } + + before do + pipeline.skip! + end + + describe '#status' do + it 'uses latest non-skipped status' do + expect(new_badge.status).not_to eq 'skipped' + end + end + end + + context 'when ignored_skipped is set to false' do + let(:new_badge) { described_class.new(project, branch, opts: { ignore_skipped: false }) } + + before do + pipeline.skip! + end + + describe '#status' do + it 'uses latest status' do + expect(new_badge.status).to eq 'skipped' + end + end + end + end + + context 'build does not exist' do + describe '#status' do + it 'is unknown' do + expect(badge.status).to eq 'unknown' + end + end + end + + def create_pipeline(project, sha, branch) + pipeline = create(:ci_empty_pipeline, + project: project, + sha: sha, + ref: branch) + + create(:ci_build, pipeline: pipeline, stage: 'notify') + end +end diff --git a/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb new file mode 100644 index 00000000000..696bb62b4d6 --- /dev/null +++ b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do + let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) } + let(:template) { described_class.new(badge) } + + describe '#key_text' do + it 'says pipeline by default' do + expect(template.key_text).to eq 'pipeline' + end + + context 'when custom key_text is defined' do + before do + allow(badge).to receive(:customization).and_return({ key_text: 'custom text' }) + end + + it 'returns custom value' do + expect(template.key_text).to eq 'custom text' + end + + context 'when its size is larger than the max allowed value' do + before do + allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 }) + end + + it 'returns default value' do + expect(template.key_text).to eq 'pipeline' + end + end + end + end + + describe '#value_text' do + it 'is status value' do + expect(template.value_text).to eq 'passed' + end + end + + describe '#key_width' do + it 'is fixed by default' do + expect(template.key_width).to eq 62 + end + + context 'when custom key_width is defined' do + before do + allow(badge).to receive(:customization).and_return({ key_width: 101 }) + end + + it 'returns custom value' do + expect(template.key_width).to eq 101 + end + + context 'when it is larger than the max allowed value' do + before do + allow(badge).to receive(:customization).and_return({ key_width: 513 }) + end + + it 'returns default value' do + expect(template.key_width).to eq 62 + end + end + end + end + + describe 'widths and text anchors' do + it 'has fixed width and text anchors' do + expect(template.width).to eq 116 + expect(template.key_width).to eq 62 + expect(template.value_width).to eq 54 + expect(template.key_text_anchor).to eq 31 + expect(template.value_text_anchor).to eq 89 + end + end + + describe '#key_color' do + it 'is always the same' do + expect(template.key_color).to eq '#555' + end + end + + describe '#value_color' do + context 'when status is success' do + it 'has expected color' do + expect(template.value_color).to eq '#4c1' + end + end + + context 'when status is failed' do + before do + allow(badge).to receive(:status).and_return('failed') + end + + it 'has expected color' do + expect(template.value_color).to eq '#e05d44' + end + end + + context 'when status is running' do + before do + allow(badge).to receive(:status).and_return('running') + end + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + + context 'when status is preparing' do + before do + allow(badge).to receive(:status).and_return('preparing') + end + + it 'has expected color' do + expect(template.value_color).to eq '#a7a7a7' + end + end + + context 'when status is unknown' do + before do + allow(badge).to receive(:status).and_return('unknown') + end + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + + context 'when status does not match any known statuses' do + before do + allow(badge).to receive(:status).and_return('invalid') + end + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + end +end diff --git a/spec/lib/gitlab/ci/badge/shared/metadata.rb b/spec/lib/gitlab/ci/badge/shared/metadata.rb new file mode 100644 index 00000000000..c99a65bb2f4 --- /dev/null +++ b/spec/lib/gitlab/ci/badge/shared/metadata.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'badge metadata' do + describe '#to_html' do + let(:html) { Nokogiri::HTML.parse(metadata.to_html) } + let(:a_href) { html.at('a') } + + it 'points to link' do + expect(a_href[:href]).to eq metadata.link_url + end + + it 'contains clickable image' do + expect(a_href.children.first.name).to eq 'img' + end + end + + describe '#to_markdown' do + subject { metadata.to_markdown } + + it { is_expected.to include metadata.image_url } + it { is_expected.to include metadata.link_url } + end + + describe '#to_asciidoc' do + subject { metadata.to_asciidoc } + + it { is_expected.to include metadata.image_url } + it { is_expected.to include metadata.link_url } + it { is_expected.to include 'image:' } + it { is_expected.to include 'link=' } + it { is_expected.to include 'title=' } + end +end 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/gitlab_registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb new file mode 100644 index 00000000000..43913e91085 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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' } + + subject { described_class.new(build) } + + before do + stub_container_registry_config(host_port: registry_url) + end + + it 'contains valid DockerRegistry 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 registry_url + expect(subject.type).to eq 'registry' + end + + describe '.valid?' do + subject { described_class.new(build).valid? } + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it { is_expected.to be_truthy } + end + + context 'when registry is disabled' do + before do + stub_container_registry_config(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_spec.rb deleted file mode 100644 index c0a76973f60..00000000000 --- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Build::Credentials::Registry do - let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } - let(:registry_url) { 'registry.example.com:5005' } - - subject { described_class.new(build) } - - before do - stub_container_registry_config(host_port: registry_url) - end - - it 'contains valid DockerRegistry 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 registry_url - expect(subject.type).to eq 'registry' - end - - describe '.valid?' do - subject { described_class.new(build).valid? } - - context 'when registry is enabled' do - before do - stub_container_registry_config(enabled: true) - end - - it { is_expected.to be_truthy } - end - - context 'when registry is disabled' do - before do - stub_container_registry_config(enabled: false) - end - - it { is_expected.to be_falsey } - end - end -end 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/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/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/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/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/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/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/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/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/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 " | 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
when there are more than 10 items' do - items = ('a'..'k').to_a - - expect(helper.markdown_list(items)).to match(%r{
[^<]+
}) - 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) { 'Software Engineer in Test, 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..32619fc4c37 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 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/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( + 'Hel' \ + 'l' \ + "o \n Worl" \ + 'l' \ + 'd' + ) + 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/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..4ef8a75de4f 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -15,8 +15,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 +26,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 +36,10 @@ RSpec.describe Gitlab::Experimentation do }, test_experiment: { tracking_category: 'Team' + }, + tabular_experiment: { + tracking_category: 'Team', + rollout_strategy: rollout_strategy } }) @@ -46,6 +51,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 +181,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/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/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/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..2d616ec8862 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 @@ -561,6 +562,7 @@ project: - exported_protected_branches - incident_management_oncall_schedules - debian_distributions +- merge_request_metrics award_emoji: - awardable - user @@ -589,6 +591,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/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..48b76c4cdbf 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -37,7 +37,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) 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/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/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/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..158d472f7ea 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' diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index b99a5352717..856ae87c5bf 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -38,7 +38,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 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..b12a0310dac 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -9,11 +9,37 @@ 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 'default fields' do + 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 + + context 'development or test' do + include_examples 'contains environment', 'development' + end + + context 'staging' do + before do + allow(Gitlab).to receive(:staging?).and_return(true) + end + + include_examples 'contains environment', 'staging' + end + + context 'production' do + before do + allow(Gitlab).to receive(:com_and_canary?).and_return(true) + end + + include_examples 'contains environment', 'production' + end + end + + it 'contains source' do + expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE) end end @@ -28,8 +54,8 @@ RSpec.describe Gitlab::Tracking::StandardContext do context 'with namespace' do subject { described_class.new(namespace: namespace) } - it 'creates a Snowplow context using the given data' do - expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id) + it 'creates a Snowplow context without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end end @@ -37,18 +63,18 @@ RSpec.describe Gitlab::Tracking::StandardContext do context 'with project' do subject { described_class.new(project: project) } - 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) + it 'creates a Snowplow context without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil + expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end end context 'with project and namespace' do subject { described_class.new(namespace: namespace, project: project) } - 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 without namespace and project' do + expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil + expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil end 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..e62861cd677 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -0,0 +1,21 @@ +# 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) + + 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..ceb00867c95 --- /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 + :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..bc64bdfbf56 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -5,17 +5,13 @@ 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', status: 'data_available', default_generation: 'generation_1', - full_path: { - generation_1: 'uuid', - generation_2: 'license.uuid' - }, + key_path: 'uuid', group: 'group::product analytics', time_frame: 'none', data_source: 'database', @@ -44,12 +40,11 @@ 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 + :key_path | nil :group | nil :time_frame | nil :time_frame | '29d' @@ -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..a391872c030 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -0,0 +1,256 @@ +# 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' } + + around do |example| + # We need to freeze to a reference time + # because visits are grouped by the week number in the year + # Without freezing the time, the test may behave inconsistently + # depending on which day of the week test is run. + # Monday 6th of June + reference_time = Time.utc(2020, 6, 1) + travel_to(reference_time) { example.run } + end + + context 'aggregated_metrics_data' do + 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: 'event4', category: 'category2', aggregation: "weekly" } + ].map(&:with_indifferent_access) + end + + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).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_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 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) + + expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3) + 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', events: %w[event1_slot], operator: "SUM" }]) + end + + expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationOperator + 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', 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', events: %w[event1_slot], operator: "SUM" }]) + 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', events: %w[event1_slot], operator: "OR" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + end + end + end + + describe '.aggregated_metrics_weekly_data' do + subject(:aggregated_metrics_data) { described_class.new.weekly_data } + + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 3.days.ago) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 8.days.ago) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity4, time: 2.days.ago) + end + + it_behaves_like 'aggregated_metrics_data' + end + + describe '.aggregated_metrics_monthly_data' do + subject(:aggregated_metrics_data) { described_class.new.monthly_data } + + it_behaves_like 'aggregated_metrics_data' do + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 10.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1)) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1)) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.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 + + it 'caches intermediate operations' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + + aggregated_metrics[0][:events].each do |event| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: event, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + + 2.upto(4) do |subset_size| + aggregated_metrics[0][:events].combination(subset_size).each do |events| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + end + + aggregated_metrics_data + end + end + 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..6ca7039b3de 100644 --- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'aggregated metrics' do Gitlab::UsageDataCounters::HLLRedisCounter.known_events end - Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics.tap do |aggregated_metrics| + Gitlab::Usage::Metrics::Aggregates::Aggregate.new.send(:aggregated_metrics).tap do |aggregated_metrics| it 'all events has unique name' do event_names = aggregated_metrics&.map { |event| event[:name] } @@ -37,7 +37,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/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index b8eddc0ca7f..f0b8ce6c2fb 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 @@ -39,7 +39,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'snippets', 'code_review', 'terraform', - 'ci_templates' + 'ci_templates', + 'quickactions', + 'pipeline_authoring' ) end end @@ -425,182 +427,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..509ba43ef32 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,22 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl 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_create_comment_action' do subject { described_class.track_create_comment_action(note: note) } @@ -148,4 +164,36 @@ 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_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 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_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..27248d1d95a 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 @@ -362,4 +372,97 @@ RSpec.describe Gitlab::Utils::UsageData do end end end + + describe '#save_aggregated_metrics', :clean_gitlab_redis_shared_state do + let(:timestamp) { Time.current.to_i } + let(:time_period) { { created_at: 7.days.ago..Date.current } } + let(:metric_name) { 'test_metric' } + let(:method_params) do + { + metric_name: metric_name, + time_period: time_period, + recorded_at_timestamp: timestamp, + 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 + time_period_name = 'weekly' + + expect(described_class).to receive(:time_period_to_human_name).with(time_period).and_return(time_period_name) + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with("#{metric_name}_#{time_period_name}-#{timestamp}", '{"141":1,"56":1}', ex: 80.hours) + end + + described_class.save_aggregated_metrics(**method_params) + 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 { described_class.save_aggregated_metrics(**method_params) }.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 { described_class.save_aggregated_metrics(**method_params) }.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 + + described_class.save_aggregated_metrics(**method_params) + end + end + + it 'raises error for development environment' do + expect { described_class.save_aggregated_metrics(**method_params) }.to raise_error /Unsupported data type/ + end + end + end + + describe '#time_period_to_human_name' do + it 'translates empty time period as all_time' do + expect(described_class.time_period_to_human_name({})).to eql 'all_time' + end + + it 'translates time period not longer than 7 days as weekly', :aggregate_failures do + days_6_time_period = 6.days.ago..Date.current + days_7_time_period = 7.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_6_time_period)).to eql 'weekly' + expect(described_class.time_period_to_human_name(column_name: days_7_time_period)).to eql 'weekly' + end + + it 'translates time period longer than 7 days as monthly', :aggregate_failures do + days_8_time_period = 8.days.ago..Date.current + days_31_time_period = 31.days.ago..Date.current + + expect(described_class.time_period_to_human_name(column_name: days_8_time_period)).to eql 'monthly' + expect(described_class.time_period_to_human_name(column_name: days_31_time_period)).to eql 'monthly' + end + end end 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" -- cgit v1.2.3