diff options
Diffstat (limited to 'spec/lib/gitlab')
197 files changed, 4737 insertions, 4092 deletions
diff --git a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb deleted file mode 100644 index d7184c89933..00000000000 --- a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb +++ /dev/null @@ -1,153 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do - let_it_be(:project) { create(:project) } - - let(:raw_payload) { {} } - - let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } - - it_behaves_like 'subclass has expected api' - - shared_context 'with gitlab alert' do - let_it_be(:gitlab_alert) { create(:prometheus_alert, project: project) } - let(:metric_id) { gitlab_alert.prometheus_metric_id.to_s } - let(:alert_id) { gitlab_alert.id.to_s } - end - - describe '#metric_id' do - subject { parsed_payload.metric_id } - - it { is_expected.to be_nil } - - context 'with gitlab_alert_id' do - let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => '12' } } } - - it { is_expected.to eq(12) } - end - end - - describe '#gitlab_prometheus_alert_id' do - subject { parsed_payload.gitlab_prometheus_alert_id } - - it { is_expected.to be_nil } - - context 'with gitlab_alert_id' do - let(:raw_payload) { { 'labels' => { 'gitlab_prometheus_alert_id' => '12' } } } - - it { is_expected.to eq(12) } - end - end - - describe '#gitlab_alert' do - subject { parsed_payload.gitlab_alert } - - context 'without alert info in payload' do - it { is_expected.to be_nil } - end - - context 'with metric id in payload' do - let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } - let(:metric_id) { '-1' } - - context 'without matching alert' do - it { is_expected.to be_nil } - end - - context 'with matching alert' do - include_context 'with gitlab alert' - - it { is_expected.to eq(gitlab_alert) } - - context 'when unclear which alert applies' do - # With multiple alerts for different environments, - # we can't be sure which prometheus alert the payload - # belongs to - let_it_be(:another_alert) do - create(:prometheus_alert, - prometheus_metric: gitlab_alert.prometheus_metric, - project: project) - end - - it { is_expected.to be_nil } - end - end - end - - context 'with alert id' do - # gitlab_prometheus_alert_id is a stronger identifier, - # but was added after gitlab_alert_id; we won't - # see it without gitlab_alert_id also present - let(:raw_payload) do - { - 'labels' => { - 'gitlab_alert_id' => metric_id, - 'gitlab_prometheus_alert_id' => alert_id - } - } - end - - context 'without matching alert' do - let(:alert_id) { '-1' } - let(:metric_id) { '-1' } - - it { is_expected.to be_nil } - end - - context 'with matching alerts' do - include_context 'with gitlab alert' - - it { is_expected.to eq(gitlab_alert) } - end - end - end - - describe '#full_query' do - subject { parsed_payload.full_query } - - it { is_expected.to be_nil } - - context 'with gitlab alert' do - include_context 'with gitlab alert' - - let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } - - it { is_expected.to eq(gitlab_alert.full_query) } - end - - context 'with sufficient fallback info' do - let(:raw_payload) { { 'generatorURL' => 'http://localhost:9090/graph?g0.expr=vector%281%29' } } - - it { is_expected.to eq('vector(1)') } - end - end - - describe '#environment' do - subject { parsed_payload.environment } - - context 'with gitlab alert' do - include_context 'with gitlab alert' - - let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } - - it { is_expected.to eq(gitlab_alert.environment) } - end - - context 'with sufficient fallback info' do - let_it_be(:environment) { create(:environment, project: project, name: 'production') } - - let(:raw_payload) do - { - 'labels' => { - 'gitlab_alert_id' => '-1', - 'gitlab_environment_name' => 'production' - } - } - end - - it { is_expected.to eq(environment) } - end - end -end diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb index efde7ed3772..fe14e6ae53c 100644 --- a/spec/lib/gitlab/alert_management/payload_spec.rb +++ b/spec/lib/gitlab/alert_management/payload_spec.rb @@ -19,12 +19,6 @@ RSpec.describe Gitlab::AlertManagement::Payload do let(:payload) { { 'monitoring_tool' => 'Prometheus' } } it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus } - - context 'with gitlab-managed attributes' do - let(:payload) { { 'monitoring_tool' => 'Prometheus', 'labels' => { 'gitlab_alert_id' => '12' } } } - - it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus } - end end context 'with the payload specifying an unknown tool' do @@ -43,12 +37,6 @@ RSpec.describe Gitlab::AlertManagement::Payload do context 'with an externally managed prometheus payload' do it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus } end - - context 'with a self-managed prometheus payload' do - let(:payload) { { 'labels' => { 'gitlab_alert_id' => '14' } } } - - it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus } - end end context 'as an unknown tool' do diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb index e9a9dfeca82..276f797536b 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb @@ -117,7 +117,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do }) end - before(:all) do + before_all do issue1.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) issue2.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) issue3.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago) diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb index 24248c557bd..df0e4fb92a0 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent, feature_category: :product_analytics do +RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent, feature_category: :product_analytics_data_management do let(:instance) { described_class.new({}) } it { expect(described_class).to respond_to(:name) } diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb index 386d4157e90..bde72a656b8 100644 --- a/spec/lib/gitlab/audit/auditor_spec.rb +++ b/spec/lib/gitlab/audit/auditor_spec.rb @@ -25,35 +25,33 @@ RSpec.describe Gitlab::Audit::Auditor, feature_category: :audit_events do describe '.audit' do let(:audit!) { auditor.audit(context) } + before do + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_call_original + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).with(name).and_return(true) + end + context 'when yaml definition is not defined' do before do - allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_return(false) - allow(Gitlab::AppLogger).to receive(:warn).and_return(app_logger) + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_call_original + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).with(name).and_return(false) end - it 'logs a warning when YAML is not defined' do - expected_warning = { - message: 'Logging audit events without an event type definition will be deprecated soon ' \ - '(https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions)', - event_type: name - } - - audit! + it 'raises an error' do + expected_error = "Audit event type YML file is not defined for audit_operation. " \ + "Please read https://docs.gitlab.com/ee/development/audit_event_guide/" \ + "#how-to-instrument-new-audit-events for adding a new audit event" - expect(Gitlab::AppLogger).to have_received(:warn).with(expected_warning) + expect { audit! }.to raise_error(StandardError, expected_error) end end context 'when yaml definition is defined' do before do allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_return(true) - allow(Gitlab::AppLogger).to receive(:warn).and_return(app_logger) end - it 'does not log a warning when YAML is defined' do - audit! - - expect(Gitlab::AppLogger).not_to have_received(:warn) + it 'does not raise an error' do + expect { audit! }.not_to raise_error end end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 1a1e165c50a..b0ec46a3a0e 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -516,17 +516,23 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do set_bearer_token(token_3.token) end - it 'revokes the latest rotated token' do - expect(token_1).not_to be_revoked + context 'with url related to access tokens' do + before do + set_header('SCRIPT_NAME', "/personal_access_tokens/#{token_3.id}/rotate") + end + + it 'revokes the latest rotated token' do + expect(token_1).not_to be_revoked - expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError) + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError) - expect(token_1.reload).to be_revoked + expect(token_1.reload).to be_revoked + end end - context 'when the feature flag is disabled' do + context 'with url not related to access tokens' do before do - stub_feature_flags(pat_reuse_detection: false) + set_header('SCRIPT_NAME', '/epics/1') end it 'does not revoke the latest rotated token' do diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb index f1fad946f35..5286e22abc9 100644 --- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb @@ -40,6 +40,32 @@ RSpec.describe Gitlab::Auth::Saml::AuthHash do end end + describe '#azure_group_overage_claim?' do + context 'when the claim is not present' do + let(:raw_info_attr) { {} } + + it 'is false' do + expect(saml_auth_hash.azure_group_overage_claim?).to eq(false) + end + end + + context 'when the claim is present' do + # The value of the claim is irrelevant, but it's still included + # in the test response to keep tests as real-world as possible. + # https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles#group-overages + let(:raw_info_attr) do + { + 'http://schemas.microsoft.com/claims/groups.link' => + ['https://graph.windows.net/8c750e43/users/e631c82c/getMemberObjects'] + } + end + + it 'is true' do + expect(saml_auth_hash.azure_group_overage_claim?).to eq(true) + end + end + end + describe '#authn_context' do let(:auth_hash_data) do { diff --git a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb index 876c23a91bd..e0ef45d5621 100644 --- a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb +++ b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb @@ -5,10 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do using RSpec::Parameterized::TableSyntax - subject(:verifier) { described_class.new(user) } + let(:request) { instance_double(ActionDispatch::Request, session: session) } + let(:session) { {} } let(:user) { build_stubbed(:user, otp_grace_period_started_at: Time.zone.now) } + subject(:verifier) { described_class.new(user, request) } + describe '#two_factor_authentication_enforced?' do subject { verifier.two_factor_authentication_enforced? } @@ -34,25 +37,69 @@ RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do describe '#two_factor_authentication_required?' do subject { verifier.two_factor_authentication_required? } - where(:instance_level_enabled, :group_level_enabled, :should_be_required) do - true | false | true - false | true | true - false | false | false + where(:instance_level_enabled, :group_level_enabled, :should_be_required, :provider_2FA) do + true | false | true | false + false | true | false | true + false | true | true | false + false | false | false | true end with_them do before do stub_application_setting(require_two_factor_authentication: instance_level_enabled) allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled) + session[:provider_2FA] = provider_2FA end it { is_expected.to eq(should_be_required) } end + + context 'when feature by_pass_two_factor_for_current_session is disabled' do + where(:instance_level_enabled, :group_level_enabled, :should_be_required, :provider_2FA) do + true | false | true | false + false | true | true | true + false | false | false | true + end + + with_them do + before do + allow(request).to receive(:session).and_return(session) + stub_feature_flags(by_pass_two_factor_for_current_session: false) + stub_application_setting(require_two_factor_authentication: instance_level_enabled) + allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled) + session[:provider_2FA] = provider_2FA + end + + it { is_expected.to eq(should_be_required) } + end + end + + context 'when request is nil' do + let(:request) { nil } + + where(:instance_level_enabled, :group_level_enabled, :should_be_required, :provider_2FA) do + true | false | true | false + false | true | true | true + false | false | false | true + end + + with_them do + before do + allow(request).to receive(:session).and_return(session) + stub_feature_flags(bypass_two_factor: false) + stub_application_setting(require_two_factor_authentication: instance_level_enabled) + allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled) + session[:provider_2FA] = provider_2FA + end + + it { is_expected.to eq(should_be_required) } + end + end end describe '#current_user_needs_to_setup_two_factor?' do it 'returns false when current_user is nil' do - expect(described_class.new(nil).current_user_needs_to_setup_two_factor?).to be_falsey + expect(described_class.new(nil, request).current_user_needs_to_setup_two_factor?).to be_falsey end it 'returns false when current_user does not have temp email' do diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb index c959c5d80b2..65cde195a61 100644 --- a/spec/lib/gitlab/avatar_cache_spec.rb +++ b/spec/lib/gitlab/avatar_cache_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do it "finds the cached value in the request store and doesn't execute the block" do expect(thing).to receive(:avatar_path).once - Gitlab::WithRequestStore.with_request_store do + Gitlab::SafeRequestStore.ensure_request_store do described_class.by_email("foo@bar.com", 20, 2, true) do thing.avatar_path end diff --git a/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb new file mode 100644 index 00000000000..62c9e240b7a --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting, + schema: 20230724071541, + feature_category: :database do + let(:namespaces_table) { table(:namespaces) } + let(:namespace_settings_table) { table(:namespace_settings) } + + subject(:perform_migration) do + described_class.new( + start_id: 1, + end_id: 30, + batch_table: :namespace_settings, + batch_column: :namespace_id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ).perform + end + + before do + namespaces_table.create!(id: 1, name: 'group_namespace', path: 'path-1', type: 'Group', + default_branch_protection: 0) + namespaces_table.create!(id: 2, name: 'user_namespace', path: 'path-2', type: 'User', default_branch_protection: 1) + namespaces_table.create!(id: 3, name: 'user_three_namespace', path: 'path-3', type: 'User', + default_branch_protection: 2) + namespaces_table.create!(id: 4, name: 'group_four_namespace', path: 'path-4', type: 'Group', + default_branch_protection: 3) + namespaces_table.create!(id: 5, name: 'group_five_namespace', path: 'path-5', type: 'Group', + default_branch_protection: 4) + + namespace_settings_table.create!(namespace_id: 1, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 2, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 3, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 4, default_branch_protection_defaults: {}) + namespace_settings_table.create!(namespace_id: 5, default_branch_protection_defaults: {}) + end + + it 'updates default_branch_protection_defaults to a correct value', :aggregate_failures do + expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(16) + + expect(migrated_attribute(1)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(2)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 30 }] }) + expect(migrated_attribute(3)).to eq({ "allow_force_push" => false, + "allowed_to_merge" => [{ "access_level" => 40 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(4)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 40 }] }) + expect(migrated_attribute(5)).to eq({ "allow_force_push" => true, + "allowed_to_merge" => [{ "access_level" => 30 }], + "allowed_to_push" => [{ "access_level" => 40 }], + "developer_can_initial_push" => true }) + end + + def migrated_attribute(namespace_id) + namespace_settings_table.find(namespace_id).default_branch_protection_defaults + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb new file mode 100644 index 00000000000..c85636f4998 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob, + schema: 20230719083202, + feature_category: :consumables_cost_management do + include MigrationHelpers::ProjectStatisticsHelper + + include_context 'when backfilling project statistics' + + let(:default_pipeline_artifacts_size) { 5 } + let(:default_stats) do + { + repository_size: 1, + wiki_size: 1, + lfs_objects_size: 1, + build_artifacts_size: 1, + packages_size: 1, + snippets_size: 1, + uploads_size: 1, + pipeline_artifacts_size: default_pipeline_artifacts_size, + storage_size: default_storage_size + } + end + + describe '#filter_batch' do + it 'filters out project_statistics with no artifacts size' do + project_statistics = generate_records(default_projects, project_statistics_table, default_stats) + project_statistics_table.create!( + project_id: proj5.id, + namespace_id: proj5.namespace_id, + repository_size: 1, + wiki_size: 1, + lfs_objects_size: 1, + build_artifacts_size: 1, + packages_size: 1, + snippets_size: 1, + pipeline_artifacts_size: 0, + uploads_size: 1, + storage_size: 7 + ) + + expected = project_statistics.map(&:id) + actual = migration.filter_batch(project_statistics_table).pluck(:id) + + expect(actual).to match_array(expected) + end + end + + describe '#perform' do + subject(:perform_migration) { migration.perform } + + context 'when project_statistics backfill runs' do + before do + generate_records(default_projects, project_statistics_table, default_stats) + end + + context 'when storage_size includes pipeline_artifacts_size' do + it 'removes pipeline_artifacts_size from storage_size' do + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array([default_storage_size]) + + perform_migration + + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array( + [default_storage_size - default_pipeline_artifacts_size] + ) + expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(4).times + end + end + + context 'when storage_size does not include default_pipeline_artifacts_size' do + it 'does not update the record' do + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + proj_stat = project_statistics_table.last + expect(proj_stat.storage_size).to eq(default_storage_size) + proj_stat.storage_size = default_storage_size - default_pipeline_artifacts_size + proj_stat.save! + + perform_migration + + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array( + [default_storage_size - default_pipeline_artifacts_size] + ) + expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(3).times + end + end + end + + it 'coerces a null wiki_size to 0' do + project_statistics = create_project_stats(projects, namespaces, default_stats, { wiki_size: nil }) + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + migration = create_migration(end_id: project_statistics.project_id) + + migration.perform + + project_statistics.reload + expect(project_statistics.storage_size).to eq(6) + end + + it 'coerces a null snippets_size to 0' do + project_statistics = create_project_stats(projects, namespaces, default_stats, { snippets_size: nil }) + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + migration = create_migration(end_id: project_statistics.project_id) + + migration.perform + + project_statistics.reload + expect(project_statistics.storage_size).to eq(6) + end + end +end diff --git a/spec/lib/gitlab/background_migration/fix_allow_descendants_override_disabled_shared_runners_spec.rb b/spec/lib/gitlab/background_migration/fix_allow_descendants_override_disabled_shared_runners_spec.rb new file mode 100644 index 00000000000..5f5dcb35836 --- /dev/null +++ b/spec/lib/gitlab/background_migration/fix_allow_descendants_override_disabled_shared_runners_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::FixAllowDescendantsOverrideDisabledSharedRunners, schema: 20230802085923, feature_category: :runner_fleet do # rubocop:disable Layout/LineLength + let(:namespaces) { table(:namespaces) } + + let!(:valid_enabled) do + namespaces.create!(name: 'valid_enabled', path: 'valid_enabled', + shared_runners_enabled: true, + allow_descendants_override_disabled_shared_runners: false) + end + + let!(:invalid_enabled) do + namespaces.create!(name: 'invalid_enabled', path: 'invalid_enabled', + shared_runners_enabled: true, + allow_descendants_override_disabled_shared_runners: true) + end + + let!(:disabled_and_overridable) do + namespaces.create!(name: 'disabled_and_overridable', path: 'disabled_and_overridable', + shared_runners_enabled: false, + allow_descendants_override_disabled_shared_runners: true) + end + + let!(:disabled_and_unoverridable) do + namespaces.create!(name: 'disabled_and_unoverridable', path: 'disabled_and_unoverridable', + shared_runners_enabled: false, + allow_descendants_override_disabled_shared_runners: false) + end + + let(:migration_attrs) do + { + start_id: namespaces.minimum(:id), + end_id: namespaces.maximum(:id), + batch_table: :namespaces, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ApplicationRecord.connection + } + end + + it 'fixes invalid allow_descendants_override_disabled_shared_runners and does not affect others' do + expect do + described_class.new(**migration_attrs).perform + end.to change { invalid_enabled.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false) + .and not_change { valid_enabled.reload.allow_descendants_override_disabled_shared_runners }.from(false) + .and not_change { disabled_and_overridable.reload.allow_descendants_override_disabled_shared_runners }.from(true) + .and not_change { disabled_and_unoverridable.reload.allow_descendants_override_disabled_shared_runners } + .from(false) + end +end diff --git a/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb index e3b1b67cb40..c52d1b4c9f2 100644 --- a/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb +++ b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb @@ -26,7 +26,16 @@ RSpec.describe Gitlab::BackgroundMigration::Redis::BackfillProjectPipelineStatus describe '#scan_match_pattern' do it "finds all the required keys only" do - expect(redis.scan('0').second).to match_array(keys + invalid_keys) + cursor = '0' + scanned = [] + loop do + # multiple scans are performed if it is a Redis cluster + cursor, result = redis.scan(cursor) + scanned.concat(result) + break if cursor == '0' + end + + expect(scanned).to match_array(keys + invalid_keys) expect(subject.redis.scan_each(match: subject.scan_match_pattern).to_a).to contain_exactly(*keys) end end diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb index 582c0fe1b1b..af8b5240e40 100644 --- a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt describe '#perform' do let(:job_artifact) { table(:ci_job_artifacts, database: :ci) } + let(:jobs) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } } let(:test_worker) do described_class.new( @@ -85,7 +86,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt private def create_job_artifact(id:, file_type:, expire_at:) - job = table(:ci_builds, database: :ci).create!(id: id, partition_id: 100) + job = jobs.create!(partition_id: 100) job_artifact.create!( id: id, job_id: job.id, expire_at: expire_at, project_id: project.id, file_type: file_type, partition_id: 100 diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb index f636ce283ae..bfe2b7d1360 100644 --- a/spec/lib/gitlab/blame_spec.rb +++ b/spec/lib/gitlab/blame_spec.rb @@ -33,12 +33,18 @@ RSpec.describe Gitlab::Blame do expect(subject.count).to eq(18) expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) + expect(subject[0][:span]).to eq(3) + expect(subject[0][:lineno]).to eq(1) expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') expect(subject[1][:lines]).to eq(["module Popen", " extend self"]) + expect(subject[1][:span]).to eq(2) + expect(subject[1][:lineno]).to eq(4) expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') expect(subject[-1][:lines]).to eq([" end", "end"]) + expect(subject[-1][:span]).to eq(2) + expect(subject[-1][:lineno]).to eq(36) end context 'with a range 1..5' do diff --git a/spec/lib/gitlab/cache/json_cache_spec.rb b/spec/lib/gitlab/cache/json_cache_spec.rb index 05126319ef9..1904e42f937 100644 --- a/spec/lib/gitlab/cache/json_cache_spec.rb +++ b/spec/lib/gitlab/cache/json_cache_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Cache::JsonCache, feature_category: :shared do describe '#active?' do context 'when backend respond to active? method' do it 'delegates to the underlying cache implementation' do - backend = instance_double(Gitlab::NullRequestStore, active?: false) + backend = instance_double(Gitlab::SafeRequestStore::NullStore, active?: false) cache = described_class.new(namespace: namespace, backend: backend) diff --git a/spec/lib/gitlab/cache/json_caches/json_keyed_spec.rb b/spec/lib/gitlab/cache/json_caches/json_keyed_spec.rb index c4ec393c3ac..8afd5c2bfcd 100644 --- a/spec/lib/gitlab/cache/json_caches/json_keyed_spec.rb +++ b/spec/lib/gitlab/cache/json_caches/json_keyed_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Gitlab::Cache::JsonCaches::JsonKeyed, feature_category: :shared d current_cache = { '_other_revision_' => '_other_value_' }.merge(nested_cache_result).to_json allow(backend).to receive(:read).with(expanded_key).and_return(current_cache) - expect(cache.read(key, BroadcastMessage)).to eq(broadcast_message) + expect(cache.read(key, System::BroadcastMessage)).to eq(broadcast_message) end end end diff --git a/spec/lib/gitlab/cache/json_caches/redis_keyed_spec.rb b/spec/lib/gitlab/cache/json_caches/redis_keyed_spec.rb index 6e98cdd74ce..f408bbf8d25 100644 --- a/spec/lib/gitlab/cache/json_caches/redis_keyed_spec.rb +++ b/spec/lib/gitlab/cache/json_caches/redis_keyed_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Gitlab::Cache::JsonCaches::RedisKeyed, feature_category: :shared allow(backend).to receive(:read).with(expanded_key).and_return(true) expect(Gitlab::Json).to receive(:parse).with("true").and_call_original - expect(cache.read(key, BroadcastMessage)).to eq(true) + expect(cache.read(key, System::BroadcastMessage)).to eq(true) end end @@ -30,7 +30,7 @@ RSpec.describe Gitlab::Cache::JsonCaches::RedisKeyed, feature_category: :shared allow(backend).to receive(:read).with(expanded_key).and_return(false) expect(Gitlab::Json).to receive(:parse).with("false").and_call_original - expect(cache.read(key, BroadcastMessage)).to eq(false) + expect(cache.read(key, System::BroadcastMessage)).to eq(false) end end end diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 9950d4dbd12..c3d6b9510e5 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -38,6 +38,18 @@ RSpec.describe Gitlab::Checks::BranchCheck, feature_category: :source_code_manag expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") end + it "prohibits 64-character hexadecimal branch names" do + allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") + end + + it "prohibits 64-character hexadecimal branch names as the start of a path" do + allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175/test") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") + end + it "doesn't prohibit a nested hexadecimal in a branch name" do allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix") diff --git a/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb b/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb deleted file mode 100644 index 3b52d2e1364..00000000000 --- a/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs, feature_category: :source_code_management do - subject { checker.find } - - let_it_be(:project) { create(:project, :public, :repository) } - let(:checker) do - described_class.new( - project: project, - changes: changes, - file_size_limit_megabytes: 1) - end - - describe '#find' do - let(:branch_name) { SecureRandom.uuid } - let(:other_branch_name) { SecureRandom.uuid } - let(:filename) { 'log.log' } - let(:create_file) do - project.repository.create_file( - project.owner, - filename, - initial_contents, - branch_name: branch_name, - message: 'whatever' - ) - end - - let(:changed_ref) do - project.repository.update_file( - project.owner, - filename, - changed_contents, - branch_name: other_branch_name, - start_branch_name: branch_name, - message: 'whatever' - ) - end - - let(:changes) { [oldrev: create_file, newrev: changed_ref] } - - before do - # set up a branch - create_file - - # branch off that branch - changed_ref - - # delete stuff so it can be picked up by new_blobs - project.repository.delete_branch(other_branch_name) - end - - context 'when changing from valid to oversized' do - let(:initial_contents) { 'a' } - let(:changed_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte - - it 'returns an array with blobs that became oversized' do - blob = subject.first - expect(blob.path).to eq(filename) - expect(subject).to contain_exactly(blob) - end - end - - context 'when changing from oversized to oversized' do - let(:initial_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte - let(:changed_contents) { 'a' * ((2**20) + 2) } # 1 MB + 1 byte - - it { is_expected.to be_blank } - end - - context 'when changing from oversized to valid' do - let(:initial_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte - let(:changed_contents) { 'aa' } - - it { is_expected.to be_blank } - end - - context 'when changing from valid to valid' do - let(:initial_contents) { 'abc' } - let(:changed_contents) { 'def' } - - it { is_expected.to be_blank } - end - end -end diff --git a/spec/lib/gitlab/checks/file_size_check/hook_environment_aware_any_oversized_blobs_spec.rb b/spec/lib/gitlab/checks/file_size_check/hook_environment_aware_any_oversized_blobs_spec.rb new file mode 100644 index 00000000000..bea0c02cfb8 --- /dev/null +++ b/spec/lib/gitlab/checks/file_size_check/hook_environment_aware_any_oversized_blobs_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBlobs, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :small_repo) } + let(:repository) { project.repository } + let(:file_size_limit) { 1 } + let(:any_quarantined_blobs) do + described_class.new( + project: project, + changes: changes, + file_size_limit_megabytes: file_size_limit) + end + + let(:changes) { [{ newrev: 'master' }] } + + describe '#find' do + subject { any_quarantined_blobs.find } + + let(:stubbed_result) { 'stubbed' } + + it 'returns the result from AnyOversizedBlobs' do + expect_next_instance_of(Gitlab::Checks::FileSizeCheck::AnyOversizedBlobs) do |instance| + expect(instance).to receive(:find).and_return(stubbed_result) + end + + expect(subject).to eq(stubbed_result) + end + + context 'with hook env' do + context 'with hook environment' do + let(:git_env) do + { + 'GIT_OBJECT_DIRECTORY_RELATIVE' => "objects", + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['/dir/one', '/dir/two'] + } + end + + before do + allow(Gitlab::Git::HookEnv).to receive(:all).with(repository.gl_repository).and_return(git_env) + end + + it 'returns an emtpy array' do + expect(subject).to eq([]) + end + + context 'when the file is over the limit' do + let(:file_size_limit) { 0 } + + context 'when the blob does not exist in the repo' do + before do + allow(repository.gitaly_commit_client).to receive(:object_existence_map).and_return(Hash.new { false }) + end + + it 'returns an array with the blobs that are over the limit' do + expect(subject.size).to eq(1) + expect(subject.first).to be_kind_of(Gitlab::Git::Blob) + end + end + + context 'when the blob exists in the repo' do + before do + allow(repository.gitaly_commit_client).to receive(:object_existence_map).and_return(Hash.new { true }) + end + + it 'filters out the blobs in the repo' do + expect(subject).to eq([]) + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/checks/global_file_size_check_spec.rb b/spec/lib/gitlab/checks/global_file_size_check_spec.rb index 9ea0c73b1c7..a2b3ee0f761 100644 --- a/spec/lib/gitlab/checks/global_file_size_check_spec.rb +++ b/spec/lib/gitlab/checks/global_file_size_check_spec.rb @@ -14,13 +14,13 @@ RSpec.describe Gitlab::Checks::GlobalFileSizeCheck, feature_category: :source_co it 'does not log' do expect(subject).not_to receive(:log_timed) expect(Gitlab::AppJsonLogger).not_to receive(:info) - expect(Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs).not_to receive(:new) + expect(Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBlobs).not_to receive(:new) subject.validate! end end it 'checks for file sizes' do - expect_next_instance_of(Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs, + expect_next_instance_of(Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBlobs, project: project, changes: changes, file_size_limit_megabytes: 100 @@ -32,5 +32,35 @@ RSpec.describe Gitlab::Checks::GlobalFileSizeCheck, feature_category: :source_co expect(Gitlab::AppJsonLogger).to receive(:info).with('Checking for blobs over the file size limit') subject.validate! end + + context 'when there are oversized blobs' do + let(:blob_double) { instance_double(Gitlab::Git::Blob, size: 10) } + + before do + allow_next_instance_of(Gitlab::Checks::FileSizeCheck::HookEnvironmentAwareAnyOversizedBlobs, + project: project, + changes: changes, + file_size_limit_megabytes: 100 + ) do |check| + allow(check).to receive(:find).and_return([blob_double]) + end + end + + it 'logs a message with blob size and raises an exception' do + expect(Gitlab::AppJsonLogger).to receive(:info).with('Checking for blobs over the file size limit') + expect(Gitlab::AppJsonLogger).to receive(:info).with(message: 'Found blob over global limit', blob_sizes: [10]) + expect { subject.validate! }.to raise_exception(Gitlab::GitAccess::ForbiddenError) + end + + context 'when the enforce_global_file_size_limit feature flag is disabled' do + before do + stub_feature_flags(enforce_global_file_size_limit: false) + end + + it 'does not raise an exception' do + expect { subject.validate! }.not_to raise_error + end + end + end end end diff --git a/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb index ef39a431d63..47d91e2478e 100644 --- a/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator, feature let(:gzip_valid?) { true } let(:validator) { instance_double(::Gitlab::Ci::DecompressedGzipSizeValidator, valid?: gzip_valid?) } - before(:all) do + before_all do Zlib::GzipWriter.open(file_path) do |gz| gz.write('Hello World!') end diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 511036efd37..f4bc706f9b4 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now) end - before(:all) do + before_all do # Previous release create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now - 1.day) end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb index 10c1d92e209..dd15b049b9b 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb @@ -1,117 +1,132 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule, feature_category: :pipeline_composition do let(:factory) do - Gitlab::Config::Entry::Factory.new(described_class) - .value(config) + Gitlab::Config::Entry::Factory.new(described_class).value(config) end subject(:entry) { factory.create! } - describe '.new' do - shared_examples 'an invalid config' do |error_message| - it { is_expected.not_to be_valid } + before do + entry.compose! + end + + shared_examples 'a valid config' do + it { is_expected.to be_valid } + + it 'returns the expected value' do + expect(entry.value).to eq(config.compact) + end - it 'has errors' do - expect(entry.errors).to include(error_message) + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) end + + it 'returns the expected value' do + expect(entry.value).to eq(config) + end + end + end + + shared_examples 'an invalid config' do |error_message| + it { is_expected.not_to be_valid } + + it 'has errors' do + expect(entry.errors).to include(error_message) end + end - context 'when specifying an if: clause' do - let(:config) { { if: '$THIS || $THAT' } } + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT' } } - it { is_expected.to be_valid } + it_behaves_like 'a valid config' - context 'with when:' do - let(:config) { { if: '$THIS || $THAT', when: 'never' } } + context 'with when:' do + let(:config) { { if: '$THIS || $THAT', when: 'never' } } - it { is_expected.to be_valid } - end + it_behaves_like 'a valid config' end - context 'when specifying an exists: clause' do - let(:config) { { exists: './this.md' } } + context 'with when: <invalid string>' do + let(:config) { { if: '$THIS || $THAT', when: 'on_success' } } - it { is_expected.to be_valid } + it_behaves_like 'an invalid config', /when unknown value: on_success/ end - context 'using a list of multiple expressions' do - let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } + context 'with when: null' do + let(:config) { { if: '$THIS || $THAT', when: nil } } - it_behaves_like 'an invalid config', /invalid expression syntax/ + it_behaves_like 'a valid config' end - context 'when specifying an invalid if: clause expression' do - let(:config) { { if: ['$MY_VAR =='] } } + context 'when if: clause is invalid' do + let(:config) { { if: '$MY_VAR ==' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when specifying an if: clause expression with an invalid token' do - let(:config) { { if: ['$MY_VAR == 123'] } } + context 'when if: clause has an integer operand' do + let(:config) { { if: '$MY_VAR == 123' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when using invalid regex in an if: clause' do - let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } } + context 'when if: clause has invalid regex' do + let(:config) { { if: '$MY_VAR =~ /some ( thing/' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when using an if: clause with lookahead regex character "?"' do + context 'when if: clause has lookahead regex character "?"' do let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when specifying unknown policy' do - let(:config) { { invalid: :something } } + context 'when if: clause has array of expressions' do + let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } - it_behaves_like 'an invalid config', /unknown keys: invalid/ + it_behaves_like 'an invalid config', /invalid expression syntax/ end + end + + context 'when specifying an exists: clause' do + let(:config) { { exists: './this.md' } } - context 'when clause is empty' do - let(:config) { {} } + it_behaves_like 'a valid config' - it_behaves_like 'an invalid config', /can't be blank/ + context 'when array' do + let(:config) { { exists: ['./this.md', './that.md'] } } + + it_behaves_like 'a valid config' end - context 'when policy strategy does not match' do - let(:config) { 'string strategy' } + context 'when null' do + let(:config) { { exists: nil } } - it_behaves_like 'an invalid config', /should be a hash/ + it_behaves_like 'a valid config' end end - describe '#value' do - subject(:value) { entry.value } - - context 'when specifying an if: clause' do - let(:config) { { if: '$THIS || $THAT' } } + context 'when specifying an unknown keyword' do + let(:config) { { invalid: :something } } - it 'returns the config' do - expect(subject).to eq(if: '$THIS || $THAT') - end + it_behaves_like 'an invalid config', /unknown keys: invalid/ + end - context 'with when:' do - let(:config) { { if: '$THIS || $THAT', when: 'never' } } + context 'when config is blank' do + let(:config) { {} } - it 'returns the config' do - expect(subject).to eq(if: '$THIS || $THAT', when: 'never') - end - end - end + it_behaves_like 'an invalid config', /can't be blank/ + end - context 'when specifying an exists: clause' do - let(:config) { { exists: './test.md' } } + context 'when config type is invalid' do + let(:config) { 'invalid' } - it 'returns the config' do - expect(subject).to eq(exists: './test.md') - end - end + it_behaves_like 'an invalid config', /should be a hash/ end end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb index d5988dbbb58..05db81abfc1 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed require_dependency 'active_model' -RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do +RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pipeline_composition do let(:factory) do Gitlab::Config::Entry::Factory.new(described_class) .value(config) @@ -77,23 +77,68 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do describe '#value' do subject(:value) { entry.value } - context 'with an "if"' do - let(:config) do - [{ if: '$THIS == "that"' }] + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', when: 'never' } + ] + end + + it { is_expected.to eq([]) } + + context 'when composed' do + before do + entry.compose! end - it { is_expected.to eq(config) } + it 'returns the composed entries value' do + expect(entry).to be_valid + is_expected.to eq( + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', when: 'never' } + ] + ) + end + + context 'when invalid' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', invalid: 'invalid' } + ] + end + + it 'returns the invalid config' do + expect(entry).not_to be_valid + is_expected.to eq(config) + end + end end - context 'with a list of two rules' do - let(:config) do - [ - { if: '$THIS == "that"' }, - { if: '$SKIP' } - ] + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + context 'with an "if"' do + let(:config) do + [{ if: '$THIS == "that"' }] + end + + it { is_expected.to eq(config) } end - it { is_expected.to eq(config) } + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP' } + ] + end + + it { is_expected.to eq(config) } + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index ab2e8d4db78..eba9411560e 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Ci::Config::Entry::Need do +RSpec.describe ::Gitlab::Ci::Config::Entry::Need, feature_category: :pipeline_composition do subject(:need) { described_class.new(config) } shared_examples 'job type' do @@ -219,6 +219,81 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do it_behaves_like 'job type' end + + context 'when parallel:matrix has a value' do + before do + need.compose! + end + + context 'and it is a string value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: 'p1', stack: 's1' }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ "platform" => ['p1'], "stack" => ['s1'] }] } + ) + end + end + + it_behaves_like 'job type' + end + + context 'and it is an array value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: %w[s1 s2] }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => %w[s1 s2] }] } + ) + end + end + + it_behaves_like 'job type' + end + + context 'and it is a both an array and string value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: 's1' }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => ['s1'] }] } + ) + end + end + + it_behaves_like 'job type' + end + end end context 'with cross pipeline artifacts needs' do diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index 489fbac68b2..d1a8a74ac06 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do +RSpec.describe ::Gitlab::Ci::Config::Entry::Needs, feature_category: :pipeline_composition do subject(:needs) { described_class.new(config) } before do @@ -67,6 +67,141 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do end end + context 'when needs value is a hash' do + context 'with a job value' do + let(:config) do + { job: 'job_name' } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + + context 'with a parallel value that is a numeric value' do + let(:config) do + { job: 'job_name', parallel: 2 } + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about number values being invalid for needs:parallel' do + expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."]) + end + end + end + end + + context 'when needs:parallel value is incorrect' do + context 'with a keyword that is not "matrix"' do + let(:config) do + [ + { job: 'job_name', parallel: { not_matrix: [{ one: 'aaa', two: 'bbb' }] } } + ] + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about incorrect matrix keyword' do + expect(needs.errors).to match_array([ + 'need:parallel config contains unknown keys: not_matrix', + 'need:parallel config missing required keys: matrix' + ]) + end + end + end + + context 'with a number value' do + let(:config) { [{ job: 'job_name', parallel: 2 }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about number values being invalid for needs:parallel' do + expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."]) + end + end + end + end + + context 'when needs:parallel:matrix value is empty' do + let(:config) { [{ job: 'job_name', parallel: { matrix: {} } }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need:parallel:matrix config should be an array of hashes') + end + end + end + + context 'when needs:parallel:matrix value is incorrect' do + let(:config) { [{ job: 'job_name', parallel: { matrix: 'aaa' } }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need:parallel:matrix config should be an array of hashes') + end + end + end + + context 'when needs:parallel:matrix value is correct' do + context 'with a simple config' do + let(:config) do + [ + { job: 'job_name', parallel: { matrix: [{ A: 'a1', B: 'b1' }] } } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + + context 'with a complex config' do + let(:config) do + [ + { + job: 'job_name1', + artifacts: true, + parallel: { matrix: [{ A: %w[a1 a2], B: %w[b1 b2 b3], C: %w[c1 c2] }] } + }, + { + job: 'job_name2', + parallel: { + matrix: [ + { A: %w[a1 a2], D: %w[d1 d2] }, + { E: %w[e1 e2], F: ['f1'] }, + { C: %w[c1 c2 c3], G: %w[g1 g2], H: ['h1'] } + ] + } + } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + end + context 'with too many cross pipeline dependencies' do let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT } diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 73bf2d422b7..d610c3ce2f6 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -48,6 +48,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_c :terraform | 'tfplan.json' :accessibility | 'gl-accessibility.json' :cyclonedx | 'gl-sbom.cdx.zip' + :annotations | 'gl-annotations.json' end with_them do diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index d917924f257..d8bd578be94 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -57,6 +57,24 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin end end end + + describe 'max_total_yaml_size_bytes' do + context 'when application setting `max_total_yaml_size_bytes` is requsted and was never updated by the admin' do + it 'returns the default value `max_total_yaml_size_bytes`' do + expect(subject.max_total_yaml_size_bytes).to eq(157286400) + end + end + + context 'when `max_total_yaml_size_bytes` was adjusted by the admin' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 200000000) + end + + it 'returns the updated value of application setting `max_total_yaml_size_bytes`' do + expect(subject.max_total_yaml_size_bytes).to eq(200000000) + end + end + end end describe '#set_deadline' do diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index d6dd75f4b10..1415dbeb532 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -254,7 +254,12 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe describe '#load_and_validate_expanded_hash!' do let(:location) { 'some/file/config.yml' } let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) } - let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger } } + let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger, user: user } } + let(:user) { instance_double(User, id: 'test-user-id') } + + before do + allow(logger).to receive(:instrument).and_yield + end it 'includes instrumentation for loading and expanding the content' do expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield @@ -262,5 +267,26 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe file.load_and_validate_expanded_hash! end + + context 'when the content is interpolated' do + let(:content) { "spec:\n inputs:\n website:\n---\nkey: value" } + + subject(:file) { test_class.new({ inputs: { website: 'test' }, location: location, content: content }, ctx) } + + it 'increments the ci_interpolation_users usage counter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + .with('ci_interpolation_users', values: 'test-user-id') + + file.load_and_validate_expanded_hash! + end + end + + context 'when the content is not interpolated' do + it 'does not increment the ci_interpolation_users usage counter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + file.load_and_validate_expanded_hash! + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 7e3406413d0..487690296b5 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -41,14 +41,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:params) { { component: 'some-value' } } it { is_expected.to be_truthy } - - context 'when feature flag ci_include_components is disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - it { is_expected.to be_falsey } - end end context 'when component is not specified' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb index 719c75dca80..cea65faccd7 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -18,54 +18,26 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: describe '#process' do subject(:process) { matcher.process(locations) } - context 'with ci_include_components FF disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - let(:locations) do - [ - { local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' } - ] - end - - it 'returns an array of file objects' do - is_expected.to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Local), - an_instance_of(Gitlab::Ci::Config::External::File::Project), - an_instance_of(Gitlab::Ci::Config::External::File::Remote), - an_instance_of(Gitlab::Ci::Config::External::File::Template), - an_instance_of(Gitlab::Ci::Config::External::File::Artifact) - ) - end + let(:locations) do + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { component: 'gitlab.com/org/component@1.0' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] end - context 'with ci_include_components FF enabled' do - let(:locations) do - [ - { local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { component: 'gitlab.com/org/component@1.0' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' } - ] - end - - it 'returns an array of file objects' do - is_expected.to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Local), - an_instance_of(Gitlab::Ci::Config::External::File::Project), - an_instance_of(Gitlab::Ci::Config::External::File::Component), - an_instance_of(Gitlab::Ci::Config::External::File::Remote), - an_instance_of(Gitlab::Ci::Config::External::File::Template), - an_instance_of(Gitlab::Ci::Config::External::File::Artifact) - ) - end + it 'returns an array of file objects' do + is_expected.to contain_exactly( + an_instance_of(Gitlab::Ci::Config::External::File::Local), + an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Component), + an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Template), + an_instance_of(Gitlab::Ci::Config::External::File::Artifact) + ) end context 'when a location is not valid' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb index e7dd5bd5079..69b0524be9e 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -364,5 +364,77 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: end end end + + describe '#verify_max_total_pipeline_size' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context) + ] + end + + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + build: + script: echo Hello World + YAML + 'myfolder/file2.yml' => <<~YAML + include: + - local: myfolder/file1.yml + build: + script: echo Hello from the other file + YAML + } + end + + context 'when pipeline tree size is within the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 10000) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + + context 'when pipeline tree size is larger then the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 50) + end + + let(:expected_error_class) { Gitlab::Ci::Config::External::Mapper::TooMuchDataInPipelineTreeError } + + it 'raises a limit error' do + expect { process }.to raise_error(expected_error_class) + end + end + + context 'when introduce_ci_max_total_yaml_size_bytes is disabled' do + before do + stub_feature_flags(introduce_ci_max_total_yaml_size_bytes: false) + end + + context 'when pipeline tree size is within the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 10000) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + + context 'when pipeline tree size is larger then the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 100) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 935b6989dd7..19113ce6a4e 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -425,17 +425,6 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel output = processor.perform expect(output.keys).to match_array([:image, :component_x_job]) end - - context 'when feature flag ci_include_components is disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - it 'returns an error' do - expect { processor.perform } - .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./) - end - end end context 'when a valid project file is defined' do @@ -572,7 +561,17 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel end it 'raises IncludeError' do - expect { subject }.to raise_error(described_class::IncludeError, /invalid include rule/) + expect { subject }.to raise_error(described_class::IncludeError, /contains unknown keys: changes/) + end + + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + it 'raises IncludeError' do + expect { subject }.to raise_error(described_class::IncludeError, /invalid include rule/) + end end end end diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 25b7998ef5e..8674af7ab65 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -76,8 +76,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:if=>"$MY_VAR == \"hello\"", :when=>"on_success"}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) end end @@ -105,8 +104,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:exists=>"Dockerfile", :when=>"on_success"}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) end end @@ -121,8 +119,94 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:changes=>["$MY_VAR"]}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /contains unknown keys: changes/) + end + end + + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + context 'when there is no rule' do + let(:rule_hashes) {} + + it { is_expected.to eq(true) } + end + + it_behaves_like 'when there is a rule with if' + + context 'when there is a rule with exists' do + let(:rule_hashes) { [{ exists: 'Dockerfile' }] } + + it_behaves_like 'when there is a rule with exists' + end + + context 'when there is a rule with if and when' do + context 'with when: never' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'never' }] } + + it_behaves_like 'when there is a rule with if', false, false + end + + context 'with when: always' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'always' }] } + + it_behaves_like 'when there is a rule with if' + end + + context 'with when: <invalid string>' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:if=>"$MY_VAR == \"hello\"", :when=>"on_success"}') + end + end + + context 'with when: null' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: nil }] } + + it_behaves_like 'when there is a rule with if' + end + end + + context 'when there is a rule with exists and when' do + context 'with when: never' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'never' }] } + + it_behaves_like 'when there is a rule with exists', false, false + end + + context 'with when: always' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'always' }] } + + it_behaves_like 'when there is a rule with exists' + end + + context 'with when: <invalid string>' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:exists=>"Dockerfile", :when=>"on_success"}') + end + end + + context 'with when: null' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: nil }] } + + it_behaves_like 'when there is a rule with exists' + end + end + + context 'when there is a rule with changes' do + let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:changes=>["$MY_VAR"]}') + end end end end diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb index 73b5b8f9497..b5155dff6e8 100644 --- a/spec/lib/gitlab/ci/config/header/input_spec.rb +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -46,12 +46,29 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'a valid input' end - context 'when is a required required input' do + context 'when is a required input' do let(:input_hash) { nil } it_behaves_like 'a valid input' end + context 'when given a valid type' do + where(:input_type) { ::Gitlab::Ci::Config::Interpolation::Inputs.input_types } + + with_them do + let(:input_hash) { { type: input_type } } + + it_behaves_like 'a valid input' + end + end + + context 'when given an invalid type' do + let(:input_hash) { { type: 'datetime' } } + let(:expected_errors) { ['foo input type unknown value: datetime'] } + + it_behaves_like 'an invalid input' + end + context 'when contains unknown keywords' do let(:input_hash) { { test: 123 } } let(:expected_errors) { ['foo config contains unknown keys: test'] } diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/config/interpolation/access_spec.rb index f327377b7e3..ee414c209f7 100644 --- a/spec/lib/gitlab/ci/interpolation/access_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/access_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Access, feature_category: :pipeline_composition do subject { described_class.new(access, ctx) } let(:access) do @@ -46,4 +46,13 @@ RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_co .to eq 'invalid interpolation access pattern' end end + + context 'when a non-existent key is accessed' do + let(:access) { 'inputs.nonexistent' } + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('unknown interpolation key: `nonexistent`') + end + end end diff --git a/spec/lib/gitlab/ci/config/interpolation/block_spec.rb b/spec/lib/gitlab/ci/config/interpolation/block_spec.rb new file mode 100644 index 00000000000..bfaa4eb3e05 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/block_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Block, feature_category: :pipeline_composition do + subject { described_class.new(block, data, ctx) } + + let(:data) do + 'inputs.data' + end + + let(:block) do + "$[[ #{data} ]]" + end + + let(:ctx) do + { inputs: { data: 'abcdef' }, env: { 'ENV' => 'dev' } } + end + + it 'knows its content' do + expect(subject.content).to eq 'inputs.data' + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abcdef' + end + + describe '.match' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } + .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) + end + + it 'matches an empty block' do + expect { |b| described_class.match('$[[]]', &b) } + .to yield_with_args('$[[]]', '') + end + + context 'when functions are specified in the block' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 | func1 ]] $[[ access2 | func1 | func2(0,1) ]]', &b) } + .to yield_successive_args(['$[[ access1 | func1 ]]', 'access1 | func1'], + ['$[[ access2 | func1 | func2(0,1) ]]', 'access2 | func1 | func2(0,1)']) + end + end + end + + describe 'when functions are specified in the block' do + let(:function_string1) { 'truncate(1,5)' } + let(:data) { "inputs.data | #{function_string1}" } + let(:access_value) { 'abcdef' } + + it 'returns the modified value' do + expect(subject).to be_valid + expect(subject.value).to eq('bcdef') + end + + context 'when there is an access error' do + let(:data) { "inputs.undefined | #{function_string1}" } + + it 'returns the access error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('unknown interpolation key: `undefined`') + end + end + + context 'when there is a function error' do + let(:data) { 'inputs.data | undefined' } + + it 'returns the function error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `undefined`/) + end + end + + context 'when multiple functions are specified' do + let(:function_string2) { 'truncate(2,2)' } + let(:data) { "inputs.data | #{function_string1} | #{function_string2}" } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + + context 'when the data has inconsistent spacing' do + let(:data) { "inputs.data|#{function_string1} | #{function_string2} " } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + end + + context 'when a stack of functions errors in the middle' do + let(:function_string2) { 'truncate(2)' } + + it 'does not modify the value' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `truncate\(2\)`/) + expect(subject.instance_variable_get(:@value)).to be_nil + end + end + + context 'when too many functions are specified' do + it 'returns error' do + stub_const('Gitlab::Ci::Config::Interpolation::Block::MAX_FUNCTIONS', 1) + + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('too many functions in interpolation block') + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/config_spec.rb b/spec/lib/gitlab/ci/config/interpolation/config_spec.rb new file mode 100644 index 00000000000..1731e954906 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/config_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Config, feature_category: :pipeline_composition do + subject { described_class.new(YAML.safe_load(config)) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + describe '.fabricate' do + subject { described_class.fabricate(config) } + + context 'when given an Interpolation::Config' do + let(:config) { described_class.new(YAML.safe_load('yaml:')) } + + it 'returns the given config' do + is_expected.to be(config) + end + end + + context 'when given an unknown object' do + let(:config) { [] } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError, 'unknown interpolation config') + end + end + end + + describe '#replace!' do + it 'replaces each of the nodes with a block return value' do + result = subject.replace! { |node| "abc#{node}cde" } + + expect(result).to eq({ + 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, + 'abc$[[ inputs.key ]]cde' => { + 'abcnamecde' => 'abc$[[ inputs.key ]]cde', + 'abcscriptcde' => 'abcmy-valuecde' + } + }) + expect(subject.to_h).to eq({ + '$[[ inputs.key ]]' => { 'name' => '$[[ inputs.key ]]', 'script' => 'my-value' }, + 'test' => { 'spec' => { 'env' => '$[[ inputs.env ]]' } } + }) + end + + context 'when config size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODES", 7) + end + + it 'returns a config size error' do + replaced = 0 + + subject.replace! { replaced += 1 } + + expect(replaced).to eq 4 + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end + + context 'when node size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODE_SIZE", 1) + end + + it 'returns a config size error' do + subject.replace! { |node| "abc#{node}cde" } + + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config node too large' + end + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb index 2b126f4a8b3..c90866c986a 100644 --- a/spec/lib/gitlab/ci/interpolation/context_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb @@ -2,13 +2,27 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Context, feature_category: :pipeline_composition do subject { described_class.new(ctx) } let(:ctx) do { inputs: { key: 'abc' } } end + describe '.fabricate' do + context 'when given an unexpected object' do + it 'raises an ArgumentError' do + expect { described_class.fabricate([]) }.to raise_error(ArgumentError, 'unknown interpolation context') + end + end + end + + describe '#to_h' do + it 'returns the context hash' do + expect(subject.to_h).to eq(ctx) + end + end + describe '#depth' do it 'returns a max depth of the hash' do expect(subject.depth).to eq 2 diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb new file mode 100644 index 00000000000..c193e88dbe2 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Base, feature_category: :pipeline_composition do + let(:custom_function_klass) do + Class.new(described_class) do + def self.function_expression_pattern + /.*/ + end + + def self.name + 'test_function' + end + end + end + + it 'defines an expected interface for child classes' do + expect { described_class.function_expression_pattern }.to raise_error(NotImplementedError) + expect { described_class.name }.to raise_error(NotImplementedError) + expect { custom_function_klass.new('test').execute('input') }.to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb new file mode 100644 index 00000000000..c521eff9811 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Truncate, feature_category: :pipeline_composition do + it 'matches exactly the truncate function with 2 numeric arguments' do + expect(described_class.matches?('truncate(1,2)')).to be_truthy + expect(described_class.matches?('truncate( 11 , 222 )')).to be_truthy + expect(described_class.matches?('truncate( string , 222 )')).to be_falsey + expect(described_class.matches?('truncate(222)')).to be_falsey + expect(described_class.matches?('unknown(1,2)')).to be_falsey + end + + it 'truncates the given input' do + function = described_class.new('truncate(1,2)') + + output = function.execute('test') + + expect(function).to be_valid + expect(output).to eq('es') + end + + context 'when given a non-string input' do + it 'returns an error' do + function = described_class.new('truncate(1,2)') + + function.execute(100) + + expect(function).not_to be_valid + expect(function.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb new file mode 100644 index 00000000000..881f092c440 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::FunctionsStack, feature_category: :pipeline_composition do + let(:functions) { ['truncate(0,4)', 'truncate(1,2)'] } + let(:input_value) { 'test_input_value' } + + subject { described_class.new(functions).evaluate(input_value) } + + it 'modifies the given input value according to the function expressions' do + expect(subject).to be_success + expect(subject.value).to eq('es') + end + + context 'when applying a function fails' do + let(:input_value) { 666 } + + it 'returns the error given by the failure' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end + + context 'when function expressions do not match any function' do + let(:functions) { ['truncate(0)', 'unknown'] } + + it 'returns an error' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'no function matching `truncate(0)`: check that the function name, arguments, and types are correct', + 'no function matching `unknown`: check that the function name, arguments, and types are correct' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb new file mode 100644 index 00000000000..30036ee68ed --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do + describe '.matches?' do + it 'is not implemented' do + expect { described_class.matches?(double) }.to raise_error(NotImplementedError) + end + end + + describe '.type_name' do + it 'is not implemented' do + expect { described_class.type_name }.to raise_error(NotImplementedError) + end + end + + describe '#valid_value?' do + it 'is not implemented' do + expect do + described_class.new( + name: 'website', spec: { website: nil }, value: { website: 'example.com' } + ).valid_value?('test') + end.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb new file mode 100644 index 00000000000..ea06f181fa4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pipeline_composition do + let(:inputs) { described_class.new(specs, args) } + let(:specs) { { foo: { default: 'bar' } } } + let(:args) { {} } + + context 'when inputs are valid' do + where(:specs, :args, :merged) do + [ + [ + { foo: { default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: nil }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string' } }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' }, baz: nil }, { baz: 'test' }, + { foo: 'bar', baz: 'test' } + ], + [ + { number_input: { type: 'number' } }, + { number_input: 8 }, + { number_input: 8 } + ], + [ + { default_number_input: { default: 9, type: 'number' } }, + {}, + { default_number_input: 9 } + ], + [ + { true_input: { type: 'boolean' }, false_input: { type: 'boolean' } }, + { true_input: true, false_input: false }, + { true_input: true, false_input: false } + ], + [ + { default_boolean_input: { default: true, type: 'boolean' } }, + {}, + { default_boolean_input: true } + ] + ] + end + + with_them do + it 'contains the merged inputs' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(merged) + end + end + end + + context 'when inputs are invalid' do + where(:specs, :args, :errors) do + [ + [ + { foo: nil }, { foo: 'bar', test: 'bar' }, + ['unknown input arguments: test'] + ], + [ + { foo: nil }, { test: 'bar', gitlab: '1' }, + ['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided'] + ], + [ + { foo: 123 }, {}, + ['unknown input specification for `foo` (valid types: boolean, number, string)'] + ], + [ + { a: nil, foo: 123 }, { a: '123' }, + ['unknown input specification for `foo` (valid types: boolean, number, string)'] + ], + [ + { foo: nil }, {}, + ['`foo` input: required value has not been provided'] + ], + [ + { foo: { default: 123 } }, { foo: 'test' }, + ['`foo` input: default value is not a string'] + ], + [ + { foo: { default: 'test' } }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ], + [ + { foo: nil }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ], + [ + { number_input: { type: 'number' } }, + { number_input: 'NaN' }, + ['`number_input` input: provided value is not a number'] + ], + [ + { default_number_input: { default: 'NaN', type: 'number' } }, + {}, + ['`default_number_input` input: default value is not a number'] + ], + [ + { boolean_input: { type: 'boolean' } }, + { boolean_input: 'string' }, + ['`boolean_input` input: provided value is not a boolean'] + ], + [ + { default_boolean_input: { default: 'string', type: 'boolean' } }, + {}, + ['`default_boolean_input` input: default value is not a boolean'] + ] + ] + end + + with_them do + it 'contains the merged inputs', :aggregate_failures do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly(*errors) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 888756a3eb1..7bb09d35064 100644 --- a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -2,13 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Interpolator, feature_category: :pipeline_composition do let_it_be(:project) { create(:project) } - let(:current_user) { build(:user, id: 1234) } let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) } - subject { described_class.new(result, arguments, current_user: current_user) } + subject { described_class.new(result, arguments) } context 'when input data is valid' do let(:header) do @@ -26,16 +25,10 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli it 'correctly interpolates the config' do subject.interpolate! + expect(subject).to be_interpolated expect(subject).to be_valid expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) end - - it 'tracks the event' do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) - .with('ci_interpolation_users', { values: 1234 }) - - subject.interpolate! - end end context 'when config has a syntax error' do @@ -54,6 +47,20 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli end end + context 'when spec header is missing but inputs are specified' do + let(:header) { nil } + let(:content) { { test: 'echo' } } + let(:arguments) { { foo: 'bar' } } + + it 'surfaces an error about invalid inputs' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include('unknown input arguments') + end + end + context 'when spec header is invalid' do let(:header) do { spec: { arguments: { website: nil } } } @@ -76,47 +83,47 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli end end - context 'when interpolation block is invalid' do + context 'when provided interpolation argument is invalid' do let(:header) do { spec: { inputs: { website: nil } } } end let(:content) do - { test: 'deploy $[[ inputs.abc ]]' } + { test: 'deploy $[[ inputs.website ]]' } end let(:arguments) do - { website: 'gitlab.com' } + { website: ['gitlab.com'] } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.errors).to include 'unknown interpolation key: `abc`' - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include '`website` input: provided value is not a string' end end - context 'when provided interpolation argument is invalid' do + context 'when interpolation block is invalid' do let(:header) do { spec: { inputs: { website: nil } } } end let(:content) do - { test: 'deploy $[[ inputs.website ]]' } + { test: 'deploy $[[ inputs.abc ]]' } end let(:arguments) do - { website: ['gitlab.com'] } + { website: 'gitlab.com' } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include 'unsupported value in input argument `website`' + expect(subject.errors).to include 'unknown interpolation key: `abc`' + expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' end end @@ -133,11 +140,12 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli { website: 'gitlab.com' } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' + expect(subject.error_message) + .to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' end end diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/config/interpolation/template_spec.rb index a3ef1bb4445..c7d88822558 100644 --- a/spec/lib/gitlab/ci/interpolation/template_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/template_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Template, feature_category: :pipeline_composition do subject { described_class.new(YAML.safe_load(config), ctx) } let(:config) do @@ -67,7 +67,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_ context 'when template contains symbols that need interpolation' do subject do - described_class.new({ '$[[ inputs.key ]]'.to_sym => 'cde' }, ctx) + described_class.new({ '$[[ inputs.key ]]': 'cde' }, ctx) end it 'performs a valid interpolation' do @@ -78,7 +78,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_ context 'when template is too large' do before do - stub_const('Gitlab::Ci::Interpolation::Config::MAX_NODES', 1) + stub_const('Gitlab::Ci::Config::Interpolation::Config::MAX_NODES', 1) end it 'returns an error' do diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 96ca5d98a6e..cc549b38dc3 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Normalizer do let(:job_name) { :rspec } @@ -103,6 +103,34 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do end end + shared_examples 'needs:parallel:matrix' do + let(:expanded_needs_parallel_job_attributes) do + expanded_needs_parallel_job_names.map do |job_name| + { name: job_name } + end + end + + context 'when job has needs:parallel:matrix on parallelized jobs' do + let(:config) do + { + job_name => job_config, + other_job: { + script: 'echo 1', + needs: { + job: [ + { name: job_name.to_s, parallel: needs_parallel_config } + ] + } + } + } + end + + it 'parallelizes and only keeps needs specified by needs:parallel:matrix' do + expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_needs_parallel_job_attributes) + end + end + end + context 'with parallel config as integer' do let(:variables_config) { {} } let(:parallel_config) { 5 } @@ -167,7 +195,7 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do it_behaves_like 'parallel needs' end - context 'with parallel matrix config' do + context 'with a simple parallel matrix config' do let(:variables_config) do { USER_VARIABLE: 'user value' @@ -192,6 +220,19 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do ] end + let(:needs_parallel_config) do + { + matrix: [ + { + VAR_1: ['A'], + VAR_2: ['C'] + } + ] + } + end + + let(:expanded_needs_parallel_job_names) { ['rspec: [A, C]'] } + it 'does not have original job' do is_expected.not_to include(job_name) end @@ -228,6 +269,66 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do it_behaves_like 'parallel dependencies' it_behaves_like 'parallel needs' + it_behaves_like 'needs:parallel:matrix' + end + + context 'with a complex parallel matrix config' do + let(:variables_config) { {} } + let(:parallel_config) do + { + matrix: [ + { + PLATFORM: ['centos'], + STACK: %w[ruby python java], + DB: %w[postgresql mysql] + }, + { + PLATFORM: ['ubuntu'], + PROVIDER: %w[aws gcp] + } + ] + } + end + + let(:needs_parallel_config) do + { + matrix: [ + { + PLATFORM: ['centos'], + STACK: %w[ruby python], + DB: ['postgresql'] + }, + { + PLATFORM: ['ubuntu'], + PROVIDER: ['aws'] + } + ] + } + end + + let(:expanded_needs_parallel_job_names) do + [ + 'rspec: [centos, ruby, postgresql]', + 'rspec: [centos, python, postgresql]', + 'rspec: [ubuntu, aws]' + ] + end + + let(:expanded_job_names) do + [ + 'rspec: [centos, ruby, postgresql]', + 'rspec: [centos, ruby, mysql]', + 'rspec: [centos, python, postgresql]', + 'rspec: [centos, python, mysql]', + 'rspec: [centos, java, postgresql]', + 'rspec: [centos, java, mysql]', + 'rspec: [ubuntu, aws]', + 'rspec: [ubuntu, gcp]' + ] + end + + it_behaves_like 'parallel needs' + it_behaves_like 'needs:parallel:matrix' end context 'when parallel config does not matches a factory' do diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb index 4e6151677e6..57a9a47d699 100644 --- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb @@ -21,12 +21,13 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c YAML end - subject(:result) { described_class.new(yaml, inputs: inputs, current_user: project.creator).load } + subject(:result) { described_class.new(yaml, inputs: inputs).load } it 'loads and interpolates CI config YAML' do expected_config = { test_job: { script: ['echo "hello test"'] } } expect(result).to be_valid + expect(result).to be_interpolated expect(result.content).to eq(expected_config) end diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb index d17e0609ef6..a66c630dfc9 100644 --- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb @@ -51,4 +51,14 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com expect(result).not_to be_valid expect(result.error).to be_a ArgumentError end + + describe '#interpolated?' do + it 'defaults to false' do + expect(described_class.new).not_to be_interpolated + end + + it 'returns the value passed to the initializer' do + expect(described_class.new(interpolated: true)).to be_interpolated + end + end end diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb index 27d93d555f1..e30ddbb8033 100644 --- a/spec/lib/gitlab/ci/config/yaml_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml_spec.rb @@ -36,17 +36,5 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition .to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/ end end - - context 'when given a user' do - let(:user) { instance_double(User) } - - subject(:config) { described_class.load!(yaml, current_user: user) } - - it 'passes it to Loader' do - expect(::Gitlab::Ci::Config::Yaml::Loader).to receive(:new).with(yaml, current_user: user).and_call_original - - config - end - end end end diff --git a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb index dad5bd2548b..f1b10648f51 100644 --- a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb +++ b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::DecompressedGzipSizeValidator, feature_category: :importers do let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_gzip_size_validator_spec.gz') } - before(:all) do + before_all do create_compressed_file end diff --git a/spec/lib/gitlab/ci/input/arguments/base_spec.rb b/spec/lib/gitlab/ci/input/arguments/base_spec.rb deleted file mode 100644 index ed8e99b7257..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/base_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Base, feature_category: :pipeline_composition do - subject do - Class.new(described_class) do - def validate!; end - def to_value; end - end - end - - it 'fabricates an invalid input argument if unknown value is provided' do - argument = subject.new(:something, { spec: 123 }, [:a, :b]) - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq 'unsupported value in input argument `something`' - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/default_spec.rb b/spec/lib/gitlab/ci/input/arguments/default_spec.rb deleted file mode 100644 index bc0cee6ac4e..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/default_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Default, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is present' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, 'https://example.gitlab.com') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://example.gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) - end - - it 'returns an empty value if user-provider input is empty' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, '') - - expect(argument).to be_valid - expect(argument.to_value).to eq '' - expect(argument.to_hash).to eq({ website: '' }) - end - - it 'returns a default value if user-provider one is unknown' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, nil) - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://gitlab.com' }) - end - - it 'returns an error if the default argument has not been recognized' do - argument = described_class.new(:website, { default: ['gitlab.com'] }, 'abc') - - expect(argument).not_to be_valid - end - - it 'returns an error if the argument has not been fabricated correctly' do - argument = described_class.new(:website, { required: 'https://gitlab.com' }, 'https://example.gitlab.com') - - expect(argument).not_to be_valid - end - - describe '.matches?' do - it 'matches specs with default configuration' do - expect(described_class.matches?({ default: 'abc' })).to be true - end - - it 'does not match specs different configuration keyword' do - expect(described_class.matches?({ options: %w[a b] })).to be false - expect(described_class.matches?('a b c')).to be false - expect(described_class.matches?(%w[default a])).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/options_spec.rb b/spec/lib/gitlab/ci/input/arguments/options_spec.rb deleted file mode 100644 index 17e3469b294..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/options_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Options, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is an allowed one' do - argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt1') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'opt1' - expect(argument.to_hash).to eq({ run: 'opt1' }) - end - - it 'returns an error if user-provided value is not allowlisted' do - argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt3') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`run` input: argument value opt3 not allowlisted' - end - - it 'returns an error if specification is not correct' do - argument = described_class.new(:website, { options: nil }, 'opt1') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: argument specification invalid' - end - - it 'returns an error if specification is using a hash' do - argument = described_class.new(:website, { options: { a: 1 } }, 'opt1') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: argument specification invalid' - end - - it 'returns an empty value if it is allowlisted' do - argument = described_class.new(:run, { options: ['opt1', ''] }, '') - - expect(argument).to be_valid - expect(argument.to_value).to be_empty - expect(argument.to_hash).to eq({ run: '' }) - end - - describe '.matches?' do - it 'matches specs with options configuration' do - expect(described_class.matches?({ options: %w[a b] })).to be true - end - - it 'does not match specs different configuration keyword' do - expect(described_class.matches?({ default: 'abc' })).to be false - expect(described_class.matches?(['options'])).to be false - expect(described_class.matches?('options')).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/required_spec.rb b/spec/lib/gitlab/ci/input/arguments/required_spec.rb deleted file mode 100644 index 847272998c2..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/required_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Required, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is present' do - argument = described_class.new(:website, nil, 'https://example.gitlab.com') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://example.gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) - end - - it 'returns an empty value if user-provider value is empty' do - argument = described_class.new(:website, nil, '') - - expect(argument).to be_valid - expect(argument.to_hash).to eq(website: '') - end - - it 'returns an error if user-provided value is unspecified' do - argument = described_class.new(:website, nil, nil) - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: required value has not been provided' - end - - describe '.matches?' do - it 'matches specs without configuration' do - expect(described_class.matches?(nil)).to be true - end - - it 'matches specs with empty configuration' do - expect(described_class.matches?('')).to be true - end - - it 'matches specs with an empty hash configuration' do - expect(described_class.matches?({})).to be true - end - - it 'does not match specs with configuration' do - expect(described_class.matches?({ options: %w[a b] })).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb deleted file mode 100644 index 1270423ac72..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Unknown, feature_category: :pipeline_composition do - it 'raises an error when someone tries to evaluate the value' do - argument = described_class.new(:website, nil, 'https://example.gitlab.com') - - expect(argument).not_to be_valid - expect { argument.to_value }.to raise_error ArgumentError - end - - describe '.matches?' do - it 'always matches' do - expect(described_class.matches?('abc')).to be true - end - end -end diff --git a/spec/lib/gitlab/ci/input/inputs_spec.rb b/spec/lib/gitlab/ci/input/inputs_spec.rb deleted file mode 100644 index 5d2d5192299..00000000000 --- a/spec/lib/gitlab/ci/input/inputs_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Inputs, feature_category: :pipeline_composition do - describe '#valid?' do - let(:spec) { { website: nil } } - - it 'describes user-provided inputs' do - inputs = described_class.new(spec, { website: 'http://example.gitlab.com' }) - - expect(inputs).to be_valid - end - end - - context 'when proper specification has been provided' do - let(:spec) do - { - website: nil, - env: { default: 'development' }, - run: { options: %w[tests spec e2e] } - } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'fabricates desired input arguments' do - inputs = described_class.new(spec, args) - - expect(inputs).to be_valid - expect(inputs.count).to eq 3 - expect(inputs.to_hash).to eq(args.merge(env: 'development')) - end - end - - context 'when inputs and args are empty' do - it 'is a valid use-case' do - inputs = described_class.new({}, {}) - - expect(inputs).to be_valid - expect(inputs.to_hash).to be_empty - end - end - - context 'when there are arguments recoincilation errors present' do - context 'when required argument is missing' do - let(:spec) { { website: nil } } - - it 'returns an error' do - inputs = described_class.new(spec, {}) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`website` input: required value has not been provided' - end - end - - context 'when argument is not present but configured as allowlist' do - let(:spec) do - { run: { options: %w[opt1 opt2] } } - end - - it 'returns an error' do - inputs = described_class.new(spec, {}) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`run` input: argument not provided' - end - end - end - - context 'when unknown specification argument has been used' do - let(:spec) do - { - website: nil, - env: { default: 'development' }, - run: { options: %w[tests spec e2e] }, - test: { unknown: 'something' } - } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'fabricates an unknown argument entry and returns an error' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.count).to eq 4 - expect(inputs.errors.first).to eq '`test` input: unrecognized input argument specification: `unknown`' - end - end - - context 'when unknown arguments are being passed by a user' do - let(:spec) do - { env: { default: 'development' } } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'returns an error with a list of unknown arguments' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq 'unknown input arguments: [:website, :run]' - end - end - - context 'when composite specification is being used' do - let(:spec) do - { - env: { - default: 'dev', - options: %w[test dev prod] - } - } - end - - let(:args) { { env: 'dev' } } - - it 'returns an error describing an unknown specification' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`env` input: unrecognized input argument definition' - end - end -end diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb deleted file mode 100644 index 4a8709df3dc..00000000000 --- a/spec/lib/gitlab/ci/interpolation/block_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_composition do - subject { described_class.new(block, data, ctx) } - - let(:data) do - 'inputs.data' - end - - let(:block) do - "$[[ #{data} ]]" - end - - let(:ctx) do - { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } } - end - - it 'knows its content' do - expect(subject.content).to eq 'inputs.data' - end - - it 'properly evaluates the access pattern' do - expect(subject.value).to eq 'abc' - end - - describe '.match' do - it 'matches each block in a string' do - expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } - .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) - end - - it 'matches an empty block' do - expect { |b| described_class.match('$[[]]', &b) } - .to yield_with_args('$[[]]', '') - end - end -end diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb deleted file mode 100644 index e745269d8c0..00000000000 --- a/spec/lib/gitlab/ci/interpolation/config_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_composition do - subject { described_class.new(YAML.safe_load(config)) } - - let(:config) do - <<~CFG - test: - spec: - env: $[[ inputs.env ]] - - $[[ inputs.key ]]: - name: $[[ inputs.key ]] - script: my-value - CFG - end - - describe '#replace!' do - it 'replaces each od the nodes with a block return value' do - result = subject.replace! { |node| "abc#{node}cde" } - - expect(result).to eq({ - 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, - 'abc$[[ inputs.key ]]cde' => { - 'abcnamecde' => 'abc$[[ inputs.key ]]cde', - 'abcscriptcde' => 'abcmy-valuecde' - } - }) - end - end - - context 'when config size is exceeded' do - before do - stub_const("#{described_class}::MAX_NODES", 7) - end - - it 'returns a config size error' do - replaced = 0 - - subject.replace! { replaced += 1 } - - expect(replaced).to eq 4 - expect(subject.errors.size).to eq 1 - expect(subject.errors.first).to eq 'config too large' - end - end -end diff --git a/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb b/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb new file mode 100644 index 00000000000..0dd0d2fcf0d --- /dev/null +++ b/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2::ClaimMapper::Repository, feature_category: :continuous_integration do + let_it_be(:sha) { '35fa264414ee3ed7d0b8a6f5da40751c8600a772' } + let_it_be(:pipeline) { build_stubbed(:ci_pipeline, ref: 'test-branch-for-claim-mapper', sha: sha) } + + let(:url) { 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' } + let(:project_config) { instance_double(Gitlab::Ci::ProjectConfig, url: url) } + + subject(:mapper) { described_class.new(project_config, pipeline) } + + describe '#to_h' do + it 'returns expected claims' do + expect(mapper.to_h).to eq({ + ci_config_ref_uri: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml@refs/heads/test-branch-for-claim-mapper', + ci_config_sha: sha + }) + end + + context 'when ref is a tag' do + let_it_be(:tag) { 'test-tag-for-claim-mapper' } + let_it_be(:pipeline) { build_stubbed(:ci_pipeline, tag: tag, ref: tag, sha: sha) } + + it 'returns expected claims' do + expect(mapper.to_h).to eq({ + ci_config_ref_uri: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml@refs/tags/test-tag-for-claim-mapper', + ci_config_sha: sha + }) + end + end + end +end diff --git a/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb b/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb new file mode 100644 index 00000000000..b7a73c938a3 --- /dev/null +++ b/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2::ClaimMapper, feature_category: :continuous_integration do + let_it_be(:pipeline) { build_stubbed(:ci_pipeline) } + + let(:source) { :unknown_source } + let(:url) { 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' } + let(:project_config) { instance_double(Gitlab::Ci::ProjectConfig, url: url, source: source) } + + subject(:mapper) { described_class.new(project_config, pipeline) } + + describe '#to_h' do + it 'returns an empty hash when source is not implemented' do + expect(mapper.to_h).to eq({}) + end + + context 'when mapper for source is implemented' do + where(:source) { described_class::MAPPER_FOR_CONFIG_SOURCE.keys } + let(:result) do + { + ci_config_ref_uri: 'ci_config_ref_uri', + ci_config_sha: 'ci_config_sha' + } + end + + with_them do + it 'uses mapper' do + mapper_class = described_class::MAPPER_FOR_CONFIG_SOURCE[source] + expect_next_instance_of(mapper_class, project_config, pipeline) do |instance| + expect(instance).to receive(:to_h).and_return(result) + end + + expect(mapper.to_h).to eq(result) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index 575f174f737..d45d8cacb88 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -129,75 +129,39 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do end end - describe 'ci_config_ref_uri' do - it 'joins project_config.url and pipeline.source_ref_path with @' do - expect(payload[:ci_config_ref_uri]).to eq('gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' \ - '@refs/heads/auto-deploy-2020-03-19') - end - - context 'when project config is nil' do - before do - allow(Gitlab::Ci::ProjectConfig).to receive(:new).and_return(nil) - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end - end - - context 'when ProjectConfig#url raises an error' do - before do - allow(project_config).to receive(:url).and_raise(RuntimeError) - end + describe 'claims delegated to mapper' do + let(:ci_config_ref_uri) { 'ci_config_ref_uri' } + let(:ci_config_sha) { 'ci_config_sha' } - it 'raises the same error' do - expect { payload }.to raise_error(RuntimeError) + it 'delegates claims to Gitlab::Ci::JwtV2::ClaimMapper' do + expect_next_instance_of(Gitlab::Ci::JwtV2::ClaimMapper, project_config, pipeline) do |mapper| + expect(mapper).to receive(:to_h).and_return({ + ci_config_ref_uri: ci_config_ref_uri, + ci_config_sha: ci_config_sha + }) end - context 'in production' do - before do - stub_rails_env('production') - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end - end - end - - context 'when config source is not repository' do - before do - allow(project_config).to receive(:source).and_return(:auto_devops_source) - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end + expect(payload[:ci_config_ref_uri]).to eq(ci_config_ref_uri) + expect(payload[:ci_config_sha]).to eq(ci_config_sha) end end - describe 'ci_config_sha' do - it 'is the SHA of the pipeline' do - expect(payload[:ci_config_sha]).to eq(pipeline.sha) - end + describe 'project_visibility' do + using RSpec::Parameterized::TableSyntax - context 'when project config is nil' do - before do - allow(Gitlab::Ci::ProjectConfig).to receive(:new).and_return(nil) - end - - it 'is nil' do - expect(payload[:ci_config_sha]).to be_nil - end + where(:visibility_level, :visibility_level_string) do + Project::PUBLIC | 'public' + Project::INTERNAL | 'internal' + Project::PRIVATE | 'private' end - context 'when config source is not repository' do + with_them do before do - allow(project_config).to receive(:source).and_return(:auto_devops_source) + project.visibility_level = visibility_level end - it 'is nil' do - expect(payload[:ci_config_sha]).to be_nil + it 'is a string representation of the project visibility_level' do + expect(payload[:project_visibility]).to eq(visibility_level_string) end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index 9c268d9039e..66e4b987ac1 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -42,9 +42,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: : before do expect(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(pipeline.sha, ci_config_path) - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'builds root config including the local custom file' do @@ -132,9 +132,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: : before do expect(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(pipeline.sha, '.gitlab-ci.yml') - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'builds root config including the canonical CI config file' do diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb index e8a997a7e43..bd95eefe821 100644 --- a/spec/lib/gitlab/ci/project_config/repository_spec.rb +++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Repository, feature_category: :continu context 'when Gitaly raises error' do before do - allow(project.repository).to receive(:gitlab_ci_yml_for).and_raise(GRPC::Internal) + allow(project.repository).to receive(:blob_at).and_raise(GRPC::Internal) end it { is_expected.to be_nil } diff --git a/spec/lib/gitlab/ci/project_config_spec.rb b/spec/lib/gitlab/ci/project_config_spec.rb index 13ef0939ddd..6a4af3c61bf 100644 --- a/spec/lib/gitlab/ci/project_config_spec.rb +++ b/spec/lib/gitlab/ci/project_config_spec.rb @@ -45,9 +45,9 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio before do allow(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(sha, ci_config_path) - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'returns root config including the local custom file' do @@ -122,9 +122,9 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio before do allow(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(sha, '.gitlab-ci.yml') - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'returns root config including the canonical CI config file' do diff --git a/spec/lib/gitlab/ci/queue/metrics_spec.rb b/spec/lib/gitlab/ci/queue/metrics_spec.rb new file mode 100644 index 00000000000..2fb4226ba5a --- /dev/null +++ b/spec/lib/gitlab/ci/queue/metrics_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Queue::Metrics, feature_category: :continuous_integration do + let(:metrics) { described_class.new(build(:ci_runner)) } + + describe '#observe_queue_depth' do + subject { metrics.observe_queue_depth(:found, 1) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + + describe '#observe_queue_size' do + subject { metrics.observe_queue_size(-> { 0 }, :some_runner_type) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + + describe '#observe_queue_time' do + subject { metrics.observe_queue_time(:process, :some_runner_type) { 1 } } + + specify do + expect(described_class).to receive(:queue_iteration_duration_seconds).and_call_original + + subject + end + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + specify do + expect(described_class).not_to receive(:queue_iteration_duration_seconds) + + subject + end + end + + describe '.observe_active_runners' do + subject { described_class.observe_active_runners(-> { 0 }) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb index 5dbcc1991d4..d62d25aeefe 100644 --- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -27,6 +27,154 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen ) end + describe '#name' do + subject { component.name } + + it { is_expected.to eq(name) } + + context 'with namespace' do + let(:purl) do + 'pkg:maven/org.NameSpace/Name@v0.0.1' + end + + it { is_expected.to eq('org.NameSpace/Name') } + + context 'when needing normalization' do + let(:purl) do + 'pkg:pypi/org.NameSpace/Name@v0.0.1' + end + + it { is_expected.to eq('org.namespace/name') } + end + end + end + + describe '#<=>' do + where do + { + 'equal' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 0 + }, + 'name lesser' => { + a_name: 'component-a', + b_name: 'component-b', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-b@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'name greater' => { + a_name: 'component-b', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-b@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'purl type lesser' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:composer/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'purl type greater' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:composer/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'purl type nulls first' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: nil, + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'version lesser' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '2.0.0', + expected: -1 + }, + 'version greater' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '2.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'version nulls first' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: nil, + b_version: '1.0.0', + expected: -1 + } + } + end + + with_them do + specify do + a = described_class.new( + name: a_name, + type: a_type, + purl: a_purl, + version: a_version + ) + + b = described_class.new( + name: b_name, + type: b_type, + purl: b_purl, + version: b_version + ) + + expect(a <=> b).to eq(expected) + end + end + end + describe '#ingestible?' do subject { component.ingestible? } diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 702341a7ea7..34e430202c9 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous end context 'when stage has manual builds' do - Ci::HasStatus::BLOCKED_STATUS.each do |core_status| + (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| context "when status is #{core_status}" do let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb index e23645c106b..fc52b7bf9d4 100644 --- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continu context 'when stage is skipped' do let(:stage) { create(:ci_stage, status: :skipped) } - it { is_expected.to be_falsy } + it { is_expected.to be_truthy } end context 'when stage is manual' do diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb index b72a818c16c..460ecbb05d0 100644 --- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb +++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do subject(:service) { described_class.new(statuses) } describe 'gem version' do - let(:acceptable_version) { '9.0.0' } + let(:acceptable_version) { '9.0.1' } let(:error_message) do <<~MESSAGE diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb index e5324560944..0880c556523 100644 --- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr CI_PIPELINE_IID CI_PIPELINE_SOURCE CI_PIPELINE_CREATED_AT + CI_PIPELINE_NAME CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA @@ -43,6 +44,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr CI_PIPELINE_IID CI_PIPELINE_SOURCE CI_PIPELINE_CREATED_AT + CI_PIPELINE_NAME CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 28c9bdc4c4b..3411426fcdb 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -111,6 +111,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: pipeline.source }, { key: 'CI_PIPELINE_CREATED_AT', value: pipeline.created_at.iso8601 }, + { key: 'CI_PIPELINE_NAME', + value: pipeline.name }, { key: 'CI_COMMIT_SHA', value: job.sha }, { key: 'CI_COMMIT_SHORT_SHA', diff --git a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb index 5b33527e06c..95d0f089f6d 100644 --- a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb @@ -7,13 +7,19 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, f Gitlab::Ci::Variables::Collection.fabricate( [ { key: 'REF1', value: 'ref 1' }, - { key: 'REF2', value: 'ref 2' } + { key: 'REF2', value: 'ref 2' }, + { key: 'NESTED_REF1', value: 'nested $REF1' } ] ) end + let(:expand_file_refs) { false } + let(:context) do - Gitlab::Ci::Variables::Downstream::Generator::Context.new(all_bridge_variables: all_bridge_variables) + Gitlab::Ci::Variables::Downstream::Generator::Context.new( + all_bridge_variables: all_bridge_variables, + expand_file_refs: expand_file_refs + ) end subject(:generator) { described_class.new(context) } @@ -34,5 +40,54 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, f expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 ' }]) end end + + context 'when given a variable with nested interpolation' do + it 'returns an array containing the expanded variables' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1 $REF2 $NESTED_REF1' }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 nested $REF1' }]) + end + end + + context 'when given a variable with expansion on a file variable' do + let(:all_bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF2', value: 'ref 2', file: true }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', file: true } + ] + ) + end + + context 'when expand_file_refs is false' do + let(:expand_file_refs) { false } + + it 'returns an array containing the unexpanded variable and the file variable dependency' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = [ + { key: 'VAR1', value: 'ref 1 $FILE_REF2 $NESTED_REF3' }, + { key: 'FILE_REF2', value: 'ref 2', variable_type: :file }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', variable_type: :file } + ] + + expect(generator.for(var)).to match_array(expected) + end + end + + context 'when expand_file_refs is true' do + let(:expand_file_refs) { true } + + it 'returns an array containing the expanded variables' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = { key: 'VAR1', value: 'ref 1 ref 2 ref 3 $REF1 and $FILE_REF2' } + expect(generator.for(var)).to contain_exactly(expected) + end + end + end end end diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb index 61e8b9a8c4a..cd68b0cdf2b 100644 --- a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb @@ -45,6 +45,7 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : variables: bridge_variables, forward_yaml_variables?: true, forward_pipeline_variables?: true, + expand_file_refs?: false, yaml_variables: yaml_variables, pipeline_variables: pipeline_variables, pipeline_schedule_variables: pipeline_schedule_variables @@ -81,5 +82,61 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : expect(generator.calculate).to be_empty end + + context 'with file variable interpolation' do + let(:bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF3', value: 'ref 3', file: true } + ] + ) + end + + let(:yaml_variables) do + [{ key: 'INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_variables) do + [{ key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_schedule_variables) do + [{ key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + context 'when expand_file_refs is true' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(true) + end + + it 'expands file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + + context 'when expand_file_refs is false' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(false) + end + + it 'does not expand file variables and adds file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'FILE_REF3', value: 'ref 3', variable_type: :file } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index c4e27d0e420..f8f1d71e773 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2675,6 +2675,42 @@ module Gitlab it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings' end + + context 'needs with parallel:matrix' do + let(:config) do + { + build1: { + stage: 'build', + script: 'build', + parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': %w[monitoring app1 app2] }] } + }, + test1: { + stage: 'test', + script: 'test', + needs: [{ job: 'build1', parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': ['app1'] }] } }] + } + } + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(4) + expect(subject.builds[3]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + only: { refs: %w[branches tags] }, + options: { script: ["test"] }, + needs_attributes: [ + { name: "build1: [aws, app1]", artifacts: true, optional: false } + ], + when: "on_success", + allow_failure: false, + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :dag + ) + end + end end context 'with when/rules' do diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb index d03d4f64a0f..56745759c5a 100644 --- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb +++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb @@ -23,26 +23,8 @@ RSpec.describe Gitlab::Cleanup::OrphanJobArtifactFilesBatch do expect(batch.artifact_files.count).to eq(2) expect(batch.lost_and_found.count).to eq(1) expect(batch.lost_and_found.first.artifact_id).to eq(orphan_artifact.id) - end - - it 'does not mix up job ID and artifact ID' do - # take maximum ID of both tables to avoid any collision - max_id = [Ci::Build.maximum(:id), Ci::JobArtifact.maximum(:id)].compact.max.to_i - job_a = create(:ci_build, id: max_id + 1) - job_b = create(:ci_build, id: max_id + 2) - # reuse the build IDs for the job artifact IDs, but swap them - job_artifact_b = create(:ci_job_artifact, :archive, job: job_b, id: max_id + 1) - job_artifact_a = create(:ci_job_artifact, :archive, job: job_a, id: max_id + 2) - - batch << artifact_path(job_artifact_a) - batch << artifact_path(job_artifact_b) - - job_artifact_b.delete - - batch.clean! - - expect(File.exist?(job_artifact_a.file.path)).to be_truthy - expect(File.exist?(job_artifact_b.file.path)).to be_falsey + expect(File.exist?(job_artifact.file.path)).to be_truthy + expect(File.exist?(orphan_artifact.file.path)).to be_falsey end end diff --git a/spec/lib/gitlab/config/entry/validators_spec.rb b/spec/lib/gitlab/config/entry/validators_spec.rb index abf3dbacb3d..6fa9f9d0767 100644 --- a/spec/lib/gitlab/config/entry/validators_spec.rb +++ b/spec/lib/gitlab/config/entry/validators_spec.rb @@ -102,4 +102,37 @@ RSpec.describe Gitlab::Config::Entry::Validators, feature_category: :pipeline_co end end end + + describe described_class::OnlyOneOfKeysValidator do + using RSpec::Parameterized::TableSyntax + + where(:config, :valid_result) do + { foo: '1' } | true + { foo: '1', bar: '2', baz: '3' } | false + { bar: '2' } | true + { foo: '1' } | true + {} | false + { baz: '3' } | false + end + + with_them do + before do + klass.instance_eval do + validates :config, only_one_of_keys: %i[foo bar] + end + + allow(instance).to receive(:config).and_return(config) + end + + it 'validates the instance' do + expect(instance.valid?).to be(valid_result) + + unless valid_result + expect(instance.errors.messages_for(:config)).to( + include "must use exactly one of these keys: foo, bar" + ) + end + end + end + end end diff --git a/spec/lib/gitlab/container_repository/tags/cache_spec.rb b/spec/lib/gitlab/container_repository/tags/cache_spec.rb index 4b8c843eb3a..fcfc8e7a348 100644 --- a/spec/lib/gitlab/container_repository/tags/cache_spec.rb +++ b/spec/lib/gitlab/container_repository/tags/cache_spec.rb @@ -81,9 +81,7 @@ RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_c ::Gitlab::Redis::Cache.with do |redis| expect(redis).to receive(:pipelined).and_call_original - times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1 - - expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline| + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| expect(pipeline) .to receive(:set) .with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i) diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb index b40829d72a0..dd633820ad9 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -2,8 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do +RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :shared do let(:policy) { ActionDispatch::ContentSecurityPolicy.new } + let(:lfs_enabled) { false } + let(:proxy_download) { false } + let(:csp_config) do { enabled: true, @@ -20,6 +23,32 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do } end + let(:lfs_config) do + { + enabled: lfs_enabled, + remote_directory: 'lfs-objects', + connection: object_store_connection_config, + direct_upload: false, + proxy_download: proxy_download, + storage_options: {} + } + end + + let(:object_store_connection_config) do + { + provider: 'AWS', + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY' + } + end + + before do + stub_lfs_setting(enabled: lfs_enabled) + allow(LfsObjectUploader) + .to receive(:object_store_options) + .and_return(GitlabSettings::Options.build(lfs_config)) + end + describe '.default_enabled' do let(:enabled) { described_class.default_enabled } @@ -29,7 +58,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do context 'when in production' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + stub_rails_env('production') end it 'is disabled' do @@ -40,6 +69,16 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do describe '.default_directives' do let(:directives) { described_class.default_directives } + let(:child_src) { directives['child_src'] } + let(:connect_src) { directives['connect_src'] } + let(:font_src) { directives['font_src'] } + let(:frame_src) { directives['frame_src'] } + let(:img_src) { directives['img_src'] } + let(:media_src) { directives['media_src'] } + let(:report_uri) { directives['report_uri'] } + let(:script_src) { directives['script_src'] } + let(:style_src) { directives['style_src'] } + let(:worker_src) { directives['worker_src'] } it 'returns default directives' do directive_names = (described_class::DIRECTIVES - ['report_uri']) @@ -49,68 +88,231 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end expect(directives.has_key?('report_uri')).to be_truthy - expect(directives['report_uri']).to be_nil - expect(directives['child_src']).to eq("#{directives['frame_src']} #{directives['worker_src']}") + expect(report_uri).to be_nil + expect(child_src).to eq("#{frame_src} #{worker_src}") end describe 'the images-src directive' do it 'can be loaded from anywhere' do - expect(directives['img_src']).to include('http: https:') + expect(img_src).to include('http: https:') end end describe 'the media-src directive' do it 'can be loaded from anywhere' do - expect(directives['media_src']).to include('http: https:') + expect(media_src).to include('http: https:') end end - context 'adds all websocket origins to support Safari' do + describe 'Webpack dev server websocket connections' do + let(:webpack_dev_server_host) { 'webpack-dev-server.com' } + let(:webpack_dev_server_port) { '9999' } + let(:webpack_dev_server_https) { true } + + before do + stub_config_setting( + webpack: { dev_server: { + host: webpack_dev_server_host, + webpack_dev_server_port: webpack_dev_server_port, + https: webpack_dev_server_https + } } + ) + end + + context 'when in production' do + before do + stub_rails_env('production') + end + + context 'with secure domain' do + it 'does not include webpack dev server in connect-src' do + expect(connect_src).not_to include(webpack_dev_server_host) + expect(connect_src).not_to include(webpack_dev_server_port) + end + end + + context 'with insecure domain' do + let(:webpack_dev_server_https) { false } + + it 'does not include webpack dev server in connect-src' do + expect(connect_src).not_to include(webpack_dev_server_host) + expect(connect_src).not_to include(webpack_dev_server_port) + end + end + end + + context 'when in development' do + before do + stub_rails_env('development') + end + + context 'with secure domain' do + before do + stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: true) + end + + it 'includes secure websocket url for webpack dev server in connect-src' do + expect(connect_src).to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + expect(connect_src).not_to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + end + end + + context 'with insecure domain' do + before do + stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: false) + end + + it 'includes insecure websocket url for webpack dev server in connect-src' do + expect(connect_src).not_to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + expect(connect_src).to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}") + end + end + end + end + + describe 'Websocket connections' do it 'with insecure domain' do stub_config_setting(host: 'example.com', https: false) - expect(directives['connect_src']).to eq("'self' ws://example.com") + expect(connect_src).to eq("'self' ws://example.com") end it 'with secure domain' do stub_config_setting(host: 'example.com', https: true) - expect(directives['connect_src']).to eq("'self' wss://example.com") + expect(connect_src).to eq("'self' wss://example.com") end it 'with custom port' do stub_config_setting(host: 'example.com', port: '1234') - expect(directives['connect_src']).to eq("'self' ws://example.com:1234") + expect(connect_src).to eq("'self' ws://example.com:1234") end it 'with custom port and secure domain' do stub_config_setting(host: 'example.com', https: true, port: '1234') - expect(directives['connect_src']).to eq("'self' wss://example.com:1234") + expect(connect_src).to eq("'self' wss://example.com:1234") + end + + it 'when port is included in HTTP_PORTS' do + described_class::HTTP_PORTS.each do |port| + stub_config_setting(host: 'example.com', https: true, port: port) + expect(connect_src).to eq("'self' wss://example.com") + end end end - context 'when CDN host is defined' do + describe 'LFS connect-src headers' do + let(:url_for_provider) { described_class.send(:build_lfs_url) } + + context 'when LFS is enabled' do + let(:lfs_enabled) { true } + + context 'and direct downloads are enabled' do + let(:provider) { LfsObjectUploader.object_store_options.connection.provider } + + context 'when provider is AWS' do + it { expect(provider).to eq('AWS') } + + it { expect(url_for_provider).to be_present } + + it { expect(directives['connect_src']).to include(url_for_provider) } + end + + context 'when provider is AzureRM' do + let(:object_store_connection_config) do + { + provider: 'AzureRM', + azure_storage_account_name: 'azuretest', + azure_storage_access_key: 'ABCD1234' + } + end + + it { expect(provider).to eq('AzureRM') } + + it { expect(url_for_provider).to be_present } + + it { expect(directives['connect_src']).to include(url_for_provider) } + end + + context 'when provider is Google' do + let(:object_store_connection_config) do + { + provider: 'Google', + google_project: 'GOOGLE_PROJECT', + google_application_default: true + } + end + + it { expect(provider).to eq('Google') } + + it { expect(url_for_provider).to be_present } + + it { expect(directives['connect_src']).to include(url_for_provider) } + end + end + + context 'but direct downloads are disabled' do + let(:proxy_download) { true } + + it { expect(directives['connect_src']).not_to include(url_for_provider) } + end + end + + context 'when LFS is disabled' do + let(:proxy_download) { true } + + it { expect(directives['connect_src']).not_to include(url_for_provider) } + end + end + + describe 'CDN connections' do before do - stub_config_setting(cdn_host: 'https://cdn.example.com') + allow(described_class).to receive(:allow_letter_opener) + allow(described_class).to receive(:allow_zuora) + allow(described_class).to receive(:allow_framed_gitlab_paths) + allow(described_class).to receive(:allow_customersdot) + allow(described_class).to receive(:csp_level_3_backport) + end + + context 'when CDN host is defined' do + let(:cdn_host) { 'https://cdn.example.com' } + + before do + stub_config_setting(cdn_host: cdn_host) + end + + it 'adds CDN host to CSP' do + expect(script_src).to include(cdn_host) + expect(style_src).to include(cdn_host) + expect(font_src).to include(cdn_host) + expect(worker_src).to include(cdn_host) + expect(frame_src).to include(cdn_host) + end end - it 'adds CDN host to CSP' do - expect(directives['script_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src + " https://cdn.example.com") - expect(directives['style_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src + " https://cdn.example.com") - expect(directives['font_src']).to eq("'self' https://cdn.example.com") - expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com') - expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/") + context 'when CDN host is undefined' do + before do + stub_config_setting(cdn_host: nil) + end + + it 'does not include CDN host in CSP' do + expect(script_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src) + expect(style_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src) + expect(font_src).to eq("'self'") + expect(worker_src).to eq("http://localhost/assets/ blob: data:") + expect(frame_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src) + end end end describe 'Zuora directives' do context 'when on SaaS', :saas do it 'adds Zuora host to CSP' do - expect(directives['frame_src']).to include('https://*.zuora.com/apps/PublicHostedPageLite.do') + expect(frame_src).to include('https://*.zuora.com/apps/PublicHostedPageLite.do') end end context 'when is not Gitlab.com?' do it 'does not add Zuora host to CSP' do - expect(directives['frame_src']).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do') + expect(frame_src).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do') end end end @@ -131,7 +333,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds legacy sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com") end end @@ -143,7 +345,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds new sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com") end end @@ -159,11 +361,22 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'config is backwards compatible, does not add sentry path to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com") + expect(connect_src).to eq("'self' ws://gitlab.example.com") end end context 'when legacy sentry and sentry are both configured' do + let(:connect_src_expectation) do + # rubocop:disable Lint/PercentStringArray + %w[ + 'self' + ws://gitlab.example.com + dummy://legacy-sentry.example.com + dummy://sentry.example.com + ].join(' ') + # rubocop:enable Lint/PercentStringArray + end + before do allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(legacy_dsn) @@ -173,24 +386,57 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds both sentry paths to CSP' do - expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com dummy://sentry.example.com") + expect(connect_src).to eq(connect_src_expectation) end end end - context 'when CUSTOMER_PORTAL_URL is set' do - let(:customer_portal_url) { 'https://customers.example.com' } + describe 'Customer portal frames' do + context 'when CUSTOMER_PORTAL_URL is set' do + let(:customer_portal_url) { 'https://customers.example.com' } + let(:frame_src_expectation) do + [ + ::Gitlab::ContentSecurityPolicy::Directives.frame_src, + 'http://localhost/admin/', + 'http://localhost/assets/', + 'http://localhost/-/speedscope/index.html', + 'http://localhost/-/sandbox/', + customer_portal_url + ].join(' ') + end - before do - stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + before do + stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + end + + it 'adds CUSTOMER_PORTAL_URL to CSP' do + expect(frame_src).to eq(frame_src_expectation) + end end - it 'adds CUSTOMER_PORTAL_URL to CSP' do - expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/ #{customer_portal_url}") + context 'when CUSTOMER_PORTAL_URL is blank' do + let(:customer_portal_url) { '' } + let(:frame_src_expectation) do + [ + ::Gitlab::ContentSecurityPolicy::Directives.frame_src, + 'http://localhost/admin/', + 'http://localhost/assets/', + 'http://localhost/-/speedscope/index.html', + 'http://localhost/-/sandbox/' + ].join(' ') + end + + before do + stub_env('CUSTOMER_PORTAL_URL', customer_portal_url) + end + + it 'adds CUSTOMER_PORTAL_URL to CSP' do + expect(frame_src).to eq(frame_src_expectation) + end end end - context 'letter_opener application URL' do + describe 'letter_opener application URL' do let(:gitlab_url) { 'http://gitlab.example.com' } let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" } @@ -200,21 +446,21 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do context 'when in production' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + stub_rails_env('production') end it 'does not add letter_opener to CSP' do - expect(directives['frame_src']).not_to include(letter_opener_url) + expect(frame_src).not_to include(letter_opener_url) end end context 'when in development' do before do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + stub_rails_env('development') end it 'adds letter_opener to CSP' do - expect(directives['frame_src']).to include(letter_opener_url) + expect(frame_src).to include(letter_opener_url) end end end @@ -234,7 +480,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'does not add Snowplow Micro URL to connect-src' do - expect(directives['connect_src']).not_to include(snowplow_micro_url) + expect(connect_src).not_to include(snowplow_micro_url) end end @@ -244,7 +490,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'adds Snowplow Micro URL with trailing slash to connect-src' do - expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url)) + expect(connect_src).to match(Regexp.new(snowplow_micro_url)) end context 'when not enabled using config' do @@ -253,7 +499,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end it 'does not add Snowplow Micro URL to connect-src' do - expect(directives['connect_src']).not_to include(snowplow_micro_url) + expect(connect_src).not_to include(snowplow_micro_url) end end @@ -262,8 +508,18 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do stub_env('REVIEW_APPS_ENABLED', 'true') end - it 'adds gitlab-org/gitlab merge requests API endpoint to CSP' do - expect(directives['connect_src']).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') + it "includes review app's merge requests API endpoint in the CSP" do + expect(connect_src).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') + end + end + + context 'when REVIEW_APPS_ENABLED is blank' do + before do + stub_env('REVIEW_APPS_ENABLED', '') + end + + it "does not include review app's merge requests API endpoint in the CSP" do + expect(connect_src).not_to include('https://gitlab.com/api/v4/projects/278964/merge_requests/') end end end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 7cd0af0dcec..66890315ee8 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -53,7 +53,9 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do it { expect(data[:runner][:description]).to eq(ci_build.runner.description) } it { expect(data[:runner][:runner_type]).to eq(ci_build.runner.runner_type) } it { expect(data[:runner][:is_shared]).to eq(ci_build.runner.instance_type?) } + it { expect(data[:project]).to eq(ci_build.project.hook_attrs(backward: false)) } it { expect(data[:environment]).to be_nil } + it { expect(data[:source_pipeline]).to be_nil } it 'does not exceed number of expected queries' do ci_build # Make sure the Ci::Build model is created before recording. @@ -63,7 +65,7 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory end - expect(control.count).to eq(14) + expect(control.count).to eq(16) end context 'commit author_url' do @@ -98,5 +100,33 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do it { expect(data[:environment][:action]).to eq(ci_build.environment_action) } end end + + context 'when the build job has an upstream' do + let(:source_pipeline_attrs) { data[:source_pipeline] } + + shared_examples 'source pipeline attributes' do + it 'has source pipeline attributes', :aggregate_failures do + expect(source_pipeline_attrs[:pipeline_id]).to eq upstream_pipeline.id + expect(source_pipeline_attrs[:job_id]).to eq pipeline.reload.source_bridge.id + expect(source_pipeline_attrs[:project][:id]).to eq upstream_pipeline.project.id + expect(source_pipeline_attrs[:project][:web_url]).to eq upstream_pipeline.project.web_url + expect(source_pipeline_attrs[:project][:path_with_namespace]).to eq upstream_pipeline.project.full_path + end + end + + context 'in same project' do + let_it_be(:upstream_pipeline) { create(:ci_pipeline, upstream_of: pipeline, project: ci_build.project) } + + it_behaves_like 'source pipeline attributes' + end + + context 'in different project' do + let_it_be(:upstream_pipeline) { create(:ci_pipeline, upstream_of: pipeline) } + + it_behaves_like 'source pipeline attributes' + + it { expect(source_pipeline_attrs[:project][:id]).not_to eq pipeline.project.id } + end + end end end diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index 82ec3e791a4..bbcfa1973ea 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment, feature_category: :continuous_de subject(:data) { described_class.build(deployment, 'created', Time.current) } - before(:all) do + before_all do project.repository.remove end @@ -74,7 +74,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment, feature_category: :continuous_de subject(:data) { described_class.build(deployment, 'created', Time.current) } - before(:all) do + before_all do deployment.user = nil end diff --git a/spec/lib/gitlab/data_builder/issuable_spec.rb b/spec/lib/gitlab/data_builder/issuable_spec.rb index 455800a3f7d..22c0eb1c7f9 100644 --- a/spec/lib/gitlab/data_builder/issuable_spec.rb +++ b/spec/lib/gitlab/data_builder/issuable_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' RSpec.describe Gitlab::DataBuilder::Issuable do let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:reusable_project) { create(:project, :repository, group: group) } # This shared example requires a `builder` and `user` variable shared_examples 'issuable hook data' do |kind, hook_data_issuable_builder_class| @@ -96,17 +98,17 @@ RSpec.describe Gitlab::DataBuilder::Issuable do describe '#build' do it_behaves_like 'issuable hook data', 'issue', Gitlab::HookData::IssueBuilder do - let(:issuable) { create(:issue, description: 'A description') } + let_it_be(:issuable) { create(:issue, description: 'A description', project: reusable_project) } let(:builder) { described_class.new(issuable) } end it_behaves_like 'issuable hook data', 'merge_request', Gitlab::HookData::MergeRequestBuilder do - let(:issuable) { create(:merge_request, description: 'A description') } + let_it_be(:issuable) { create(:merge_request, description: 'A description', source_project: reusable_project) } let(:builder) { described_class.new(issuable) } end context 'issue is assigned' do - let(:issue) { create(:issue, assignees: [user]) } + let(:issue) { create(:issue, assignees: [user], project: reusable_project) } let(:data) { described_class.new(issue).build(user: user) } it 'returns correct hook data' do @@ -117,8 +119,21 @@ RSpec.describe Gitlab::DataBuilder::Issuable do end end + context 'when issuable is a group level work item' do + let(:work_item) { create(:work_item, namespace: group, description: 'work item description') } + + it 'returns correct hook data', :aggregate_failures do + data = described_class.new(work_item).build(user: user) + + expect(data[:object_kind]).to eq('work_item') + expect(data[:event_type]).to eq('work_item') + expect(data.dig(:object_attributes, :id)).to eq(work_item.id) + expect(data.dig(:object_attributes, :iid)).to eq(work_item.iid) + end + end + context 'merge_request is assigned' do - let(:merge_request) { create(:merge_request, assignees: [user]) } + let(:merge_request) { create(:merge_request, assignees: [user], source_project: reusable_project) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do @@ -129,7 +144,7 @@ RSpec.describe Gitlab::DataBuilder::Issuable do end context 'merge_request is assigned reviewers' do - let(:merge_request) { create(:merge_request, reviewers: [user]) } + let(:merge_request) { create(:merge_request, reviewers: [user], source_project: reusable_project) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do @@ -139,7 +154,7 @@ RSpec.describe Gitlab::DataBuilder::Issuable do end context 'when merge_request does not have reviewers and assignees' do - let(:merge_request) { create(:merge_request) } + let(:merge_request) { create(:merge_request, source_project: reusable_project) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do diff --git a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb index 52fbf6d2f9b..02b84085cc4 100644 --- a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb +++ b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb @@ -80,12 +80,16 @@ RSpec.describe Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValida it { expect(described_class.constraint_type_exists?).to be_truthy } it 'always asks the database' do - control = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do + control1 = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do described_class.constraint_type_exists? end - expect(control.count).to be >= 1 - expect { described_class.constraint_type_exists? }.to issue_same_number_of_queries_as(control) + control2 = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do + described_class.constraint_type_exists? + end + + expect(control1.count).to eq(1) + expect(control2.count).to eq(1) end end diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index 53f8fe3dcd2..89652b81fde 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BatchCount do let_it_be(:fallback) { ::Gitlab::Database::BatchCounter::FALLBACK } let_it_be(:small_batch_size) { calculate_batch_size(::Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE) } + let_it_be(:max_allowed_loops) { ::Gitlab::Database::BatchCounter::MAX_ALLOWED_LOOPS } let(:model) { Issue } let(:column) { :author_id } @@ -34,7 +35,7 @@ RSpec.describe Gitlab::Database::BatchCount do end it 'returns fallback if loops more than allowed' do - large_finish = Gitlab::Database::BatchCounter::MAX_ALLOWED_LOOPS * default_batch_size + 1 + large_finish = max_allowed_loops * default_batch_size + 1 expect(described_class.public_send(method, *args, start: 1, finish: large_finish)).to eq(fallback) end @@ -81,6 +82,7 @@ RSpec.describe Gitlab::Database::BatchCount do relation: model.table_name, operation: operation, operation_args: operation_args, + max_allowed_loops: max_allowed_loops, start: 0, mode: mode, query: batch_count_query, diff --git a/spec/lib/gitlab/database/bump_sequences_spec.rb b/spec/lib/gitlab/database/bump_sequences_spec.rb new file mode 100644 index 00000000000..db420123350 --- /dev/null +++ b/spec/lib/gitlab/database/bump_sequences_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BumpSequences, feature_category: :cell, query_analyzers: false do + let!(:gitlab_schema) { :gitlab_main_cell } + let!(:increment_by) { 1000 } + + let!(:main_cell_sequence_name) { 'namespaces_id_seq' } + let!(:main_sequence_name) { 'vulnerabilities_id_seq' } + let!(:main_clusterwide_sequence_name) { 'users_id_seq' } + let!(:ci_sequence_name) { 'ci_build_needs_id_seq' } + + # This is just to make sure that all of the sequences start with `is_called=True` + # which means that the next call to nextval() is going to increment the sequence. + # To give predictable test results. + before do + ApplicationRecord.connection.select_value("select nextval($1)", nil, [main_cell_sequence_name]) + ApplicationRecord.connection.select_value("select nextval($1)", nil, [main_sequence_name]) + ApplicationRecord.connection.select_value("select nextval($1)", nil, [main_clusterwide_sequence_name]) + ApplicationRecord.connection.select_value("select nextval($1)", nil, [ci_sequence_name]) + end + + describe '#execute' do + subject { described_class.new(gitlab_schema, increment_by).execute } + + context 'when bumping the sequences' do + it 'changes sequences by the passed argument `increase_by` value on the main database' do + expect do + subject + end.to change { + last_value_of_sequence(ApplicationRecord.connection, main_cell_sequence_name) + }.by(1001) # the +1 is because the sequence has is_called = true + end + + it 'will still increase the value of sequences that have is_called = False' do + # see `is_called`: https://www.postgresql.org/docs/12/functions-sequence.html + # choosing a new arbitrary value for the sequence + new_value = last_value_of_sequence(ApplicationRecord.connection, main_cell_sequence_name) + 1000 + ApplicationRecord.connection.select_value( + "select setval($1, $2, false)", nil, [main_cell_sequence_name, new_value] + ) + expect do + subject + end.to change { + last_value_of_sequence(ApplicationRecord.connection, main_cell_sequence_name) + }.by(1000) + end + + it 'resets the INCREMENT value of the sequences back to 1 for the following calls to nextval()' do + subject + value_1 = ApplicationRecord.connection.select_value("select nextval($1)", nil, [main_cell_sequence_name]) + value_2 = ApplicationRecord.connection.select_value("select nextval($1)", nil, [main_cell_sequence_name]) + expect(value_2 - value_1).to eq(1) + end + + it 'increments the sequence of the tables in the given schema, but not in other schemas' do + expect do + subject + end.to change { + last_value_of_sequence(ApplicationRecord.connection, main_cell_sequence_name) + }.by(1001) + .and change { + last_value_of_sequence(ApplicationRecord.connection, main_sequence_name) + }.by(0) + .and change { + last_value_of_sequence(ApplicationRecord.connection, main_clusterwide_sequence_name) + }.by(0) + .and change { + last_value_of_sequence(ApplicationRecord.connection, ci_sequence_name) + }.by(0) + end + end + end + + private + + def last_value_of_sequence(connection, sequence_name) + allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408220') do + connection.select_value("select last_value from #{sequence_name}") + end + end +end diff --git a/spec/lib/gitlab/database/click_house_client_spec.rb b/spec/lib/gitlab/database/click_house_client_spec.rb index 502d879bf6a..50086795b2b 100644 --- a/spec/lib/gitlab/database/click_house_client_spec.rb +++ b/spec/lib/gitlab/database/click_house_client_spec.rb @@ -12,25 +12,12 @@ RSpec.describe 'ClickHouse::Client', feature_category: :database do end describe 'when click_house spec tag is added', :click_house do - around do |example| - with_net_connect_allowed do - example.run - end - end - it 'has a ClickHouse database configured' do databases = ClickHouse::Client.configuration.databases expect(databases).not_to be_empty end - it 'returns data from the DB via `select` method' do - result = ClickHouse::Client.select("SELECT 1 AS value", :main) - - # returns JSON if successful. Otherwise error - expect(result).to eq([{ 'value' => 1 }]) - end - it 'does not return data via `execute` method' do result = ClickHouse::Client.execute("SELECT 1 AS value", :main) diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb index 1c864239ae6..14ff1a462e3 100644 --- a/spec/lib/gitlab/database/gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb @@ -148,7 +148,7 @@ RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do subject { described_class.table_schemas!(tables) } it 'returns the matched schemas' do - expect(subject).to match_array %i[gitlab_main gitlab_ci].to_set + expect(subject).to match_array %i[gitlab_main_cell gitlab_main gitlab_ci].to_set end context 'when one of the tables does not have a matching table schema' do diff --git a/spec/lib/gitlab/database/health_status/indicators/patroni_apdex_spec.rb b/spec/lib/gitlab/database/health_status/indicators/patroni_apdex_spec.rb index e0e3a0a7c23..9382074f584 100644 --- a/spec/lib/gitlab/database/health_status/indicators/patroni_apdex_spec.rb +++ b/spec/lib/gitlab/database/health_status/indicators/patroni_apdex_spec.rb @@ -3,150 +3,27 @@ require 'spec_helper' RSpec.describe Gitlab::Database::HealthStatus::Indicators::PatroniApdex, :aggregate_failures, feature_category: :database do # rubocop:disable Layout/LineLength - let(:schema) { :main } - let(:connection) { Gitlab::Database.database_base_models[schema].connection } - - around do |example| - Gitlab::Database::SharedModel.using_connection(connection) do - example.run - end - end - - describe '#evaluate' do - let(:prometheus_url) { 'http://thanos:9090' } - let(:prometheus_config) { [prometheus_url, { allow_local_requests: true, verify: true }] } - - let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) } - - let(:context) do - Gitlab::Database::HealthStatus::Context.new( - described_class, - connection, - ['users'], - gitlab_schema - ) - end - - let(:gitlab_schema) { "gitlab_#{schema}" } - let(:client_ready) { true } - let(:database_apdex_sli_query_main) { 'Apdex query for main' } - let(:database_apdex_sli_query_ci) { 'Apdex query for ci' } - let(:database_apdex_slo_main) { 0.99 } - let(:database_apdex_slo_ci) { 0.95 } - let(:database_apdex_settings) do + it_behaves_like 'Prometheus Alert based health indicator' do + let(:feature_flag) { :batched_migrations_health_status_patroni_apdex } + let(:sli_query_main) { 'Apdex query for main' } + let(:sli_query_ci) { 'Apdex query for ci' } + let(:slo_main) { 0.99 } + let(:slo_ci) { 0.95 } + let(:sli_with_good_condition) { { main: 0.991, ci: 0.951 } } + let(:sli_with_bad_condition) { { main: 0.989, ci: 0.949 } } + + let(:prometheus_alert_db_indicators_settings) do { prometheus_api_url: prometheus_url, apdex_sli_query: { - main: database_apdex_sli_query_main, - ci: database_apdex_sli_query_ci + main: sli_query_main, + ci: sli_query_ci }, apdex_slo: { - main: database_apdex_slo_main, - ci: database_apdex_slo_ci + main: slo_main, + ci: slo_ci } } end - - subject(:evaluate) { described_class.new(context).evaluate } - - before do - stub_application_setting(database_apdex_settings: database_apdex_settings) - - allow(Gitlab::PrometheusClient).to receive(:new).with(*prometheus_config).and_return(prometheus_client) - allow(prometheus_client).to receive(:ready?).and_return(client_ready) - end - - shared_examples 'Patroni Apdex Evaluator' do |schema| - context "with #{schema} schema" do - let(:schema) { schema } - let(:apdex_slo_above_sli) { { main: 0.991, ci: 0.951 } } - let(:apdex_slo_below_sli) { { main: 0.989, ci: 0.949 } } - - it 'returns NoSignal signal in case the feature flag is disabled' do - stub_feature_flags(batched_migrations_health_status_patroni_apdex: false) - - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::NotAvailable) - expect(evaluate.reason).to include('indicator disabled') - end - - context 'without database_apdex_settings' do - let(:database_apdex_settings) { nil } - - it 'returns Unknown signal' do - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to include('Patroni Apdex Settings not configured') - end - end - - context 'when Prometheus client is not ready' do - let(:client_ready) { false } - - it 'returns Unknown signal' do - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to include('Prometheus client is not ready') - end - end - - context 'when apdex SLI query is not configured' do - let(:"database_apdex_sli_query_#{schema}") { nil } - - it 'returns Unknown signal' do - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to include('Apdex SLI query is not configured') - end - end - - context 'when slo is not configured' do - let(:"database_apdex_slo_#{schema}") { nil } - - it 'returns Unknown signal' do - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to include('Apdex SLO is not configured') - end - end - - it 'returns Normal signal when Patroni apdex SLI is above SLO' do - expect(prometheus_client).to receive(:query) - .with(send("database_apdex_sli_query_#{schema}")) - .and_return([{ "value" => [1662423310.878, apdex_slo_above_sli[schema]] }]) - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Normal) - expect(evaluate.reason).to include('Patroni service apdex is above SLO') - end - - it 'returns Stop signal when Patroni apdex is below SLO' do - expect(prometheus_client).to receive(:query) - .with(send("database_apdex_sli_query_#{schema}")) - .and_return([{ "value" => [1662423310.878, apdex_slo_below_sli[schema]] }]) - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Stop) - expect(evaluate.reason).to include('Patroni service apdex is below SLO') - end - - context 'when Patroni apdex can not be calculated' do - where(:result) do - [ - nil, - [], - [{}], - [{ 'value' => 1 }], - [{ 'value' => [1] }] - ] - end - - with_them do - it 'returns Unknown signal' do - expect(prometheus_client).to receive(:query).and_return(result) - expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to include('Patroni service apdex can not be calculated') - end - end - end - end - end - - Gitlab::Database.database_base_models.each do |database_base_model, connection| - next unless connection.present? - - it_behaves_like 'Patroni Apdex Evaluator', database_base_model.to_sym - end end end diff --git a/spec/lib/gitlab/database/health_status/indicators/prometheus_alert_indicator_spec.rb b/spec/lib/gitlab/database/health_status/indicators/prometheus_alert_indicator_spec.rb new file mode 100644 index 00000000000..393bbf6beff --- /dev/null +++ b/spec/lib/gitlab/database/health_status/indicators/prometheus_alert_indicator_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::HealthStatus::Indicators::PrometheusAlertIndicator, :aggregate_failures, feature_category: :database do # rubocop:disable Layout/LineLength + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + let(:context) do + Gitlab::Database::HealthStatus::Context.new( + described_class, + connection, + ['users'], + :gitlab_main + ) + end + + let(:invalid_indicator) do + Class.new(described_class).new(context) + end + + let(:valid_indicator) do + Class.new(described_class) do + def enabled? + true + end + + def slo_key + :test_indicator_slo + end + + def sli_key + :test_indicator_sli + end + end.new(context) + end + + describe '#enabled?' do + it 'throws NotImplementedError for invalid indicator' do + expect { invalid_indicator.send(:enabled?) }.to raise_error(NotImplementedError) + end + + it 'returns the defined value for valid indicator' do + expect(valid_indicator.send(:enabled?)).to eq(true) + end + end + + describe '#slo_key' do + it 'throws NotImplementedError for invalid indicator' do + expect { invalid_indicator.send(:slo_key) }.to raise_error(NotImplementedError) + end + + it 'returns the defined value for valid indicator' do + expect(valid_indicator.send(:slo_key)).to eq(:test_indicator_slo) + end + end + + describe '#sli_key' do + it 'throws NotImplementedError for invalid indicator' do + expect { invalid_indicator.send(:sli_key) }.to raise_error(NotImplementedError) + end + + it 'returns the defined value for valid indicator' do + expect(valid_indicator.send(:sli_key)).to eq(:test_indicator_sli) + end + end +end diff --git a/spec/lib/gitlab/database/health_status/indicators/wal_rate_spec.rb b/spec/lib/gitlab/database/health_status/indicators/wal_rate_spec.rb new file mode 100644 index 00000000000..d6fe7f0cead --- /dev/null +++ b/spec/lib/gitlab/database/health_status/indicators/wal_rate_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::HealthStatus::Indicators::WalRate, :aggregate_failures, feature_category: :database do # rubocop:disable Layout/LineLength + it_behaves_like 'Prometheus Alert based health indicator' do + let(:feature_flag) { :db_health_check_wal_rate } + let(:sli_query_main) { 'WAL rate query for main' } + let(:sli_query_ci) { 'WAL rate query for ci' } + let(:slo_main) { 100 } + let(:slo_ci) { 100 } + let(:sli_with_good_condition) { { main: 70, ci: 70 } } + let(:sli_with_bad_condition) { { main: 120, ci: 120 } } + + let(:prometheus_alert_db_indicators_settings) do + { + prometheus_api_url: prometheus_url, + wal_rate_sli_query: { + main: sli_query_main, + ci: sli_query_ci + }, + wal_rate_slo: { + main: slo_main, + ci: slo_ci + } + } + end + end +end diff --git a/spec/lib/gitlab/database/health_status_spec.rb b/spec/lib/gitlab/database/health_status_spec.rb index 4a2b9eee45a..95f74929b84 100644 --- a/spec/lib/gitlab/database/health_status_spec.rb +++ b/spec/lib/gitlab/database/health_status_spec.rb @@ -21,9 +21,11 @@ RSpec.describe Gitlab::Database::HealthStatus, feature_category: :database do let(:autovacuum_indicator_class) { health_status::Indicators::AutovacuumActiveOnTable } let(:wal_indicator_class) { health_status::Indicators::WriteAheadLog } let(:patroni_apdex_indicator_class) { health_status::Indicators::PatroniApdex } + let(:wal_rate_indicator_class) { health_status::Indicators::WalRate } let(:autovacuum_indicator) { instance_double(autovacuum_indicator_class) } let(:wal_indicator) { instance_double(wal_indicator_class) } let(:patroni_apdex_indicator) { instance_double(patroni_apdex_indicator_class) } + let(:wal_rate_indicator) { instance_double(wal_rate_indicator_class) } before do allow(autovacuum_indicator_class).to receive(:new).with(health_context).and_return(autovacuum_indicator) @@ -39,11 +41,17 @@ RSpec.describe Gitlab::Database::HealthStatus, feature_category: :database do expect(autovacuum_indicator).to receive(:evaluate).and_return(normal_signal) expect(wal_indicator_class).to receive(:new).with(health_context).and_return(wal_indicator) expect(wal_indicator).to receive(:evaluate).and_return(not_available_signal) - expect(patroni_apdex_indicator_class).to receive(:new).with(health_context) - .and_return(patroni_apdex_indicator) + expect(patroni_apdex_indicator_class).to receive(:new).with(health_context).and_return(patroni_apdex_indicator) expect(patroni_apdex_indicator).to receive(:evaluate).and_return(not_available_signal) - - expect(evaluate).to contain_exactly(normal_signal, not_available_signal, not_available_signal) + expect(wal_rate_indicator_class).to receive(:new).with(health_context).and_return(wal_rate_indicator) + expect(wal_rate_indicator).to receive(:evaluate).and_return(not_available_signal) + + expect(evaluate).to contain_exactly( + normal_signal, + not_available_signal, + not_available_signal, + not_available_signal + ) end end diff --git a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb index cee5f54bd6a..1ff157b51d4 100644 --- a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb @@ -3,7 +3,15 @@ require 'spec_helper' RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_category: :database do - describe 'com_or_dev_or_test_but_not_jh?' do + let(:migration) do + Class + .new + .include(described_class) + .include(Gitlab::Database::MigrationHelpers) + .new + end + + describe '#com_or_dev_or_test_but_not_jh?' do using RSpec::Parameterized::TableSyntax where(:dot_com, :dev_or_test, :jh, :expectation) do @@ -23,13 +31,46 @@ RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_cate allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test) allow(Gitlab).to receive(:jh?).and_return(jh) - migration = Class - .new - .include(Gitlab::Database::MigrationHelpers::ConvertToBigint) - .new - expect(migration.com_or_dev_or_test_but_not_jh?).to eq(expectation) end end end + + describe '#temp_column_removed?' do + it 'return true when column is not present' do + expect(migration).to receive(:column_exists?).with('test_table', 'id_convert_to_bigint').and_return(false) + + expect(migration.temp_column_removed?(:test_table, :id)).to eq(true) + end + + it 'return false when column present' do + expect(migration).to receive(:column_exists?).with('test_table', 'id_convert_to_bigint').and_return(true) + + expect(migration.temp_column_removed?(:test_table, :id)).to eq(false) + end + end + + describe '#columns_swapped?' do + it 'returns true if columns are already swapped' do + columns = [ + Struct.new(:name, :sql_type).new('id', 'bigint'), + Struct.new(:name, :sql_type).new('id_convert_to_bigint', 'integer') + ] + + expect(migration).to receive(:columns).with('test_table').and_return(columns) + + expect(migration.columns_swapped?(:test_table, :id)).to eq(true) + end + + it 'returns false if columns are not yet swapped' do + columns = [ + Struct.new(:name, :sql_type).new('id', 'integer'), + Struct.new(:name, :sql_type).new('id_convert_to_bigint', 'bigint') + ] + + expect(migration).to receive(:columns).with('test_table').and_return(columns) + + expect(migration.columns_swapped?(:test_table, :id)).to eq(false) + end + end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index b1e8301d69f..f3c181db3aa 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2867,4 +2867,43 @@ RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database d it { is_expected.to be_falsey } end end + + describe '#remove_column_default' do + let(:test_table) { :_test_defaults_table } + let(:drop_default_statement) do + /ALTER TABLE "#{test_table}" ALTER COLUMN "#{column_name}" SET DEFAULT NULL/ + end + + subject(:recorder) do + ActiveRecord::QueryRecorder.new do + model.remove_column_default(test_table, column_name) + end + end + + before do + model.create_table(test_table) do |t| + t.integer :int_with_default, default: 100 + t.integer :int_with_default_function, default: -> { 'ceil(random () * 100)::int' } + t.integer :int_without_default + end + end + + context 'with default values' do + let(:column_name) { :int_with_default } + + it { expect(recorder.log).to include(drop_default_statement) } + end + + context 'with default functions' do + let(:column_name) { :int_with_default_function } + + it { expect(recorder.log).to include(drop_default_statement) } + end + + context 'without any defaults' do + let(:column_name) { :int_without_default } + + it { expect(recorder.log).to be_empty } + end + end end diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index 82f77d2bb19..158497b1fef 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -473,7 +473,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d "\n\n" \ "For more information, check the documentation" \ "\n\n" \ - "\thttps://docs.gitlab.com/ee/user/admin_area/monitoring/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished" + "\thttps://docs.gitlab.com/ee/update/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished" end it 'does not raise error when migration exists and is marked as finished' do diff --git a/spec/lib/gitlab/database/migrations/squasher_spec.rb b/spec/lib/gitlab/database/migrations/squasher_spec.rb new file mode 100644 index 00000000000..e7ab5873f73 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/squasher_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' +RSpec.describe Gitlab::Database::Migrations::Squasher, feature_category: :database do + let(:git_output) do + <<~FILES + db/migrate/misplaced.txt + db/migrate/20221003041700_init_schema.rb + db/migrate/20221003041800_foo_migrate.rb + db/migrate/20221003041900_foo_migrate_two.rb + db/migrate/20221003042000_add_name_to_widgets.rb + db/migrate/20221003042200_add_enterprise.rb + db/post_migrate/20221003042100_post_migrate.rb + FILES + end + + let(:spec_files) do + [ + 'spec/migrations/add_name_to_widgets_spec.rb', + 'spec/migrations/20221003041800_foo_migrate_spec.rb', + 'spec/migrations/foo_migrate_three_spec.rb', + 'spec/migrations/foo_migrate_two_spec.rb', + 'spec/migrations/post_migrate_spec.rb' + ] + end + + let(:ee_spec_files) do + [ + 'ee/spec/migrations/add_enterprise_spec.rb' + ] + end + + let(:expected_list) do + [ + 'db/migrate/20221003041800_foo_migrate.rb', + 'db/migrate/20221003041900_foo_migrate_two.rb', + 'db/migrate/20221003042000_add_name_to_widgets.rb', + 'spec/migrations/add_name_to_widgets_spec.rb', + 'spec/migrations/20221003041800_foo_migrate_spec.rb', + 'spec/migrations/foo_migrate_two_spec.rb', + 'db/schema_migrations/20221003041800', + 'db/schema_migrations/20221003041900', + 'db/schema_migrations/20221003042000', + 'db/schema_migrations/20221003042100', + 'db/schema_migrations/20221003042200', + 'db/post_migrate/20221003042100_post_migrate.rb', + 'spec/migrations/post_migrate_spec.rb', + 'ee/spec/migrations/add_enterprise_spec.rb', + 'db/migrate/20221003042200_add_enterprise.rb' + ] + end + + describe "#files_to_delete" do + before do + allow(Dir).to receive(:glob).with(Rails.root.join('spec/migrations/*.rb')).and_return(spec_files) + allow(Dir).to receive(:glob).with(Rails.root.join('ee/spec/migrations/*.rb')).and_return(ee_spec_files) + end + + let(:squasher) { described_class.new(git_output) } + + it 'only deletes the files we\'re expecting' do + expect(squasher.files_to_delete).to match_array expected_list + end + end +end diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb index 7899c1588b2..6cac7abb703 100644 --- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb @@ -3,12 +3,27 @@ require 'spec_helper' RSpec.describe 'cross-database foreign keys' do - # Since we don't expect to have any cross-database foreign keys - # this is empty. If we will have an entry like - # `ci_daily_build_group_report_results.project_id` - # should be added. - let(:allowed_cross_database_foreign_keys) do - %w[].freeze + # While we are building out Cells, we will be moving tables from gitlab_main schema + # to either gitlab_main_clusterwide schema or gitlab_main_cell schema. + # During this transition phase, cross database foreign keys need + # to be temporarily allowed to exist, until we can work on converting these columns to loose foreign keys. + # The issue corresponding to the loose foreign key conversion + # should be added as a comment along with the name of the column. + let!(:allowed_cross_database_foreign_keys) do + [ + 'gitlab_subscriptions.hosted_plan_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422012 + 'group_import_states.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421210 + 'identities.saml_provider_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422010 + 'project_authorizations.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422044 + 'merge_requests.assignee_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 + 'merge_requests.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 + 'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 + 'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080 + 'projects.creator_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844 + 'projects.marked_for_deletion_by_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844 + 'routes.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/420869 + 'user_group_callouts.user_id' # https://gitlab.com/gitlab-org/gitlab/-/issues/421287 + ] end def foreign_keys_for(table_name) diff --git a/spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb index 399fcae2fa0..3650ca1d904 100644 --- a/spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb +++ b/spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::PostgresqlAdapter::ForceDisconnectableMixin, :reestablished_active_record_base do +RSpec.describe Gitlab::Database::PostgresqlAdapter::ForceDisconnectableMixin, :delete, :reestablished_active_record_base do describe 'checking in a connection to the pool' do let(:model) do Class.new(ActiveRecord::Base) do @@ -32,14 +32,29 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::ForceDisconnectableMixin, :r let(:timer) { connection.force_disconnect_timer } context 'when the timer is expired' do - it 'disconnects from the database' do + before do allow(timer).to receive(:expired?).and_return(true) + end + it 'disconnects from the database' do expect(connection).to receive(:disconnect!).and_call_original expect(timer).to receive(:reset!).and_call_original connection.force_disconnect_if_old! end + + context 'when the connection has an open transaction' do + it 'does not disconnect from the database' do + connection.begin_transaction + + expect(connection).not_to receive(:disconnect!) + expect(timer).not_to receive(:reset!) + + connection.force_disconnect_if_old! + + connection.rollback_transaction + end + end end context 'when the timer is not expired' do diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb index b5e08f58608..f325060e592 100644 --- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb +++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana model: ApplicationRecord, sql: "SELECT 1 FROM projects", expectations: { - gitlab_schemas: "gitlab_main", + gitlab_schemas: "gitlab_main_cell", db_config_name: "main" } }, @@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana model: ApplicationRecord, sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id", expectations: { - gitlab_schemas: "gitlab_ci,gitlab_main", + gitlab_schemas: "gitlab_ci,gitlab_main_cell", db_config_name: "main" } }, @@ -45,7 +45,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana model: ApplicationRecord, sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id", expectations: { - gitlab_schemas: "gitlab_ci,gitlab_main", + gitlab_schemas: "gitlab_ci,gitlab_main_cell", db_config_name: "main" } }, diff --git a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb deleted file mode 100644 index 22ff66ff55e..00000000000 --- a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::QueryAnalyzers::QueryRecorder, feature_category: :database, query_analyzers: false do - # We keep only the QueryRecorder analyzer running - around do |example| - described_class.with_suppressed(false) do - example.run - end - end - - context 'with query analyzer' do - let(:log_path) { Rails.root.join(described_class::LOG_PATH) } - let(:log_file) { described_class.log_file } - - after do - ::Gitlab::Database::QueryAnalyzer.instance.end!([described_class]) - end - - shared_examples_for 'an enabled query recorder' do - using RSpec::Parameterized::TableSyntax - - normalized_query = <<~SQL.strip.tr("\n", ' ') - SELECT \\\\"projects\\\\".\\\\"id\\\\" - FROM \\\\"projects\\\\" - WHERE \\\\"projects\\\\".\\\\"namespace_id\\\\" = \\? - AND \\\\"projects\\\\".\\\\"id\\\\" IN \\(\\?,\\?,\\?\\); - SQL - - where(:list_parameter, :bind_parameters) do - '$2, $3' | [1, 2, 3] - '$2, $3, $4' | [1, 2, 3, 4] - '$2 ,$3 ,$4 ,$5' | [1, 2, 3, 4, 5] - '$2 , $3 , $4 , $5, $6' | [1, 2, 3, 4, 5, 6] - '$2, $3 ,$4 , $5,$6,$7' | [1, 2, 3, 4, 5, 6, 7] - '$2,$3,$4,$5,$6,$7,$8' | [1, 2, 3, 4, 5, 6, 7, 8] - end - - with_them do - before do - allow(described_class).to receive(:analyze).and_call_original - allow(FileUtils).to receive(:mkdir_p) - .with(log_path) - end - - it 'logs normalized queries to a file' do - expect(File).to receive(:write) - .with(log_file, /^{"normalized":"#{normalized_query}/, mode: 'a') - - expect do - ApplicationRecord.connection.exec_query(<<~SQL.strip.tr("\n", ' '), 'SQL', bind_parameters) - SELECT "projects"."id" - FROM "projects" - WHERE "projects"."namespace_id" = $1 - AND "projects"."id" IN (#{list_parameter}); - SQL - end.not_to raise_error - end - end - end - - context 'on default branch' do - before do - stub_env('CI_MERGE_REQUEST_LABELS', nil) - stub_env('CI_DEFAULT_BRANCH', 'default_branch_name') - stub_env('CI_COMMIT_REF_NAME', 'default_branch_name') - - # This is needed to be able to stub_env the CI variable - ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class]) - end - - it_behaves_like 'an enabled query recorder' - end - - context 'on database merge requests' do - before do - stub_env('CI_MERGE_REQUEST_LABELS', 'database') - - # This is needed to be able to stub_env the CI variable - ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class]) - end - - it_behaves_like 'an enabled query recorder' - end - end - - describe '.log_file' do - let(:folder) { 'query_recorder' } - let(:extension) { 'ndjson' } - let(:default_name) { 'rspec' } - let(:job_name) { 'test-job-1' } - - subject { described_class.log_file.to_s } - - context 'when in CI' do - before do - stub_env('CI_JOB_NAME_SLUG', job_name) - end - - it { is_expected.to include("#{folder}/#{job_name}.#{extension}") } - it { is_expected.not_to include("#{folder}/#{default_name}.#{extension}") } - end - - context 'when not in CI' do - before do - stub_env('CI_JOB_NAME_SLUG', nil) - end - - it { is_expected.to include("#{folder}/#{default_name}.#{extension}") } - it { is_expected.not_to include("#{folder}/#{job_name}.#{extension}") } - end - end -end diff --git a/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb deleted file mode 100644 index fbaf8474f22..00000000000 --- a/spec/lib/gitlab/database/schema_validation/schema_inconsistency_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::SchemaValidation::SchemaInconsistency, type: :model, feature_category: :database do - it { is_expected.to be_a ApplicationRecord } - - describe 'associations' do - it { is_expected.to belong_to(:issue) } - end - - describe "Validations" do - it { is_expected.to validate_presence_of(:object_name) } - it { is_expected.to validate_presence_of(:valitador_name) } - it { is_expected.to validate_presence_of(:table_name) } - it { is_expected.to validate_presence_of(:diff) } - end - - describe 'scopes' do - describe '.with_open_issues' do - subject(:inconsistencies) { described_class.with_open_issues } - - let(:closed_issue) { create(:issue, :closed) } - let(:open_issue) { create(:issue, :opened) } - - let!(:schema_inconsistency_with_issue_closed) do - create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements', - valitador_name: 'different_definition_indexes', issue: closed_issue) - end - - let!(:schema_inconsistency_with_issue_opened) do - create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements', - valitador_name: 'different_definition_indexes', issue: open_issue) - end - - it 'returns only schema inconsistencies with GitLab issues open' do - expect(inconsistencies).to eq([schema_inconsistency_with_issue_opened]) - end - end - end -end diff --git a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb index aa25590ed58..70352775fe5 100644 --- a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do - let(:connection) { ApplicationRecord.connection } +RSpec.describe Gitlab::Database::TablesSortedByForeignKeys, feature_category: :cell do + let(:connection) { Ci::ApplicationRecord.connection } let(:tables) do %w[_test_gitlab_main_items _test_gitlab_main_references _test_gitlab_partition_parent - gitlab_partitions_dynamic._test_gitlab_partition_20220101] + gitlab_partitions_dynamic._test_gitlab_partition_20220101 + gitlab_partitions_dynamic._test_gitlab_partition_20220102] end subject do @@ -35,7 +36,18 @@ RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do PARTITION OF _test_gitlab_partition_parent FOR VALUES FROM ('20220101') TO ('20220131'); + CREATE TABLE gitlab_partitions_dynamic._test_gitlab_partition_20220102 + PARTITION OF _test_gitlab_partition_parent + FOR VALUES FROM ('20220201') TO ('20220228'); + ALTER TABLE _test_gitlab_partition_parent DETACH PARTITION gitlab_partitions_dynamic._test_gitlab_partition_20220101; + ALTER TABLE _test_gitlab_partition_parent DETACH PARTITION gitlab_partitions_dynamic._test_gitlab_partition_20220102; + + /* For some reason FK is now created in gitlab_partitions_dynamic */ + ALTER TABLE gitlab_partitions_dynamic._test_gitlab_partition_20220101 + DROP CONSTRAINT fk_constrained_1; + ALTER TABLE gitlab_partitions_dynamic._test_gitlab_partition_20220101 + ADD CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id); SQL connection.execute(statement) end @@ -47,6 +59,7 @@ RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do ['_test_gitlab_main_references'], ['_test_gitlab_partition_parent'], ['gitlab_partitions_dynamic._test_gitlab_partition_20220101'], + ['gitlab_partitions_dynamic._test_gitlab_partition_20220102'], ['_test_gitlab_main_items'] ]) end @@ -62,6 +75,7 @@ RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do [ ['_test_gitlab_partition_parent'], ['gitlab_partitions_dynamic._test_gitlab_partition_20220101'], + ['gitlab_partitions_dynamic._test_gitlab_partition_20220102'], %w[_test_gitlab_main_items _test_gitlab_main_references] ]) end diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb index ef76c9b8da3..04bec50088d 100644 --- a/spec/lib/gitlab/database/tables_truncate_spec.rb +++ b/spec/lib/gitlab/database/tables_truncate_spec.rb @@ -155,6 +155,7 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba "_test_gitlab_shared_items" => :gitlab_shared, "_test_gitlab_geo_items" => :gitlab_geo, "detached_partitions" => :gitlab_shared, + "postgres_foreign_keys" => :gitlab_shared, "postgres_partitions" => :gitlab_shared } ) diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index d51319d462b..0d8fa4dad6d 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -344,6 +344,33 @@ RSpec.describe Gitlab::Database, feature_category: :database do end end + describe '.db_config_share_with' do + using RSpec::Parameterized::TableSyntax + + where(:db_config_name, :db_config_attributes, :expected_db_config_share_with) do + 'main' | { database_tasks: true } | nil + 'main' | { database_tasks: false } | nil + 'ci' | { database_tasks: true } | nil + 'ci' | { database_tasks: false } | 'main' + 'main_clusterwide' | { database_tasks: true } | nil + 'main_clusterwide' | { database_tasks: false } | 'main' + '_test_unknown' | { database_tasks: true } | nil + '_test_unknown' | { database_tasks: false } | 'main' + end + + with_them do + it 'returns the expected result' do + db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new( + Rails.env, + db_config_name, + db_config_attributes + ) + + expect(described_class.db_config_share_with(db_config)).to eq(expected_db_config_share_with) + end + end + end + describe '.gitlab_schemas_for_connection' do it 'does return a valid schema depending on a base model used', :request_store do expect(described_class.gitlab_schemas_for_connection(Project.connection)).to include(:gitlab_main, :gitlab_shared) diff --git a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb index 8068fa30367..7f6b3b86799 100644 --- a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb @@ -32,6 +32,7 @@ RSpec.describe Gitlab::DependencyLinker::CargoTomlLinker do # Default dependencies format with fixed version and version range chrono = "0.4.7" xml-rs = ">=0.8.0" + indicatif = { version = "0.17.5", features = ["rayon"] } [dependencies.memchr] # Specific dependency with optional info @@ -45,6 +46,24 @@ RSpec.describe Gitlab::DependencyLinker::CargoTomlLinker do [build-dependencies] # Build dependency with version wildcard thread_local = "0.3.*" + + # Dependencies with a custom location should be ignored + path-ignored = { path = "local" } + git-ignored = { git = "https://example.com/.git" } + registry-ignored = { registry = "custom-registry" } + + [build-dependencies.bracked-ignored] + path = "local" + + # Unless they specify a version and no registry + [build-dependencies.rand] + version = "0.8.5" + path = "../rand" + + [build-dependencies.custom-rand] + version = "0.8.5" + path = "../custom-rand" + registry = "custom-registry" CONTENT end @@ -62,8 +81,27 @@ RSpec.describe Gitlab::DependencyLinker::CargoTomlLinker do expect(subject).to include(link('thread_local', 'https://crates.io/crates/thread_local')) end + it 'links dependencies that use an inline table' do + expect(subject).to include(link('indicatif', 'https://crates.io/crates/indicatif')) + end + + it 'links dependencies that include a version but no registry' do + expect(subject).to include(link('rand', 'https://crates.io/crates/rand')) + end + it 'does not contain metadata identified as package' do expect(subject).not_to include(link('version', 'https://crates.io/crates/version')) end + + it 'does not link dependencies without a version' do + expect(subject).not_to include(link('path-ignored', 'https://crates.io/crates/path-ignored')) + expect(subject).not_to include(link('git-ignored', 'https://crates.io/crates/git-ignored')) + expect(subject).not_to include(link('bracked-ignored', 'https://crates.io/crates/bracked-ignored')) + end + + it 'does not link dependencies with a custom registry' do + expect(subject).not_to include(link('registry-ignored', 'https://crates.io/crates/registry-ignored')) + expect(subject).not_to include(link('custom-rand', 'https://crates.io/crates/custom-rand')) + end end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index 968d26e1c38..c8325c5b359 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do +RSpec.describe Gitlab::ExclusiveLease, :request_store, :clean_gitlab_redis_shared_state, + :clean_gitlab_redis_cluster_shared_state, feature_category: :shared do let(:unique_key) { SecureRandom.hex(10) } describe '#try_obtain' do @@ -19,6 +20,67 @@ RSpec.describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do sleep(2 * timeout) # lease should have expired now expect(lease.try_obtain).to be_present end + + context 'when migrating across stores' do + let(:lease) { described_class.new(unique_key, timeout: 3600) } + + before do + stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) + allow(lease).to receive(:same_store).and_return(false) + end + + it 'acquires 2 locks' do + # stub first SETNX + Gitlab::Redis::SharedState.with { |r| expect(r).to receive(:set).and_return(true) } + Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:set).and_call_original } + + expect(lease.try_obtain).to be_truthy + end + + it 'rollback first lock if second lock is not acquired' do + Gitlab::Redis::ClusterSharedState.with do |r| + expect(r).to receive(:set).and_return(false) + expect(r).to receive(:eval).and_call_original + end + + Gitlab::Redis::SharedState.with do |r| + expect(r).to receive(:set).and_call_original + expect(r).to receive(:eval).and_call_original + end + + expect(lease.try_obtain).to be_falsey + end + end + + context 'when cutting over to ClusterSharedState' do + context 'when lock is not acquired' do + it 'waits for existing holder to yield the lock' do + Gitlab::Redis::ClusterSharedState.with { |r| expect(r).to receive(:set).and_call_original } + Gitlab::Redis::SharedState.with { |r| expect(r).not_to receive(:set) } + + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to be_truthy + end + end + + context 'when lock is still acquired' do + let(:lease) { described_class.new(unique_key, timeout: 3600) } + + before do + # simulates cutover where some application's feature-flag has not updated + stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) + lease.try_obtain + stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: true) + end + + it 'waits for existing holder to yield the lock' do + Gitlab::Redis::ClusterSharedState.with { |r| expect(r).not_to receive(:set) } + Gitlab::Redis::SharedState.with { |r| expect(r).not_to receive(:set) } + + expect(lease.try_obtain).to be_falsey + end + end + end end describe '.redis_shared_state_key' do @@ -42,131 +104,159 @@ RSpec.describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do end end - describe '#renew' do - it 'returns true when we have the existing lease' do - lease = described_class.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to be_present - expect(lease.renew).to be_truthy - end + shared_examples 'write operations' do + describe '#renew' do + it 'returns true when we have the existing lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to be_present + expect(lease.renew).to be_truthy + end - it 'returns false when we dont have a lease' do - lease = described_class.new(unique_key, timeout: 3600) - expect(lease.renew).to be_falsey + it 'returns false when we dont have a lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.renew).to be_falsey + end end - end - describe '#exists?' do - it 'returns true for an existing lease' do - lease = described_class.new(unique_key, timeout: 3600) - lease.try_obtain + describe 'cancellation' do + def new_lease(key) + described_class.new(key, timeout: 3600) + end - expect(lease.exists?).to eq(true) - end + shared_examples 'cancelling a lease' do + let(:lease) { new_lease(unique_key) } - it 'returns false for a lease that does not exist' do - lease = described_class.new(unique_key, timeout: 3600) + it 'releases the held lease' do + uuid = lease.try_obtain + expect(uuid).to be_present + expect(new_lease(unique_key).try_obtain).to eq(false) - expect(lease.exists?).to eq(false) - end - end + cancel_lease(uuid) - describe '.get_uuid' do - it 'gets the uuid if lease with the key associated exists' do - uuid = described_class.new(unique_key, timeout: 3600).try_obtain + expect(new_lease(unique_key).try_obtain).to be_present + end + end - expect(described_class.get_uuid(unique_key)).to eq(uuid) - end + describe '.cancel' do + def cancel_lease(uuid) + described_class.cancel(release_key, uuid) + end - it 'returns false if the lease does not exist' do - expect(described_class.get_uuid(unique_key)).to be false - end - end + context 'when called with the unprefixed key' do + it_behaves_like 'cancelling a lease' do + let(:release_key) { unique_key } + end + end - describe 'cancellation' do - def new_lease(key) - described_class.new(key, timeout: 3600) - end + context 'when called with the prefixed key' do + it_behaves_like 'cancelling a lease' do + let(:release_key) { described_class.redis_shared_state_key(unique_key) } + end + end - shared_examples 'cancelling a lease' do - let(:lease) { new_lease(unique_key) } + it 'does not raise errors when given a nil key' do + expect { described_class.cancel(nil, nil) }.not_to raise_error + end + end - it 'releases the held lease' do - uuid = lease.try_obtain - expect(uuid).to be_present - expect(new_lease(unique_key).try_obtain).to eq(false) + describe '#cancel' do + def cancel_lease(_uuid) + lease.cancel + end - cancel_lease(uuid) + it_behaves_like 'cancelling a lease' - expect(new_lease(unique_key).try_obtain).to be_present - end - end + it 'is safe to call even if the lease was never obtained' do + lease = new_lease(unique_key) - describe '.cancel' do - def cancel_lease(uuid) - described_class.cancel(release_key, uuid) - end + lease.cancel - context 'when called with the unprefixed key' do - it_behaves_like 'cancelling a lease' do - let(:release_key) { unique_key } + expect(new_lease(unique_key).try_obtain).to be_present end end + end - context 'when called with the prefixed key' do - it_behaves_like 'cancelling a lease' do - let(:release_key) { described_class.redis_shared_state_key(unique_key) } - end - end + describe '.reset_all!' do + it 'removes all existing lease keys from redis' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain - it 'does not raise errors when given a nil key' do - expect { described_class.cancel(nil, nil) }.not_to raise_error + expect(described_class.get_uuid(unique_key)).to eq(uuid) + + described_class.reset_all! + + expect(described_class.get_uuid(unique_key)).to be_falsey end end + end - describe '#cancel' do - def cancel_lease(_uuid) - lease.cancel + shared_examples 'read operations' do + describe '#exists?' do + it 'returns true for an existing lease' do + lease = described_class.new(unique_key, timeout: 3600) + lease.try_obtain + + expect(lease.exists?).to eq(true) end - it_behaves_like 'cancelling a lease' + it 'returns false for a lease that does not exist' do + lease = described_class.new(unique_key, timeout: 3600) + + expect(lease.exists?).to eq(false) + end + end - it 'is safe to call even if the lease was never obtained' do - lease = new_lease(unique_key) + describe '.get_uuid' do + it 'gets the uuid if lease with the key associated exists' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain - lease.cancel + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end - expect(new_lease(unique_key).try_obtain).to be_present + it 'returns false if the lease does not exist' do + expect(described_class.get_uuid(unique_key)).to be false end end - end - describe '#ttl' do - it 'returns the TTL of the Redis key' do - lease = described_class.new('kittens', timeout: 100) - lease.try_obtain + describe '#ttl' do + it 'returns the TTL of the Redis key' do + lease = described_class.new('kittens', timeout: 100) + lease.try_obtain - expect(lease.ttl <= 100).to eq(true) - end + expect(lease.ttl <= 100).to eq(true) + end - it 'returns nil when the lease does not exist' do - lease = described_class.new('kittens', timeout: 10) + it 'returns nil when the lease does not exist' do + lease = described_class.new('kittens', timeout: 10) - expect(lease.ttl).to be_nil + expect(lease.ttl).to be_nil + end end end - describe '.reset_all!' do - it 'removes all existing lease keys from redis' do - uuid = described_class.new(unique_key, timeout: 3600).try_obtain - - expect(described_class.get_uuid(unique_key)).to eq(uuid) + context 'when migrating across stores' do + before do + stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) + end - described_class.reset_all! + it_behaves_like 'read operations' + it_behaves_like 'write operations' + end - expect(described_class.get_uuid(unique_key)).to be_falsey + context 'when feature flags are all disabled' do + before do + stub_feature_flags( + use_cluster_shared_state_for_exclusive_lease: false, + enable_exclusive_lease_double_lock_rw: false + ) end + + it_behaves_like 'read operations' + it_behaves_like 'write operations' end + it_behaves_like 'read operations' + it_behaves_like 'write operations' + describe '.throttle' do it 'prevents repeated execution of the block' do number = 0 @@ -244,4 +334,74 @@ RSpec.describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do described_class.throttle(1, count: 48, period: 1.day) {} end end + + describe 'transitions between feature-flag toggles' do + shared_examples 'retains behaviours across transitions' do |flag| + it 'retains read behaviour' do + lease = described_class.new(unique_key, timeout: 3600) + uuid = lease.try_obtain + + expect(lease.ttl).not_to eq(nil) + expect(lease.exists?).to be_truthy + expect(described_class.get_uuid(unique_key)).to eq(uuid) + + # simulates transition + stub_feature_flags({ flag => true }) + Gitlab::SafeRequestStore.clear! + + expect(lease.ttl).not_to eq(nil) + expect(lease.exists?).to be_truthy + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end + + it 'retains renew behaviour' do + lease = described_class.new(unique_key, timeout: 3600) + lease.try_obtain + + expect(lease.renew).to be_truthy + + # simulates transition + stub_feature_flags({ flag => true }) + Gitlab::SafeRequestStore.clear! + + expect(lease.renew).to be_truthy + end + + it 'retains renew behaviour' do + lease = described_class.new(unique_key, timeout: 3600) + uuid = lease.try_obtain + lease.cancel + + # proves successful cancellation + expect(lease.try_obtain).to eq(uuid) + + # simulates transition + stub_feature_flags({ flag => true }) + Gitlab::SafeRequestStore.clear! + + expect(lease.try_obtain).to be_falsey + lease.cancel + expect(lease.try_obtain).to eq(uuid) + end + end + + context 'when enabling enable_exclusive_lease_double_lock_rw' do + before do + stub_feature_flags( + enable_exclusive_lease_double_lock_rw: false, + use_cluster_shared_state_for_exclusive_lease: false + ) + end + + it_behaves_like 'retains behaviours across transitions', :enable_exclusive_lease_double_lock_rw + end + + context 'when enabling use_cluster_shared_state_for_exclusive_lease' do + before do + stub_feature_flags(use_cluster_shared_state_for_exclusive_lease: false) + end + + it_behaves_like 'retains behaviours across transitions', :use_cluster_shared_state_for_exclusive_lease + end + end end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 676ea2663d2..d21ac36bf34 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -13,13 +13,17 @@ RSpec.describe Gitlab::Git::Blame do let(:result) do [].tap do |data| - blame.each do |commit, line, previous_path| - data << { commit: commit, line: line, previous_path: previous_path } + blame.each do |commit, line, previous_path, span| + data << { commit: commit, line: line, previous_path: previous_path, span: span } end end end describe 'blaming a file' do + it 'has the right commit span' do + expect(result.first[:span]).to eq(95) + end + it 'has the right number of lines' do expect(result.size).to eq(95) expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit) diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index dd9f77f0211..5c4be1003c3 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -3,15 +3,16 @@ require "spec_helper" RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do - let(:repository) { create(:project, :repository).repository.raw } + let_it_be(:repository) { create(:project, :repository).repository.raw } let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } describe "Commit info from gitaly commit" do let(:subject) { (+"My commit").force_encoding('ASCII-8BIT') } let(:body) { subject + (+"My body").force_encoding('ASCII-8BIT') } let(:body_size) { body.length } - let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size) } + let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size, tree_id: tree_id) } let(:id) { gitaly_commit.id } + let(:tree_id) { 'd7f32d821c9cc7b1a9166ca7c4ba95b5c2d0d000' } let(:committer) { gitaly_commit.committer } let(:author) { gitaly_commit.author } let(:commit) { described_class.new(repository, gitaly_commit) } @@ -26,6 +27,7 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do it { expect(commit.committer_name).to eq(committer.name) } it { expect(commit.committer_email).to eq(committer.email) } it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } + it { expect(commit.tree_id).to eq(tree_id) } context 'non-UTC dates' do let(:seconds) { Time.now.to_i } @@ -577,6 +579,14 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do it { is_expected.to eq(sample_commit_hash[:message]) } end + + describe '#tree_id' do + subject { super().tree_id } + + it "doesn't return tree id for non-Gitaly commits" do + is_expected.to be_nil + end + end end describe '#stats' do @@ -681,6 +691,100 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do end end + describe 'SHA patterns' do + shared_examples 'a SHA-matching pattern' do + let(:expected_match) { sha } + + shared_examples 'a match' do + it 'matches the pattern' do + expect(value).to match(pattern) + expect(pattern.match(value).to_a).to eq([expected_match]) + end + end + + shared_examples 'no match' do + it 'does not match the pattern' do + expect(value).not_to match(pattern) + end + end + + shared_examples 'a SHA pattern' do + context "with too short value" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH - 1] } + + it_behaves_like 'no match' + end + + context "with full length" do + let(:value) { sha } + + it_behaves_like 'a match' + end + + context "with exceeeding length" do + let(:value) { sha + sha } + + # This case is not exactly pretty for SHA1 as we would still match the full SHA256 length. It's arguable what + # the correct behaviour would be, but without starting to distinguish SHA1 and SHA256 hashes this is the best + # we can do. + let(:expected_match) { (sha + sha)[0, described_class::MAX_SHA_LENGTH] } + + it_behaves_like 'a match' + end + + context "with embedded SHA" do + let(:value) { "xxx#{sha}xxx" } + + it_behaves_like 'a match' + end + end + + context 'abbreviated SHA pattern' do + let(:pattern) { described_class::SHA_PATTERN } + + context "with minimum length" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH] } + let(:expected_match) { value } + + it_behaves_like 'a match' + end + + context "with medium length" do + let(:value) { sha[0, described_class::MIN_SHA_LENGTH + 20] } + let(:expected_match) { value } + + it_behaves_like 'a match' + end + + it_behaves_like 'a SHA pattern' + end + + context 'full SHA pattern' do + let(:pattern) { described_class::FULL_SHA_PATTERN } + + context 'with abbreviated length' do + let(:value) { sha[0, described_class::SHA1_LENGTH - 1] } + + it_behaves_like 'no match' + end + + it_behaves_like 'a SHA pattern' + end + end + + context 'SHA1' do + let(:sha) { "5716ca5987cbf97d6bb54920bea6adde242d87e6" } + + it_behaves_like 'a SHA-matching pattern' + end + + context 'SHA256' do + let(:sha) { "a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92" } + + it_behaves_like 'a SHA-matching pattern' + end + end + def sample_commit_hash { author_email: "dmitriy.zaporozhets@gmail.com", diff --git a/spec/lib/gitlab/git/diff_tree_spec.rb b/spec/lib/gitlab/git/diff_tree_spec.rb new file mode 100644 index 00000000000..614a8f03dd8 --- /dev/null +++ b/spec/lib/gitlab/git/diff_tree_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::Git::DiffTree, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository } + + describe '.from_commit' do + subject(:diff_tree) { described_class.from_commit(commit) } + + context 'when commit is an initial commit' do + let(:commit) { repository.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + + it 'returns the expected diff tree object' do + expect(diff_tree.left_tree_id).to eq(Gitlab::Git::EMPTY_TREE_ID) + expect(diff_tree.right_tree_id).to eq(commit.tree_id) + end + end + + context 'when commit is a regular commit' do + let(:commit) { repository.commit('60ecb67744cb56576c30214ff52294f8ce2def98') } + + it 'returns the expected diff tree object' do + expect(diff_tree.left_tree_id).to eq(commit.parent.tree_id) + expect(diff_tree.right_tree_id).to eq(commit.tree_id) + end + end + end +end diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index b158c7227d4..f65ed319462 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Git::ObjectPool do +RSpec.describe Gitlab::Git::ObjectPool, feature_category: :source_code_management do let(:pool_repository) { create(:pool_repository) } let(:source_repository) { pool_repository.source_project.repository } @@ -15,6 +15,29 @@ RSpec.describe Gitlab::Git::ObjectPool do end end + describe '.init_from_gitaly' do + let(:gitaly_object_pool) { Gitaly::ObjectPool.new(repository: repository) } + let(:repository) do + Gitaly::Repository.new( + storage_name: 'default', + relative_path: '@pools/ef/2d/ef2d127d', + gl_project_path: '' + ) + end + + it 'returns an object pool object' do + object_pool = described_class.init_from_gitaly(gitaly_object_pool, source_repository) + + expect(object_pool).to be_kind_of(described_class) + expect(object_pool).to have_attributes( + storage: repository.storage_name, + relative_path: repository.relative_path, + source_repository: source_repository, + gl_project_path: repository.gl_project_path + ) + end + end + describe '#create' do before do subject.create # rubocop:disable Rails/SaveBang diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9ce8a674146..e27b97ea0e6 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -309,6 +309,32 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end end + describe '#recent_objects_size' do + subject(:recent_objects_size) { repository.recent_objects_size } + + it { is_expected.to be_a(Float) } + + it 'uses repository_info for size' do + expect(repository.gitaly_repository_client).to receive(:repository_info).and_call_original + + recent_objects_size + end + + it 'returns the recent objects size' do + objects_response = Gitaly::RepositoryInfoResponse::ObjectsInfo.new(recent_size: 5.megabytes) + + allow(repository.gitaly_repository_client).to receive(:repository_info).and_return( + Gitaly::RepositoryInfoResponse.new(objects: objects_response) + ) + + expect(recent_objects_size).to eq 5.0 + end + + it_behaves_like 'wrapping gRPC errors', Gitaly::RepositoryInfoResponse::ObjectsInfo, :recent_size do + subject { recent_objects_size } + end + end + describe '#to_s' do subject { repository.to_s } @@ -1675,9 +1701,13 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end describe '#find_changed_paths' do - let(:commit_1) { TestEnv::BRANCH_SHA['with-executables'] } - let(:commit_2) { TestEnv::BRANCH_SHA['master'] } - let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } + let_it_be(:commit_1) { repository.commit(TestEnv::BRANCH_SHA['with-executables']) } + let_it_be(:commit_2) { repository.commit(TestEnv::BRANCH_SHA['master']) } + let_it_be(:commit_3) { repository.commit('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + + let_it_be(:initial_commit) { repository.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + let_it_be(:diff_tree) { Gitlab::Git::DiffTree.from_commit(initial_commit) } + let(:commit_1_files) do [Gitlab::Git::ChangedPath.new(status: :ADDED, path: "files/executables/ls")] end @@ -1693,18 +1723,26 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen ] end + let(:diff_tree_files) do + [ + Gitlab::Git::ChangedPath.new(status: :ADDED, path: ".gitignore"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "LICENSE"), + Gitlab::Git::ChangedPath.new(status: :ADDED, path: "README.md") + ] + end + it 'returns a list of paths' do - collection = repository.find_changed_paths([commit_1, commit_2, commit_3]) + collection = repository.find_changed_paths([commit_1, commit_2, commit_3, diff_tree]) expect(collection).to be_a(Enumerable) - expect(collection.as_json).to eq((commit_1_files + commit_2_files + commit_3_files).as_json) + expect(collection.as_json).to eq((commit_1_files + commit_2_files + commit_3_files + diff_tree_files).as_json) end - it 'returns no paths when SHAs are invalid' do + it 'returns only paths with valid SHAs' do collection = repository.find_changed_paths(['invalid', commit_1]) expect(collection).to be_a(Enumerable) - expect(collection.to_a).to be_empty + expect(collection.as_json).to eq(commit_1_files.as_json) end it 'returns a list of paths even when containing a blank ref' do @@ -2535,6 +2573,12 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end end + describe '#get_patch_id' do + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :get_patch_id do + subject { repository.get_patch_id('HEAD~', 'HEAD') } + end + end + def create_remote_branch(remote_name, branch_name, source_branch_name) source_branch = repository.find_branch(source_branch_name) repository.write_ref("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) @@ -2723,4 +2767,39 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen expect(repository.check_objects_exist(single_sha)).to eq({ single_sha => true }) end end + + describe '#list_all_blobs' do + subject { repository.list_all_blobs(expected_params) } + + let(:expected_params) { { bytes_limit: 0, dynamic_timeout: nil, ignore_alternate_object_directories: true } } + + it 'calls delegates to BlobService' do + expect(repository.gitaly_blob_client).to receive(:list_all_blobs).with(expected_params) + subject + end + end + + describe '#object_pool' do + subject { repository.object_pool } + + context 'without object pool' do + it { is_expected.to be_nil } + end + + context 'when pool repository exists' do + let!(:pool) { create(:pool_repository, :ready, source_project: project) } + + it { is_expected.to be_nil } + + context 'when repository is linked to the pool repository' do + before do + pool.link_repository(pool.source_project.repository) + end + + it 'returns a object pool for the repository' do + is_expected.to be_kind_of(Gitaly::ObjectPool) + end + end + end + end end diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb index c5b44b260c6..d320b9c4091 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do let(:feature_flag_name) { wrapper.rugged_feature_keys.first } let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file } - before(:all) do + before_all do create_gitaly_metadata_file end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 4a20e0b1156..84ab8376fe1 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -9,13 +9,14 @@ RSpec.describe Gitlab::Git::Tree do let(:repository) { project.repository.raw } shared_examples 'repo' do - subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, pagination_params) } + subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, pagination_params) } let(:sha) { SeedRepo::Commit::ID } let(:path) { nil } let(:recursive) { false } let(:pagination_params) { nil } let(:skip_flat_paths) { false } + let(:rescue_not_found) { true } let(:entries) { tree.first } let(:cursor) { tree.second } @@ -30,8 +31,14 @@ RSpec.describe Gitlab::Git::Tree do context 'with an invalid ref' do let(:sha) { 'foobar-does-not-exist' } - it { expect(entries).to eq([]) } - it { expect(cursor).to be_nil } + context 'when handle_structured_gitaly_errors feature is disabled' do + before do + stub_feature_flags(handle_structured_gitaly_errors: false) + end + + it { expect(entries).to eq([]) } + it { expect(cursor).to be_nil } + end end context 'when path is provided' do @@ -162,11 +169,23 @@ RSpec.describe Gitlab::Git::Tree do end context 'and invalid reference is used' do - it 'returns no entries and nil cursor' do + before do allow(repository.gitaly_commit_client).to receive(:tree_entries).and_raise(Gitlab::Git::Index::IndexError) + end + + context 'when rescue_not_found is set to false' do + let(:rescue_not_found) { false } - expect(entries.count).to eq(0) - expect(cursor).to be_nil + it 'raises an IndexError error' do + expect { entries }.to raise_error(Gitlab::Git::Index::IndexError) + end + end + + context 'when rescue_not_found is set to true' do + it 'returns no entries and nil cursor' do + expect(entries.count).to eq(0) + expect(cursor).to be_nil + end end end end @@ -196,7 +215,7 @@ RSpec.describe Gitlab::Git::Tree do let(:entries_count) { entries.count } it 'returns all entries without a cursor' do - result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: entries_count, page_token: nil }) + result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: entries_count, page_token: nil }) expect(cursor).to be_nil expect(result.entries.count).to eq(entries_count) @@ -225,7 +244,7 @@ RSpec.describe Gitlab::Git::Tree do let(:entries_count) { entries.count } it 'returns all entries' do - result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: nil }) + result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: nil }) expect(result.count).to eq(entries_count) expect(cursor).to be_nil @@ -236,7 +255,7 @@ RSpec.describe Gitlab::Git::Tree do let(:token) { entries.second.id } it 'returns all entries after token' do - result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: token }) + result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: token }) expect(result.count).to eq(entries.count - 2) expect(cursor).to be_nil @@ -268,7 +287,7 @@ RSpec.describe Gitlab::Git::Tree do expected_entries = entries loop do - result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: 5, page_token: token }) + result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: 5, page_token: token }) collected_entries += result.entries token = cursor&.next_cursor diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb index becf97bb24e..9ba021e838e 100644 --- a/spec/lib/gitlab/git_access_snippet_spec.rb +++ b/spec/lib/gitlab/git_access_snippet_spec.rb @@ -346,7 +346,7 @@ RSpec.describe Gitlab::GitAccessSnippet do expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(false) expect(snippet.repository_size_checker) .to receive(:changes_will_exceed_size_limit?) - .with(change_size) + .with(change_size, nil) .and_return(false) expect { push_access_check }.not_to raise_error @@ -360,7 +360,7 @@ RSpec.describe Gitlab::GitAccessSnippet do expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(false) expect(snippet.repository_size_checker) .to receive(:changes_will_exceed_size_limit?) - .with(change_size) + .with(change_size, nil) .and_return(true) expect do diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb index d02b4492216..ee76811fea5 100644 --- a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb @@ -209,4 +209,23 @@ RSpec.describe Gitlab::GitalyClient::BlobService do end end end + + describe '#list_all_blobs' do + subject { client.list_all_blobs(**expected_params) } + + let(:expected_params) { { limit: 0, bytes_limit: 0 } } + + before do + ::Gitlab::GitalyClient.clear_stubs! + end + + it 'sends a list all blobs message' do + expect_next_instance_of(Gitaly::BlobService::Stub) do |service| + expect(service).to receive(:list_all_blobs) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + end + + subject + end + end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index fd66efe12c8..2ee9d85c723 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -192,7 +192,9 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do Gitaly::FindChangedPathsRequest.new(repository: repository_message, requests: requests, merge_commit_diff_mode: merge_commit_diff_mode) end - subject { described_class.new(repository).find_changed_paths(commits, merge_commit_diff_mode: merge_commit_diff_mode).as_json } + let(:treeish_objects) { repository.commits_by(oids: commits) } + + subject { described_class.new(repository).find_changed_paths(treeish_objects, merge_commit_diff_mode: merge_commit_diff_mode).as_json } before do allow(Gitaly::FindChangedPathsRequest).to receive(:new).and_call_original @@ -334,6 +336,40 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do include_examples 'uses requests format' end end + + context 'when all requested objects are invalid' do + it 'does not send RPC request' do + expect_any_instance_of(Gitaly::DiffService::Stub).not_to receive(:find_changed_paths) + + returned_value = described_class.new(repository).find_changed_paths(%w[wrong values]) + + expect(returned_value).to eq([]) + end + end + + context 'when commit has an empty SHA' do + let(:empty_commit) { build(:commit, project: project, sha: '0000000000000000000000000000000000000000') } + + it 'does not send RPC request' do + expect_any_instance_of(Gitaly::DiffService::Stub).not_to receive(:find_changed_paths) + + returned_value = described_class.new(repository).find_changed_paths([empty_commit]) + + expect(returned_value).to eq([]) + end + end + + context 'when commit sha is not set' do + let(:empty_commit) { build(:commit, project: project, sha: nil) } + + it 'does not send RPC request' do + expect_any_instance_of(Gitaly::DiffService::Stub).not_to receive(:find_changed_paths) + + returned_value = described_class.new(repository).find_changed_paths([empty_commit]) + + expect(returned_value).to eq([]) + end + end end describe '#tree_entries' do @@ -1072,4 +1108,22 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do expect(signatures[large_signed_text][:signed_text].size).to eq(4971878) end end + + describe '#get_patch_id' do + it 'returns patch_id of given revisions' do + expect(client.get_patch_id('HEAD~', 'HEAD')).to eq('45435e5d7b339dd76d939508c7687701d0c17fff') + end + + context 'when one of the param is invalid' do + it 'raises an GRPC::InvalidArgument error' do + expect { client.get_patch_id('HEAD', nil) }.to raise_error(GRPC::InvalidArgument) + end + end + + context 'when two revisions are the same' do + it 'raises an GRPC::FailedPrecondition error' do + expect { client.get_patch_id('HEAD', 'HEAD') }.to raise_error(GRPC::FailedPrecondition) + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb index 89a41ae71f3..bdc16f16e66 100644 --- a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb @@ -14,6 +14,32 @@ RSpec.describe Gitlab::GitalyClient::ConflictsService do described_class.new(target_repository, our_commit_oid, their_commit_oid) end + describe '#conflicts' do + subject(:conflicts) { client.conflicts? } + + context "with the `skip_conflict_files_in_gitaly` feature flag on" do + it 'calls list_conflict_files with `skip_content: true`' do + expect_any_instance_of(described_class).to receive(:list_conflict_files) + .with(skip_content: true).and_return(["let's pretend i'm a conflicted file"]) + + conflicts + end + end + + context "with the `skip_conflict_files_in_gitaly` feature flag off" do + before do + stub_feature_flags(skip_conflict_files_in_gitaly: false) + end + + it 'calls list_conflict_files with no parameters' do + expect_any_instance_of(described_class).to receive(:list_conflict_files) + .with(skip_content: false).and_return(["let's pretend i'm a conflicted file"]) + + conflicts + end + end + end + describe '#list_conflict_files' do let(:allow_tree_conflicts) { false } let(:request) do diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index baf7076c718..ae2bb5af2b1 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GitalyClient::ObjectPoolService do +RSpec.describe Gitlab::GitalyClient::ObjectPoolService, feature_category: :source_code_management do let(:pool_repository) { create(:pool_repository) } let(:project) { pool_repository.source_project } let(:raw_repository) { project.repository.raw } diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 4a3607ed6db..9055b284119 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -183,6 +183,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source expect(request.to_h).to eq( payload.merge({ allow_conflicts: false, + expected_old_oid: "", repository: repository.gitaly_repository.to_h, message: message.dup.force_encoding(Encoding::ASCII_8BIT), user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, @@ -730,6 +731,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source end end + describe '#user_rebase_to_ref' do + let(:first_parent_ref) { 'refs/heads/my-branch' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:target_ref) { 'refs/merge-requests/x/merge' } + let(:response) { Gitaly::UserRebaseToRefResponse.new(commit_id: 'new-commit-id') } + + let(:payload) do + { source_sha: source_sha, target_ref: target_ref, first_parent_ref: first_parent_ref } + end + + it 'sends a user_rebase_to_ref message' do + freeze_time do + expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_rebase_to_ref) do |_, request, options| + expect(options).to be_kind_of(Hash) + expect(request.to_h).to( + eq( + payload.merge( + { + expected_old_oid: "", + repository: repository.gitaly_repository.to_h, + user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, + timestamp: { nanos: 0, seconds: Time.current.to_i } + } + ) + ) + ) + end.and_return(response) + + client.user_rebase_to_ref(user, **payload) + end + end + end + describe '#user_squash' do let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' } diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 08457e20ec3..d8ae7d70bb2 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -452,4 +452,14 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital client.find_license end end + + describe '#object_pool' do + it 'sends a get_object_pool_request message' do + expect_any_instance_of(Gitaly::ObjectPoolService::Stub) + .to receive(:get_object_pool) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + + client.object_pool + end + end end diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb index c4ed4b09f04..898bc40ec1f 100644 --- a/spec/lib/gitlab/github_import_spec.rb +++ b/spec/lib/gitlab/github_import_spec.rb @@ -61,7 +61,7 @@ RSpec.describe Gitlab::GithubImport, feature_category: :importers do expect(described_class::ClientPool) .to receive(:new) - .with(token_pool: %w[foo bar], host: nil, parallel: true, per_page: 100) + .with(token_pool: %w[foo bar 123], host: nil, parallel: true, per_page: 100) described_class.new_client_for(project) end diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb index 97389b6250e..0c4ca5570f8 100644 --- a/spec/lib/gitlab/graphql/pagination/connections_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb @@ -6,7 +6,7 @@ require 'spec_helper' RSpec.describe ::Gitlab::Graphql::Pagination::Connections do include GraphqlHelpers - before(:all) do + before_all do ActiveRecord::Schema.define do create_table :_test_testing_pagination_nodes, force: true do |t| t.integer :value, null: false diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb index 1206a1c9131..071b303d777 100644 --- a/spec/lib/gitlab/group_search_results_spec.rb +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -32,8 +32,12 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do end describe 'merge_requests search' do + let_it_be(:unarchived_project) { create(:project, :public, group: group) } + let_it_be(:archived_project) { create(:project, :public, :archived, group: group) } let(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') } let(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') } + let_it_be(:unarchived_result) { create(:merge_request, source_project: unarchived_project, title: 'foo') } + let_it_be(:archived_result) { create(:merge_request, source_project: archived_project, title: 'foo') } let(:query) { 'foo' } let(:scope) { 'merge_requests' } @@ -44,6 +48,7 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do end include_examples 'search results filtered by state' + include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects' end describe '#projects' do @@ -52,10 +57,10 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do describe 'filtering' do let_it_be(:group) { create(:group) } - let_it_be(:unarchived_project) { create(:project, :public, group: group, name: 'Test1') } - let_it_be(:archived_project) { create(:project, :archived, :public, group: group, name: 'Test2') } + let_it_be(:unarchived_result) { create(:project, :public, group: group, name: 'Test1') } + let_it_be(:archived_result) { create(:project, :archived, :public, group: group, name: 'Test2') } - it_behaves_like 'search results filtered by archived' + it_behaves_like 'search results filtered by archived', 'search_projects_hide_archived' end end diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index b9490306410..9f7aaa21f5b 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -2,9 +2,13 @@ require 'spec_helper' -RSpec.describe Gitlab::HookData::IssueBuilder do - let_it_be(:label) { create(:label) } - let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) } +RSpec.describe Gitlab::HookData::IssueBuilder, feature_category: :webhooks do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:label) { create(:label, project: project) } + let_it_be(:issue) { create(:labeled_issue, labels: [label], project: project) } + let_it_be(:contact) { create(:contact, group: project.group) } + let_it_be(:issue_contact) { create(:issue_customer_relations_contact, issue: issue, contact: contact) } let(:builder) { described_class.new(issue) } @@ -50,6 +54,7 @@ RSpec.describe Gitlab::HookData::IssueBuilder do expect(data).to include(:state) expect(data).to include(:severity) expect(data).to include('labels' => [label.hook_attrs]) + expect(data).to include('customer_relations_contacts' => [contact.reload.hook_attrs]) end context 'when the issue has an image in the description' do diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index f9a6c25b786..1818693974e 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -39,6 +39,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do title updated_at updated_by_id + draft ].freeze expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys) @@ -66,6 +67,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do url last_commit work_in_progress + draft total_time_spent time_change human_total_time_spent diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index 133cd3b2f49..93d48379414 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Gitlab::HTTP do end context 'when reading the response is too slow' do - before(:all) do + before_all do # Override Net::HTTP to add a delay between sending each response chunk mocked_http = Class.new(Net::HTTP) do def request(*) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 981802ad09d..5bbb95b3ea5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -162,6 +162,7 @@ releases: - milestone_releases - milestones - evidences +- catalog_resource_version links: - release project_members: @@ -325,7 +326,7 @@ ci_pipelines: - security_findings - daily_build_group_report_results - latest_builds -- latest_successful_builds +- latest_successful_jobs - daily_report_results - latest_builds_report_results - messages @@ -432,6 +433,7 @@ builds: - dast_scanner_profiles_build - dast_scanner_profile - job_annotations +- job_artifacts_annotations bridges: - user - pipeline @@ -440,6 +442,7 @@ bridges: - needs - resource - sourced_pipeline +- deployment - resource_group - metadata - trigger_request @@ -516,6 +519,8 @@ container_repositories: - name project: - catalog_resource +- catalog_resource_versions +- ci_components - external_status_checks - base_tags - project_topics @@ -817,6 +822,7 @@ project: - scan_result_policy_reads - project_state - security_policy_bots +- target_branch_rules award_emoji: - awardable - user @@ -1039,6 +1045,13 @@ iterations_cadence: - iterations catalog_resource: - project + - catalog_resource_components + - catalog_resource_versions +catalog_resource_versions: + - project + - release + - catalog_resource + - catalog_resource_components approval_rules: - users - groups diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb index b2e047f5621..8ed3a60d7fc 100644 --- a/spec/lib/gitlab/import_export/command_line_util_spec.rb +++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb @@ -103,7 +103,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe let(:local) { false } it 'downloads the file' do - expect(subject).to receive(:download).with(:url, upload_path, size_limit: nil) + expect(subject).to receive(:download).with(:url, upload_path, size_limit: 0) subject.download_or_copy_upload(uploader, upload_path) end diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index 8c5823edc51..aceea70be92 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator, feature_category: :importers do let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_archive_size_validator_spec.gz') } - before(:all) do + before_all do create_compressed_file end @@ -47,6 +47,25 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator, feature_c end end + context 'when max_decompressed_archive_size is set to 0' do + subject { described_class.new(archive_path: filepath) } + + before do + stub_application_setting(max_decompressed_archive_size: 0) + end + + it 'is valid and does not log error message' do + expect(Gitlab::Import::Logger) + .not_to receive(:info) + .with( + import_upload_archive_path: filepath, + import_upload_archive_size: File.size(filepath), + message: 'Decompressed archive size limit reached' + ) + expect(subject.valid?).to eq(true) + end + end + context 'when exception occurs during decompression' do shared_examples 'logs raised exception and terminates validator process group' do let(:std) { double(:std, close: nil, value: nil) } diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index aff11f7ac30..d449446d7be 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Gitlab::ImportExport::FileImporter, feature_category: :importers .with( import_export_upload.import_file, kind_of(String), - size_limit: ::Import::GitlabProjects::RemoteFileValidator::FILE_SIZE_LIMIT + size_limit: Gitlab::CurrentSettings.current_application_settings.max_import_remote_file_size.megabytes ) described_class.import(importable: project, archive_file: nil, shared: shared) @@ -104,7 +104,7 @@ RSpec.describe Gitlab::ImportExport::FileImporter, feature_category: :importers .with( file_url, kind_of(String), - size_limit: ::Import::GitlabProjects::RemoteFileValidator::FILE_SIZE_LIMIT + size_limit: Gitlab::CurrentSettings.current_application_settings.max_import_remote_file_size.megabytes ) described_class.import(importable: project, archive_file: nil, shared: shared) diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb index 180a6b6ff0a..0f4f2eb573c 100644 --- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb @@ -60,7 +60,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_cate let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) } let_it_be(:group) do - create(:group, :disabled_and_unoverridable).tap { |g| g.add_maintainer(user) } + create(:group, :shared_runners_disabled_and_unoverridable).tap { |g| g.add_maintainer(user) } end before do diff --git a/spec/lib/gitlab/internal_events/event_definitions_spec.rb b/spec/lib/gitlab/internal_events/event_definitions_spec.rb index f6f79d9d906..924845504ca 100644 --- a/spec/lib/gitlab/internal_events/event_definitions_spec.rb +++ b/spec/lib/gitlab/internal_events/event_definitions_spec.rb @@ -2,9 +2,9 @@ require "spec_helper" -RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :product_analytics do +RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :product_analytics_data_management do after(:all) do - described_class.clear_events + described_class.instance_variable_set(:@events, nil) end context 'when using actual metric definitions' do diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb index 86215434ba4..c2615e0f22c 100644 --- a/spec/lib/gitlab/internal_events_spec.rb +++ b/spec/lib/gitlab/internal_events_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_analytics do +RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_analytics_data_management do include TrackingHelpers include SnowplowHelpers diff --git a/spec/lib/gitlab/jwt_authenticatable_spec.rb b/spec/lib/gitlab/jwt_authenticatable_spec.rb index 98c87ef627a..eea93c4e3fe 100644 --- a/spec/lib/gitlab/jwt_authenticatable_spec.rb +++ b/spec/lib/gitlab/jwt_authenticatable_spec.rb @@ -148,9 +148,9 @@ RSpec.describe Gitlab::JwtAuthenticatable, feature_category: :system_access do it 'returns decoded payload if issuer is correct' do encoded_message = JWT.encode(payload, test_class.secret, 'HS256') - payload = test_class.decode_jwt(encoded_message, issuer: 'test_issuer') + decoded_payload = test_class.decode_jwt(encoded_message, issuer: 'test_issuer') - expect(payload[0]).to match a_hash_including('iss' => 'test_issuer') + expect(decoded_payload[0]).to match a_hash_including('iss' => 'test_issuer') end it 'raises an error when the issuer is incorrect' do @@ -159,6 +159,38 @@ RSpec.describe Gitlab::JwtAuthenticatable, feature_category: :system_access do expect { test_class.decode_jwt(encoded_message, issuer: 'test_issuer') }.to raise_error(JWT::DecodeError) end + + it 'raises an error when the issuer is nil' do + payload['iss'] = nil + encoded_message = JWT.encode(payload, test_class.secret, 'HS256') + + expect { test_class.decode_jwt(encoded_message, issuer: 'test_issuer') }.to raise_error(JWT::DecodeError) + end + end + + context 'audience option' do + let(:payload) { { 'aud' => 'test_audience' } } + + it 'returns decoded payload if audience is correct' do + encoded_message = JWT.encode(payload, test_class.secret, 'HS256') + decoded_payload = test_class.decode_jwt(encoded_message, audience: 'test_audience') + + expect(decoded_payload[0]).to match a_hash_including('aud' => 'test_audience') + end + + it 'raises an error when the audience is incorrect' do + payload['aud'] = 'somebody else' + encoded_message = JWT.encode(payload, test_class.secret, 'HS256') + + expect { test_class.decode_jwt(encoded_message, audience: 'test_audience') }.to raise_error(JWT::DecodeError) + end + + it 'raises an error when the audience is nil' do + payload['aud'] = nil + encoded_message = JWT.encode(payload, test_class.secret, 'HS256') + + expect { test_class.decode_jwt(encoded_message, audience: 'test_audience') }.to raise_error(JWT::DecodeError) + end end context 'iat_after option' do diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb index 34eb48a3221..69d9ca7d4ed 100644 --- a/spec/lib/gitlab/kas_spec.rb +++ b/spec/lib/gitlab/kas_spec.rb @@ -10,20 +10,41 @@ RSpec.describe Gitlab::Kas do end describe '.verify_api_request' do - let(:payload) { { 'iss' => described_class::JWT_ISSUER } } + let(:payload) { { 'iss' => described_class::JWT_ISSUER, 'aud' => described_class::JWT_AUDIENCE } } - it 'returns nil if fails to validate the JWT' do - encoded_token = JWT.encode(payload, 'wrongsecret', 'HS256') - headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + context 'returns nil if fails to validate the JWT' do + it 'when secret is wrong' do + encoded_token = JWT.encode(payload, 'wrong secret', 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to be_nil + end + + it 'when issuer is wrong' do + payload['iss'] = 'wrong issuer' + encoded_token = JWT.encode(payload, described_class.secret, 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to be_nil + end - expect(described_class.verify_api_request(headers)).to be_nil + it 'when audience is wrong' do + payload['aud'] = 'wrong audience' + encoded_token = JWT.encode(payload, described_class.secret, 'HS256') + headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } + + expect(described_class.verify_api_request(headers)).to be_nil + end end it 'returns the decoded JWT' do encoded_token = JWT.encode(payload, described_class.secret, 'HS256') headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token } - expect(described_class.verify_api_request(headers)).to eq([{ "iss" => described_class::JWT_ISSUER }, { "alg" => "HS256" }]) + expect(described_class.verify_api_request(headers)).to eq([ + { 'iss' => described_class::JWT_ISSUER, 'aud' => described_class::JWT_AUDIENCE }, + { 'alg' => 'HS256' } + ]) end end @@ -111,6 +132,52 @@ RSpec.describe Gitlab::Kas do end end + describe '.tunnel_ws_url' do + before do + stub_config(gitlab_kas: { external_url: external_url }) + end + + let(:external_url) { 'xyz' } + + subject { described_class.tunnel_ws_url } + + context 'with a gitlab_kas.external_k8s_proxy_url setting' do + let(:external_k8s_proxy_url) { 'http://abc' } + + before do + stub_config(gitlab_kas: { external_k8s_proxy_url: external_k8s_proxy_url }) + end + + it { is_expected.to eq('ws://abc') } + end + + context 'without a gitlab_kas.external_k8s_proxy_url setting' do + context 'external_url uses wss://' do + let(:external_url) { 'wss://kas.gitlab.example.com' } + + it { is_expected.to eq('wss://kas.gitlab.example.com/k8s-proxy') } + end + + context 'external_url uses ws://' do + let(:external_url) { 'ws://kas.gitlab.example.com' } + + it { is_expected.to eq('ws://kas.gitlab.example.com/k8s-proxy') } + end + + context 'external_url uses grpcs://' do + let(:external_url) { 'grpcs://kas.gitlab.example.com' } + + it { is_expected.to eq('wss://kas.gitlab.example.com/k8s-proxy') } + end + + context 'external_url uses grpc://' do + let(:external_url) { 'grpc://kas.gitlab.example.com' } + + it { is_expected.to eq('ws://kas.gitlab.example.com/k8s-proxy') } + end + end + end + describe '.internal_url' do it 'returns gitlab_kas internal_url config' do expect(described_class.internal_url).to eq(Gitlab.config.gitlab_kas.internal_url) diff --git a/spec/lib/gitlab/merge_requests/message_generator_spec.rb b/spec/lib/gitlab/merge_requests/message_generator_spec.rb index df8804d38d4..b1a8ff26a86 100644 --- a/spec/lib/gitlab/merge_requests/message_generator_spec.rb +++ b/spec/lib/gitlab/merge_requests/message_generator_spec.rb @@ -96,6 +96,26 @@ RSpec.describe Gitlab::MergeRequests::MessageGenerator, feature_category: :code_ end end + context 'when project has commit template with source project id' do + let(:merge_request) do + double( + :merge_request, + title: 'Fixes', + target_project: project, + source_project: project, + to_reference: '!123', + metrics: nil, + merge_user: nil + ) + end + + let(message_template_name) { '%{source_project_id}' } + + it 'evaluates only necessary variables' do + expect(result_message).to eq project.id.to_s + end + end + context 'when project has commit template with closed issues' do let(message_template_name) { <<~MSG.rstrip } Merge branch '%{source_branch}' into '%{target_branch}' diff --git a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb b/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb deleted file mode 100644 index b8556829a59..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/defaults_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Defaults do - it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) } -end diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb deleted file mode 100644 index d3cb9760052..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb +++ /dev/null @@ -1,178 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do - include MetricsDashboardHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user) } - let_it_be(:environment) { create(:environment, project: project) } - - before do - project.add_maintainer(user) - end - - describe '.find' do - let(:dashboard_path) { '.gitlab/dashboards/test.yml' } - let(:service_call) { described_class.find(project, user, environment: environment, dashboard_path: dashboard_path) } - - it_behaves_like 'misconfigured dashboard service response', :not_found - - context 'when the dashboard exists' do - let(:project) { project_with_dashboard(dashboard_path) } - - it_behaves_like 'valid dashboard service response' - end - - context 'when the dashboard is configured incorrectly' do - let(:project) { project_with_dashboard(dashboard_path, {}) } - - it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity - end - - context 'when the dashboard contains a metric without a query' do - let(:dashboard) { { 'panel_groups' => [{ 'panels' => [{ 'metrics' => [{ 'id' => 'mock' }] }] }] } } - let(:project) { project_with_dashboard(dashboard_path, dashboard.to_yaml) } - - it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity - end - - context 'when the system dashboard is specified' do - let(:dashboard_path) { system_dashboard_path } - - it_behaves_like 'valid dashboard service response' - end - - context 'when no dashboard is specified' do - let(:service_call) { described_class.find(project, user, environment: environment) } - - it_behaves_like 'valid dashboard service response' - end - - context 'when the dashboard is expected to be embedded' do - let(:service_call) { described_class.find(project, user, **params) } - let(:params) { { environment: environment, embedded: true } } - - it_behaves_like 'valid embedded dashboard service response' - - context 'when params are incomplete' do - let(:params) { { environment: environment, embedded: true, dashboard_path: system_dashboard_path } } - - it_behaves_like 'valid embedded dashboard service response' - end - - context 'when the panel is specified' do - context 'as a custom metric' do - let(:params) do - { - environment: environment, - embedded: true, - dashboard_path: system_dashboard_path, - group: business_metric_title, - title: 'title', - y_label: 'y_label' - } - end - - it_behaves_like 'misconfigured dashboard service response', :not_found - - context 'when the metric exists' do - before do - create(:prometheus_metric, project: project) - end - - it_behaves_like 'valid embedded dashboard service response' - end - end - - context 'as a project-defined panel' do - let(:dashboard_path) { '.gitlab/dashboard/test.yml' } - let(:params) do - { - environment: environment, - embedded: true, - dashboard_path: dashboard_path, - group: 'Group A', - title: 'Super Chart A1', - y_label: 'y_label' - } - end - - it_behaves_like 'misconfigured dashboard service response', :not_found - - context 'when the metric exists' do - let(:project) { project_with_dashboard(dashboard_path) } - - it_behaves_like 'valid embedded dashboard service response' - end - end - end - end - end - - describe '.find_raw' do - let(:dashboard) { load_dashboard_yaml(File.read(Rails.root.join('config', 'prometheus', 'common_metrics.yml'))) } - let(:params) { {} } - - subject { described_class.find_raw(project, **params) } - - it { is_expected.to eq dashboard } - - context 'when the system dashboard is specified' do - let(:params) { { dashboard_path: system_dashboard_path } } - - it { is_expected.to eq dashboard } - end - - context 'when an existing project dashboard is specified' do - let(:dashboard) { load_sample_dashboard } - let(:params) { { dashboard_path: '.gitlab/dashboards/test.yml' } } - let(:project) { project_with_dashboard(params[:dashboard_path]) } - - it { is_expected.to eq dashboard } - end - end - - describe '.find_all_paths' do - let(:all_dashboard_paths) { described_class.find_all_paths(project) } - let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Overview', default: true, system_dashboard: true, out_of_the_box_dashboard: true } } - let(:k8s_pod_health_dashboard) { { path: pod_dashboard_path, display_name: 'K8s pod health', default: false, system_dashboard: false, out_of_the_box_dashboard: true } } - - it 'includes OOTB dashboards by default' do - expect(all_dashboard_paths).to eq([k8s_pod_health_dashboard, system_dashboard]) - end - - context 'when the project contains dashboards' do - let(:dashboard_content) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } - let(:project) { project_with_dashboards(dashboards) } - - let(:dashboards) do - { - '.gitlab/dashboards/metrics.yml' => dashboard_content, - '.gitlab/dashboards/better_metrics.yml' => dashboard_content - } - end - - it 'includes OOTB and project dashboards' do - project_dashboard1 = { - path: '.gitlab/dashboards/metrics.yml', - display_name: 'metrics.yml', - default: false, - system_dashboard: false, - out_of_the_box_dashboard: false - } - - project_dashboard2 = { - path: '.gitlab/dashboards/better_metrics.yml', - display_name: 'better_metrics.yml', - default: false, - system_dashboard: false, - out_of_the_box_dashboard: false - } - - expect(all_dashboard_paths).to eq([project_dashboard2, k8s_pod_health_dashboard, project_dashboard1, system_dashboard]) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/importer_spec.rb b/spec/lib/gitlab/metrics/dashboard/importer_spec.rb deleted file mode 100644 index 8b705395a2c..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/importer_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Importer do - include MetricsDashboardHelpers - - let_it_be(:dashboard_path) { '.gitlab/dashboards/sample_dashboard.yml' } - let_it_be(:project) { create(:project) } - - before do - allow(subject).to receive(:dashboard_hash).and_return(dashboard_hash) - end - - subject { described_class.new(dashboard_path, project) } - - describe '.execute' do - context 'valid dashboard hash' do - let(:dashboard_hash) { load_sample_dashboard } - - it 'imports metrics to database' do - expect { subject.execute } - .to change { PrometheusMetric.count }.from(0).to(3) - end - end - - context 'invalid dashboard hash' do - let(:dashboard_hash) { {} } - - it 'returns false' do - expect(subject.execute).to be(false) - end - end - end - - describe '.execute!' do - context 'valid dashboard hash' do - let(:dashboard_hash) { load_sample_dashboard } - - it 'imports metrics to database' do - expect { subject.execute } - .to change { PrometheusMetric.count }.from(0).to(3) - end - end - - context 'invalid dashboard hash' do - let(:dashboard_hash) { {} } - - it 'raises error' do - expect { subject.execute! }.to raise_error(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError, - 'root is missing required keys: dashboard, panel_groups') - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb deleted file mode 100644 index bc6cd383758..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do - include MetricsDashboardHelpers - - describe '#execute' do - let(:project) { create(:project) } - let(:dashboard_path) { 'path/to/dashboard.yml' } - let(:prometheus_adapter) { double('adapter', clear_prometheus_reactive_cache!: nil) } - - subject { described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path) } - - context 'valid dashboard' do - let(:dashboard_hash) { load_sample_dashboard } - - context 'with all new metrics' do - it 'creates PrometheusMetrics' do - expect { subject.execute }.to change { PrometheusMetric.count }.by(3) - end - end - - context 'with existing metrics' do - let(:existing_metric_attributes) do - { - project: project, - identifier: 'metric_b', - title: 'overwrite', - y_label: 'overwrite', - query: 'overwrite', - unit: 'overwrite', - legend: 'overwrite', - dashboard_path: dashboard_path - } - end - - let!(:existing_metric) do - create(:prometheus_metric, existing_metric_attributes) - end - - it 'updates existing PrometheusMetrics' do - subject.execute - - expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', - y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' - }) - end - - it 'creates new PrometheusMetrics' do - expect { subject.execute }.to change { PrometheusMetric.count }.by(2) - end - - context 'with stale metrics' do - let!(:stale_metric) do - create(:prometheus_metric, - project: project, - identifier: 'stale_metric', - dashboard_path: dashboard_path, - group: 3 - ) - end - - it 'updates existing PrometheusMetrics' do - subject.execute - - expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', - y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' - }) - end - - it 'deletes stale metrics' do - subject.execute - - expect { stale_metric.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - end - - context 'invalid dashboard' do - let(:dashboard_hash) { {} } - - it 'returns false' do - expect(subject.execute).to eq(false) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb index 52908a0b339..11b587e4905 100644 --- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -12,11 +12,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do describe 'process' do let(:sequence) do [ - Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, - Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter, - Gitlab::Metrics::Dashboard::Stages::CustomMetricsDetailsInserter, - Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter, - Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter, Gitlab::Metrics::Dashboard::Stages::UrlValidator ] end @@ -24,16 +19,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] } let(:dashboard) { described_class.new(*process_params).process } - it 'includes an id for each dashboard panel' do - expect(all_panels).to satisfy_all do |panel| - panel[:id].present? - end - end - - it 'includes boolean to indicate if panel group has custom metrics' do - expect(dashboard[:panel_groups]).to all(include( { has_custom_metrics: boolean } )) - end - context 'when the dashboard is not present' do let(:dashboard_yml) { nil } @@ -41,168 +26,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do expect(dashboard).to be_nil end end - - context 'when dashboard config corresponds to common metrics' do - let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } - - it 'inserts metric ids into the config' do - target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' } - - expect(target_metric).to include(:metric_id) - expect(target_metric[:metric_id]).to eq(common_metric.id) - end - end - - context 'when the project has associated metrics' do - let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) } - let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) } - let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) } - - it 'includes project-specific metrics' do - expect(all_metrics).to include get_metric_details(project_system_metric) - expect(all_metrics).to include get_metric_details(project_response_metric) - expect(all_metrics).to include get_metric_details(project_business_metric) - end - - it 'display groups and panels in the order they are defined' do - expected_metrics_order = [ - 'metric_b', - 'metric_a2', - 'metric_a1', - project_business_metric.id, - project_response_metric.id, - project_system_metric.id - ] - actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] } - - expect(actual_metrics_order).to eq expected_metrics_order - end - - context 'when the project has multiple metrics in the same group' do - let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) } - let!(:project_response_metric_2) { create(:prometheus_metric, project: project, group: :response) } - - it 'includes multiple metrics' do - expect(all_metrics).to include get_metric_details(project_response_metric) - expect(all_metrics).to include get_metric_details(project_response_metric_2) - end - end - - context 'when the dashboard should not include project metrics' do - let(:sequence) do - [ - Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, - Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter - ] - end - - let(:dashboard) { described_class.new(*process_params).process } - - it 'includes only dashboard metrics' do - metrics = all_metrics.map { |m| m[:id] } - - expect(metrics.length).to be(3) - expect(metrics).to eq %w(metric_b metric_a2 metric_a1) - end - end - - context 'when sample_metrics are requested' do - let(:process_params) { [project, dashboard_yml, sequence, { environment: environment, sample_metrics: true }] } - - it 'includes a sample metrics path for the prometheus endpoint with each metric' do - expect(all_metrics).to satisfy_all do |metric| - metric[:prometheus_endpoint_path] == sample_metrics_path(metric[:id]) - end - end - end - end - - context 'when there are no alerts' do - let!(:persisted_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } - - it 'does not insert an alert_path' do - target_metric = all_metrics.find { |metric| metric[:metric_id] == persisted_metric.id } - - expect(target_metric).to be_a Hash - expect(target_metric).not_to include(:alert_path) - end - end - - shared_examples_for 'errors with message' do |expected_message| - it 'raises a DashboardLayoutError' do - error_class = Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError - - expect { dashboard }.to raise_error(error_class, expected_message) - end - end - - context 'when the dashboard is missing panel_groups' do - let(:dashboard_yml) { {} } - - it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array' - end - - context 'when the dashboard contains a panel_group which is missing panels' do - let(:dashboard_yml) { { panel_groups: [{}] } } - - it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels' - end - - context 'when the dashboard contains a panel which is missing metrics' do - let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } } - - it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics' - end - - context 'when the dashboard contains a metric which is missing a query' do - let(:dashboard_yml) { { panel_groups: [{ panels: [{ metrics: [{}] }] }] } } - - it_behaves_like 'errors with message', 'Each "metric" must define one of :query or :query_range' - end - end - - private - - def all_metrics - all_panels.flat_map { |panel| panel[:metrics] } - end - - def all_panels - dashboard[:panel_groups].flat_map { |group| group[:panels] } - end - - def get_metric_details(metric) - { - query_range: metric.query, - unit: metric.unit, - label: metric.legend, - metric_id: metric.id, - prometheus_endpoint_path: prometheus_path(metric.query), - edit_path: edit_metric_path(metric) - } - end - - def prometheus_path(query) - Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :query_range, - query: query - ) - end - - def sample_metrics_path(metric) - Gitlab::Routing.url_helpers.sample_metrics_project_environment_path( - project, - environment, - identifier: metric - ) - end - - def edit_metric_path(metric) - Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path( - project, - metric.id - ) end end diff --git a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb deleted file mode 100644 index 343596af5cf..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb +++ /dev/null @@ -1,148 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::ServiceSelector do - include MetricsDashboardHelpers - - describe '#call' do - let(:arguments) { {} } - - subject { described_class.call(arguments) } - - it { is_expected.to be Metrics::Dashboard::SystemDashboardService } - - context 'when just the dashboard path is provided' do - let(:arguments) { { dashboard_path: '.gitlab/dashboards/test.yml' } } - - it { is_expected.to be Metrics::Dashboard::CustomDashboardService } - - context 'when the path is for the system dashboard' do - let(:arguments) { { dashboard_path: system_dashboard_path } } - - it { is_expected.to be Metrics::Dashboard::SystemDashboardService } - end - - context 'when the path is for the pod dashboard' do - let(:arguments) { { dashboard_path: pod_dashboard_path } } - - it { is_expected.to be Metrics::Dashboard::PodDashboardService } - end - end - - context 'when the embedded flag is provided' do - let(:arguments) { { embedded: true } } - - it { is_expected.to be Metrics::Dashboard::DefaultEmbedService } - - context 'when an incomplete set of dashboard identifiers are provided' do - let(:arguments) { { embedded: true, dashboard_path: '.gitlab/dashboards/test.yml' } } - - it { is_expected.to be Metrics::Dashboard::DefaultEmbedService } - end - - context 'when all the chart identifiers are provided' do - let(:arguments) do - { - embedded: true, - dashboard_path: '.gitlab/dashboards/test.yml', - group: 'Important Metrics', - title: 'Total Requests', - y_label: 'req/sec' - } - end - - it { is_expected.to be Metrics::Dashboard::DynamicEmbedService } - end - - context 'when all chart params expect dashboard_path are provided' do - let(:arguments) do - { - embedded: true, - group: 'Important Metrics', - title: 'Total Requests', - y_label: 'req/sec' - } - end - - it { is_expected.to be Metrics::Dashboard::DynamicEmbedService } - end - - context 'with a system dashboard and "custom" group' do - let(:arguments) do - { - embedded: true, - dashboard_path: system_dashboard_path, - group: business_metric_title, - title: 'Total Requests', - y_label: 'req/sec' - } - end - - it { is_expected.to be Metrics::Dashboard::CustomMetricEmbedService } - end - - context 'with a grafana link' do - let(:arguments) do - { - embedded: true, - grafana_url: 'https://grafana.example.com' - } - end - - it { is_expected.to be Metrics::Dashboard::GrafanaMetricEmbedService } - end - - context 'with the embed defined in the arguments' do - let(:arguments) do - { - embedded: true, - embed_json: '{}' - } - end - - it { is_expected.to be Metrics::Dashboard::TransientEmbedService } - end - - context 'when cluster is provided' do - let(:arguments) { { cluster: "some cluster" } } - - it { is_expected.to be Metrics::Dashboard::ClusterDashboardService } - end - - context 'when cluster is provided and embedded is not true' do - let(:arguments) { { cluster: "some cluster", embedded: 'false' } } - - it { is_expected.to be Metrics::Dashboard::ClusterDashboardService } - end - - context 'when cluster dashboard_path is provided' do - let(:arguments) { { dashboard_path: ::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH } } - - it { is_expected.to be Metrics::Dashboard::ClusterDashboardService } - end - - context 'when cluster is provided and embed params' do - let(:arguments) do - { - cluster: "some cluster", - embedded: 'true', - cluster_type: 'project', - format: :json, - group: 'Food metrics', - title: 'Pizza Consumption', - y_label: 'Slice Count' - } - end - - it { is_expected.to be Metrics::Dashboard::ClusterMetricsEmbedService } - end - - context 'when metrics embed is for an alert' do - let(:arguments) { { embedded: true, prometheus_alert_id: 5 } } - - it { is_expected.to be Metrics::Dashboard::GitlabAlertEmbedService } - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb deleted file mode 100644 index 3cfdfafb0c5..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do - include GrafanaApiHelpers - - let_it_be(:namespace) { create(:namespace, path: 'foo') } - let_it_be(:project) { create(:project, namespace: namespace, path: 'bar') } - - describe '#transform!' do - let(:grafana_dashboard) { Gitlab::Json.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) } - let(:datasource) { Gitlab::Json.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) } - let(:expected_dashboard) { Gitlab::Json.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) } - - subject(:dashboard) { described_class.new(project, {}, params).transform! } - - let(:params) do - { - grafana_dashboard: grafana_dashboard, - datasource: datasource, - grafana_url: valid_grafana_dashboard_link('https://grafana.example.com') - } - end - - context 'when the query and resources are configured correctly' do - it { is_expected.to eq expected_dashboard } - end - - context 'when a panelId is not included in the grafana_url' do - before do - params[:grafana_url].gsub('&panelId=8', '') - end - - it { is_expected.to eq expected_dashboard } - - context 'when there is also no valid panel in the dashboard' do - before do - params[:grafana_dashboard][:dashboard][:panels] = [] - end - - it 'raises a processing error' do - expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) - end - end - end - - context 'when an input is invalid' do - before do - params[:datasource][:access] = 'not-proxy' - end - - it 'raises a processing error' do - expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb deleted file mode 100644 index bb3c8626d32..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter do - include MetricsDashboardHelpers - - let(:project) { build_stubbed(:project) } - let(:environment) { build_stubbed(:environment, project: project) } - - describe '#transform!' do - subject(:transform!) { described_class.new(project, dashboard, environment: environment).transform! } - - let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } - - it 'generates prometheus_endpoint_path without newlines' do - query = 'avg( sum( container_memory_usage_bytes{ container_name!="POD", '\ - 'pod_name=~"^{{ci_environment_slug}}-(.*)", namespace="{{kube_namespace}}" } ) '\ - 'by (job) ) without (job) /1024/1024/1024' - - transform! - - expect(all_metrics[2][:prometheus_endpoint_path]).to eq(prometheus_path(query)) - end - - it 'includes a path for the prometheus endpoint with each metric' do - transform! - - expect(all_metrics).to satisfy_all do |metric| - metric[:prometheus_endpoint_path].present? && !metric[:prometheus_endpoint_path].include?("\n") - end - end - - it 'works when query/query_range is a number' do - query = 2000 - - transform! - - expect(all_metrics[1][:prometheus_endpoint_path]).to eq(prometheus_path(query)) - end - end - - private - - def all_metrics - dashboard[:panel_groups].flat_map do |group| - group[:panels].flat_map { |panel| panel[:metrics] } - end - end - - def prometheus_path(query) - Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :query_range, - query: query - ) - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb deleted file mode 100644 index 7a3a9021f86..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do - include MetricsDashboardHelpers - - let(:project) { build_stubbed(:project) } - - def fetch_panel_ids(dashboard_hash) - dashboard_hash[:panel_groups].flat_map { |group| group[:panels].flat_map { |panel| panel[:id] } } - end - - describe '#transform!' do - subject(:transform!) { described_class.new(project, dashboard, nil).transform! } - - let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } - - context 'when dashboard panels are present' do - it 'assigns unique ids to each panel using PerformanceMonitoring::PrometheusPanel', :aggregate_failures do - dashboard.fetch(:panel_groups).each do |group| - group.fetch(:panels).each do |panel| - panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel) - - expect(::PerformanceMonitoring::PrometheusPanel).to receive(:new).with(panel).and_return(panel_double) - expect(panel_double).to receive(:id).with(group[:group]).and_return(FFaker::Lorem.unique.characters(125)) - end - end - - transform! - - expect(fetch_panel_ids(dashboard)).not_to include nil - end - end - - context 'when dashboard panels has duplicated ids' do - it 'no panel has assigned id' do - panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel) - allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_return(panel_double) - allow(panel_double).to receive(:id).and_return('duplicated id') - - transform! - - expect(fetch_panel_ids(dashboard)).to all be_nil - expect(fetch_panel_ids(dashboard)).not_to include 'duplicated id' - end - end - - context 'when there are no panels in the dashboard' do - it 'raises a processing error' do - dashboard[:panel_groups][0].delete(:panels) - - expect { transform! }.to( - raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) - ) - end - end - - context 'when there are no panel_groups in the dashboard' do - it 'raises a processing error' do - dashboard.delete(:panel_groups) - - expect { transform! }.to( - raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) - ) - end - end - - context 'when dashboard panels has unknown schema attributes' do - before do - error = ActiveModel::UnknownAttributeError.new(double, 'unknown_panel_attribute') - allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_raise(error) - end - - it 'no panel has assigned id' do - transform! - - expect(fetch_panel_ids(dashboard)).to all be_nil - end - - it 'logs the failure' do - expect(Gitlab::ErrorTracking).to receive(:log_exception) - - transform! - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb deleted file mode 100644 index 60010b9f257..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/track_panel_type_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::TrackPanelType do - include MetricsDashboardHelpers - - let(:project) { build_stubbed(:project) } - let(:environment) { build_stubbed(:environment, project: project) } - - describe '#transform!', :snowplow do - subject { described_class.new(project, dashboard, environment: environment) } - - let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } - - it 'creates tracking event' do - subject.transform! - - expect_snowplow_event( - category: 'MetricsDashboard::Chart', - action: 'chart_rendered', - label: 'area-chart' - ) - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb deleted file mode 100644 index 9303ff981fb..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::VariableEndpointInserter do - include MetricsDashboardHelpers - - let(:project) { build_stubbed(:project) } - let(:environment) { build_stubbed(:environment, project: project) } - - describe '#transform!' do - subject(:transform!) { described_class.new(project, dashboard, environment: environment).transform! } - - let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } - - context 'when dashboard variables are present' do - it 'assigns prometheus_endpoint_path to metric_label_values variable type' do - endpoint_path = Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :series, - match: ['backend:haproxy_backend_availability:ratio{env="{{env}}"}'] - ) - - transform! - - expect( - dashboard.dig(:templating, :variables, :metric_label_values_variable, :options) - ).to include(prometheus_endpoint_path: endpoint_path) - end - - it 'does not modify other variable types' do - original_text_variable = dashboard[:templating][:variables][:text_variable_full_syntax].deep_dup - - transform! - - expect(dashboard[:templating][:variables][:text_variable_full_syntax]).to eq(original_text_variable) - end - - context 'when variable does not have the required series_selector' do - it 'adds prometheus_endpoint_path without match parameter' do - dashboard[:templating][:variables][:metric_label_values_variable][:options].delete(:series_selector) - endpoint_path = Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :series - ) - - transform! - - expect( - dashboard.dig(:templating, :variables, :metric_label_values_variable, :options) - ).to include(prometheus_endpoint_path: endpoint_path) - end - end - end - - context 'when no variables are present' do - it 'does not fail' do - dashboard.delete(:templating) - - expect { transform! }.not_to raise_error - end - end - - context 'with no environment' do - subject(:transform!) { described_class.new(project, dashboard, {}).transform! } - - it 'raises error' do - expect { transform! }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError, - 'Environment is required for Stages::VariableEndpointInserter' - ) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb deleted file mode 100644 index 3af8b51c889..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Transformers::Yml::V1::PrometheusMetrics do - include MetricsDashboardHelpers - - describe '#execute' do - subject { described_class.new(dashboard_hash) } - - context 'valid dashboard' do - let_it_be(:dashboard_hash) do - { - panel_groups: [{ - panels: [ - { - title: 'Panel 1 title', - y_label: 'Panel 1 y_label', - metrics: [ - { - query_range: 'Panel 1 metric 1 query_range', - unit: 'Panel 1 metric 1 unit', - label: 'Panel 1 metric 1 label', - id: 'Panel 1 metric 1 id' - }, - { - query: 'Panel 1 metric 2 query', - unit: 'Panel 1 metric 2 unit', - label: 'Panel 1 metric 2 label', - id: 'Panel 1 metric 2 id' - } - ] - }, - { - title: 'Panel 2 title', - y_label: 'Panel 2 y_label', - metrics: [{ - query_range: 'Panel 2 metric 1 query_range', - unit: 'Panel 2 metric 1 unit', - label: 'Panel 2 metric 1 label', - id: 'Panel 2 metric 1 id' - }] - } - ] - }] - } - end - - let(:expected_metrics) do - [ - { - title: 'Panel 1 title', - y_label: 'Panel 1 y_label', - query: "Panel 1 metric 1 query_range", - unit: 'Panel 1 metric 1 unit', - legend: 'Panel 1 metric 1 label', - identifier: 'Panel 1 metric 1 id', - group: 3, - common: false - }, - { - title: 'Panel 1 title', - y_label: 'Panel 1 y_label', - query: 'Panel 1 metric 2 query', - unit: 'Panel 1 metric 2 unit', - legend: 'Panel 1 metric 2 label', - identifier: 'Panel 1 metric 2 id', - group: 3, - common: false - }, - { - title: 'Panel 2 title', - y_label: 'Panel 2 y_label', - query: 'Panel 2 metric 1 query_range', - unit: 'Panel 2 metric 1 unit', - legend: 'Panel 2 metric 1 label', - identifier: 'Panel 2 metric 1 id', - group: 3, - common: false - } - ] - end - - it 'returns collection of metrics with correct attributes' do - expect(subject.execute).to match_array(expected_metrics) - end - end - - context 'invalid dashboard' do - let(:dashboard_hash) { {} } - - it 'raises missing attribute error' do - expect { subject.execute }.to raise_error( - ::Gitlab::Metrics::Dashboard::Transformers::Errors::MissingAttribute, "Missing attribute: 'panel_groups'" - ) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb deleted file mode 100644 index 4b07f9dbbab..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/validator/client_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Validator::Client do - include MetricsDashboardHelpers - - let_it_be(:schema_path) { 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json' } - - subject { described_class.new(dashboard, schema_path) } - - describe '#execute' do - context 'with no validation errors' do - let(:dashboard) { load_sample_dashboard } - - it 'returns empty array' do - expect(subject.execute).to eq([]) - end - end - - context 'with validation errors' do - let(:dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/invalid_dashboard.yml')) } - - it 'returns array of error objects' do - expect(subject.execute).to include(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb deleted file mode 100644 index 129fb631f3e..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/validator/custom_formats_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Validator::CustomFormats do - describe '#format_handlers' do - describe 'add_to_metric_id_cache' do - it 'adds data to metric id cache' do - subject.format_handlers['add_to_metric_id_cache'].call('metric_id', '_schema') - - expect(subject.metric_ids_cache).to eq(["metric_id"]) - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb deleted file mode 100644 index a50c2a506cb..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do - describe Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError do - context 'empty error hash' do - let(:error_hash) { {} } - - it 'uses default error message' do - expect(described_class.new(error_hash).message).to eq('Dashboard failed schema validation') - end - end - - context 'formatted message' do - subject { described_class.new(error_hash).message } - - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema', - 'details' => details - } - end - - context 'for root object' do - let(:pointer) { '' } - - context 'when required keys are missing' do - let(:type) { 'required' } - let(:details) { { 'missing_keys' => ['one'] } } - - it { is_expected.to eq 'root is missing required keys: one' } - end - - context 'when there is type mismatch' do - %w(null string boolean integer number array object).each do |expected_type| - context "on type: #{expected_type}" do - let(:type) { expected_type } - let(:details) { nil } - - it { is_expected.to eq "'property_name' at root is not of type: #{expected_type}" } - end - end - end - end - - context 'for nested object' do - let(:pointer) { '/nested_objects/0' } - - context 'when required keys are missing' do - let(:type) { 'required' } - let(:details) { { 'missing_keys' => ['two'] } } - - it { is_expected.to eq '/nested_objects/0 is missing required keys: two' } - end - - context 'when there is type mismatch' do - %w(null string boolean integer number array object).each do |expected_type| - context "on type: #{expected_type}" do - let(:type) { expected_type } - let(:details) { nil } - - it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" } - end - end - end - - context 'when data does not match pattern' do - let(:type) { 'pattern' } - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'pattern' => 'aa.*' } - } - end - - it { is_expected.to eq "'property_name' at /nested_objects/0 does not match pattern: aa.*" } - end - - context 'when data does not match format' do - let(:type) { 'format' } - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'format' => 'date-time' } - } - end - - it { is_expected.to eq "'property_name' at /nested_objects/0 does not match format: date-time" } - end - - context 'when data is not const' do - let(:type) { 'const' } - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'const' => 'one' } - } - end - - it { is_expected.to eq "'property_name' at /nested_objects/0 is not: \"one\"" } - end - - context 'when data is not included in enum' do - let(:type) { 'enum' } - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'enum' => %w(one two) } - } - end - - it { is_expected.to eq "'property_name' at /nested_objects/0 is not one of: [\"one\", \"two\"]" } - end - - context 'when data is not included in enum' do - let(:type) { 'unknown' } - let(:error_hash) do - { - 'data' => 'property_name', - 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema' - } - end - - it { is_expected.to eq "'property_name' at /nested_objects/0 is invalid: error_type=unknown" } - end - end - end - end - - describe Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds do - it 'has custom error message' do - expect(described_class.new.message).to eq('metric_id must be unique across a project') - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb deleted file mode 100644 index e7cb1429ca9..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/validator/post_schema_validator_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Validator::PostSchemaValidator do - describe '#validate' do - context 'with no project and dashboard_path provided' do - context 'unique local metric_ids' do - it 'returns empty array' do - expect(described_class.new(metric_ids: [1, 2, 3]).validate).to eq([]) - end - end - - context 'duplicate local metrics_ids' do - it 'returns error' do - expect(described_class.new(metric_ids: [1, 1]).validate) - .to eq([Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds]) - end - end - end - - context 'with project and dashboard_path' do - let(:project) { create(:project) } - - subject do - described_class.new( - project: project, - metric_ids: ['some_identifier'], - dashboard_path: 'test/path.yml' - ).validate - end - - context 'with unique metric identifiers' do - before do - create(:prometheus_metric, - project: project, - identifier: 'some_other_identifier', - dashboard_path: 'test/path.yml' - ) - end - - it 'returns empty array' do - expect(subject).to eq([]) - end - end - - context 'duplicate metric identifiers in database' do - context 'with different dashboard_path' do - before do - create(:prometheus_metric, - project: project, - identifier: 'some_identifier', - dashboard_path: 'some/other/path.yml' - ) - end - - it 'returns error' do - expect(subject).to include(Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds) - end - end - - context 'with same dashboard_path' do - before do - create(:prometheus_metric, - project: project, - identifier: 'some_identifier', - dashboard_path: 'test/path.yml' - ) - end - - it 'returns empty array' do - expect(subject).to eq([]) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb deleted file mode 100644 index fb55b736354..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Validator do - include MetricsDashboardHelpers - - let_it_be(:valid_dashboard) { load_sample_dashboard } - let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/invalid_dashboard.yml')) } - let_it_be(:duplicate_id_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/duplicate_id_dashboard.yml')) } - - let_it_be(:project) { create(:project) } - - describe '#validate' do - context 'valid dashboard schema' do - it 'returns true' do - expect(described_class.validate(valid_dashboard)).to be true - end - - context 'with duplicate metric_ids' do - it 'returns false' do - expect(described_class.validate(duplicate_id_dashboard)).to be false - end - end - - context 'with dashboard_path and project' do - subject { described_class.validate(valid_dashboard, dashboard_path: 'test/path.yml', project: project) } - - context 'with no conflicting metric identifiers in db' do - it { is_expected.to be true } - end - - context 'with metric identifier present in current dashboard' do - before do - create(:prometheus_metric, - identifier: 'metric_a1', - dashboard_path: 'test/path.yml', - project: project - ) - end - - it { is_expected.to be true } - end - - context 'with metric identifier present in another dashboard' do - before do - create(:prometheus_metric, - identifier: 'metric_a1', - dashboard_path: 'some/other/dashboard/path.yml', - project: project - ) - end - - it { is_expected.to be false } - end - end - end - - context 'invalid dashboard schema' do - it 'returns false' do - expect(described_class.validate(invalid_dashboard)).to be false - end - end - end - - describe '#validate!' do - shared_examples 'validation failed' do |errors_message| - it 'raises error with corresponding messages', :aggregate_failures do - expect { subject }.to raise_error do |error| - expect(error).to be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::InvalidDashboardError) - expect(error.message).to eq(errors_message) - end - end - end - - context 'valid dashboard schema' do - it 'returns true' do - expect(described_class.validate!(valid_dashboard)).to be true - end - - context 'with duplicate metric_ids' do - subject { described_class.validate!(duplicate_id_dashboard) } - - it_behaves_like 'validation failed', 'metric_id must be unique across a project' - end - - context 'with dashboard_path and project' do - subject { described_class.validate!(valid_dashboard, dashboard_path: 'test/path.yml', project: project) } - - context 'with no conflicting metric identifiers in db' do - it { is_expected.to be true } - end - - context 'with metric identifier present in current dashboard' do - before do - create(:prometheus_metric, - identifier: 'metric_a1', - dashboard_path: 'test/path.yml', - project: project - ) - end - - it { is_expected.to be true } - end - - context 'with metric identifier present in another dashboard' do - before do - create(:prometheus_metric, - identifier: 'metric_a1', - dashboard_path: 'some/other/dashboard/path.yml', - project: project - ) - end - - it_behaves_like 'validation failed', 'metric_id must be unique across a project' - end - end - end - - context 'invalid dashboard schema' do - subject { described_class.validate!(invalid_dashboard) } - - context 'wrong property type' do - it_behaves_like 'validation failed', "'this_should_be_a_int' at /panel_groups/0/panels/0/weight is not of type: number" - end - - context 'panel groups missing' do - let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } - - it_behaves_like 'validation failed', 'root is missing required keys: panel_groups' - end - - context 'groups are missing panels and group keys' do - let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_groups_missing_panels_and_group.yml')) } - - it_behaves_like 'validation failed', '/panel_groups/0 is missing required keys: group' - end - - context 'panel is missing metrics key' do - let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_panel_is_missing_metrics.yml')) } - - it_behaves_like 'validation failed', '/panel_groups/0/panels/0 is missing required keys: metrics' - end - end - end -end diff --git a/spec/lib/gitlab/metrics/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb index 5248cd08770..68793db6e41 100644 --- a/spec/lib/gitlab/metrics/global_search_slis_spec.rb +++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Metrics::GlobalSearchSlis do +RSpec.describe Gitlab::Metrics::GlobalSearchSlis, feature_category: :global_search do using RSpec::Parameterized::TableSyntax describe '#initialize_slis!' do @@ -92,6 +92,7 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do 'basic' | true | 27.538 'advanced' | false | 2.452 'advanced' | true | 15.52 + 'zoekt' | true | 15.52 end with_them do diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index afb029a96cb..2ec31a5cc3e 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -384,7 +384,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do end it 'does not store DB roles into into RequestStore' do - Gitlab::WithRequestStore.with_request_store do + Gitlab::SafeRequestStore.ensure_request_store do subscriber.sql(event) expect(described_class.db_counter_payload).to include( diff --git a/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb b/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb index c8dbc990f8c..5394cea64af 100644 --- a/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb +++ b/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Middleware::WebhookRecursionDetection do let(:env) { Rack::MockRequest.env_for("/").merge(headers) } around do |example| - Gitlab::WithRequestStore.with_request_store { example.run } + Gitlab::SafeRequestStore.ensure_request_store { example.run } end describe '#call' do diff --git a/spec/lib/gitlab/null_request_store_spec.rb b/spec/lib/gitlab/null_request_store_spec.rb deleted file mode 100644 index f68f478c73e..00000000000 --- a/spec/lib/gitlab/null_request_store_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::NullRequestStore do - let(:null_store) { described_class.new } - - describe '#store' do - it 'returns an empty hash' do - expect(null_store.store).to eq({}) - end - end - - describe '#active?' do - it 'returns falsey' do - expect(null_store.active?).to be_falsey - end - end - - describe '#read' do - it 'returns nil' do - expect(null_store.read('foo')).to be nil - end - end - - describe '#[]' do - it 'returns nil' do - expect(null_store['foo']).to be nil - end - end - - describe '#write' do - it 'returns the same value' do - expect(null_store.write('key', 'value')).to eq('value') - end - end - - describe '#[]=' do - it 'returns the same value' do - expect(null_store['key'] = 'value').to eq('value') - end - end - - describe '#exist?' do - it 'returns falsey' do - expect(null_store.exist?('foo')).to be_falsey - end - end - - describe '#fetch' do - it 'returns the block result' do - expect(null_store.fetch('key') { 'block result' }).to eq('block result') # rubocop:disable Style/RedundantFetchBlock - end - end - - describe '#delete' do - context 'when a block is given' do - it 'yields the key to the block' do - expect do |b| - null_store.delete('foo', &b) - end.to yield_with_args('foo') - end - - it 'returns the block result' do - expect(null_store.delete('foo') { |key| 'block result' }).to eq('block result') - end - end - - context 'when a block is not given' do - it 'returns nil' do - expect(null_store.delete('foo')).to be nil - end - end - end -end diff --git a/spec/lib/gitlab/pages/url_builder_spec.rb b/spec/lib/gitlab/pages/url_builder_spec.rb index 8e1581704cb..ae94bbadffe 100644 --- a/spec/lib/gitlab/pages/url_builder_spec.rb +++ b/spec/lib/gitlab/pages/url_builder_spec.rb @@ -83,60 +83,32 @@ RSpec.describe Gitlab::Pages::UrlBuilder, feature_category: :pages do context 'when not using pages_unique_domain' do subject(:pages_url) { builder.pages_url(with_unique_domain: false) } - context 'when pages_unique_domain feature flag is disabled' do - before do - stub_feature_flags(pages_unique_domain: false) - end + context 'when pages_unique_domain_enabled is false' do + let(:unique_domain_enabled) { false } it { is_expected.to eq('http://group.example.com/project') } end - context 'when pages_unique_domain feature flag is enabled' do - before do - stub_feature_flags(pages_unique_domain: true) - end - - context 'when pages_unique_domain_enabled is false' do - let(:unique_domain_enabled) { false } - - it { is_expected.to eq('http://group.example.com/project') } - end - - context 'when pages_unique_domain_enabled is true' do - let(:unique_domain_enabled) { true } + context 'when pages_unique_domain_enabled is true' do + let(:unique_domain_enabled) { true } - it { is_expected.to eq('http://group.example.com/project') } - end + it { is_expected.to eq('http://group.example.com/project') } end end context 'when using pages_unique_domain' do subject(:pages_url) { builder.pages_url(with_unique_domain: true) } - context 'when pages_unique_domain feature flag is disabled' do - before do - stub_feature_flags(pages_unique_domain: false) - end + context 'when pages_unique_domain_enabled is false' do + let(:unique_domain_enabled) { false } it { is_expected.to eq('http://group.example.com/project') } end - context 'when pages_unique_domain feature flag is enabled' do - before do - stub_feature_flags(pages_unique_domain: true) - end - - context 'when pages_unique_domain_enabled is false' do - let(:unique_domain_enabled) { false } - - it { is_expected.to eq('http://group.example.com/project') } - end - - context 'when pages_unique_domain_enabled is true' do - let(:unique_domain_enabled) { true } + context 'when pages_unique_domain_enabled is true' do + let(:unique_domain_enabled) { true } - it { is_expected.to eq('http://unique-domain.example.com') } - end + it { is_expected.to eq('http://unique-domain.example.com') } end end end @@ -144,30 +116,16 @@ RSpec.describe Gitlab::Pages::UrlBuilder, feature_category: :pages do describe '#unique_host' do subject(:unique_host) { builder.unique_host } - context 'when pages_unique_domain feature flag is disabled' do - before do - stub_feature_flags(pages_unique_domain: false) - end + context 'when pages_unique_domain_enabled is false' do + let(:unique_domain_enabled) { false } it { is_expected.to be_nil } end - context 'when pages_unique_domain feature flag is enabled' do - before do - stub_feature_flags(pages_unique_domain: true) - end + context 'when pages_unique_domain_enabled is true' do + let(:unique_domain_enabled) { true } - context 'when pages_unique_domain_enabled is false' do - let(:unique_domain_enabled) { false } - - it { is_expected.to be_nil } - end - - context 'when pages_unique_domain_enabled is true' do - let(:unique_domain_enabled) { true } - - it { is_expected.to eq('unique-domain.example.com') } - end + it { is_expected.to eq('unique-domain.example.com') } end end diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb index 3fe858f33da..ddaf555dae6 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb @@ -32,6 +32,12 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R end end + let_it_be(:model_without_ignored_columns) do + Class.new(ApplicationRecord) do + self.table_name = 'projects' + end + end + subject(:strategy) { described_class.new(finder_query, model, order_by_columns) } describe '#initializer_columns' do @@ -70,6 +76,8 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R describe '#final_projections' do context 'when model does not have ignored columns' do + let(:model) { model_without_ignored_columns } + it 'does not specify the selected column names' do expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*") end diff --git a/spec/lib/gitlab/plantuml_spec.rb b/spec/lib/gitlab/plantuml_spec.rb index c783dd66c48..c2cce59cf90 100644 --- a/spec/lib/gitlab/plantuml_spec.rb +++ b/spec/lib/gitlab/plantuml_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Plantuml, feature_category: :shared do let(:plantuml_url) { "http://plantuml.foo.bar" } before do - allow(Gitlab::CurrentSettings).to receive(:plantuml_url).and_return(plantuml_url) + stub_application_setting(plantuml_url: plantuml_url) end context "when PlantUML is enabled" do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index a762fdbde6b..8f74963d60b 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ProjectSearchResults do +RSpec.describe Gitlab::ProjectSearchResults, feature_category: :global_search do include SearchHelpers let_it_be(:user) { create(:user) } diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index b7b4ba0eb2f..a48bde5e4ab 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -17,5 +17,9 @@ RSpec.describe Gitlab::Redis::Cache do expect(described_class.active_support_config[:expires_in]).to eq(1.day) end + + it 'has a pool set to false' do + expect(described_class.active_support_config[:pool]).to eq(false) + end end end diff --git a/spec/lib/gitlab/redis/cluster_cache_spec.rb b/spec/lib/gitlab/redis/cluster_shared_state_spec.rb index e448d608c53..11a574c79c4 100644 --- a/spec/lib/gitlab/redis/cluster_cache_spec.rb +++ b/spec/lib/gitlab/redis/cluster_shared_state_spec.rb @@ -2,6 +2,6 @@ require 'spec_helper' -RSpec.describe Gitlab::Redis::ClusterCache, feature_category: :redis do - include_examples "redis_new_instance_shared_examples", 'cluster_cache', Gitlab::Redis::Cache +RSpec.describe Gitlab::Redis::ClusterSharedState, feature_category: :redis do + include_examples "redis_new_instance_shared_examples", 'cluster_shared_state', Gitlab::Redis::SharedState end diff --git a/spec/lib/gitlab/redis/etag_cache_spec.rb b/spec/lib/gitlab/redis/etag_cache_spec.rb new file mode 100644 index 00000000000..182a41bac80 --- /dev/null +++ b/spec/lib/gitlab/redis/etag_cache_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Redis::EtagCache, feature_category: :shared do + # Note: this is a pseudo-store in front of `Cache`, meant only as a tool + # to move away from `SharedState` for etag cache data. Thus, we use the + # same store configuration as the former. + let(:instance_specific_config_file) { "config/redis.cache.yml" } + + include_examples "redis_shared_examples" + + describe '#pool' do + let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } + let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } + let(:rails_root) { mktmpdir } + + subject { described_class.pool } + + before do + # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists + allow(Gitlab::Redis::SharedState).to receive(:rails_root).and_return(rails_root) + allow(Gitlab::Redis::Cache).to receive(:rails_root).and_return(rails_root) + + allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host) + allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(config_new_format_socket) + end + + around do |example| + clear_pool + example.run + ensure + clear_pool + end + + it 'instantiates an instance of MultiStore' do + subject.with do |redis_instance| + expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) + + expect(redis_instance.primary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") + expect(redis_instance.secondary_store.connection[:id]).to eq("redis://test-host:6379/99") + + expect(redis_instance.instance_name).to eq('EtagCache') + end + end + + it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_etag_cache, + :use_primary_store_as_default_for_etag_cache + end + + describe '#store_name' do + it 'returns the name of the Cache store' do + expect(described_class.store_name).to eq('Cache') + end + end +end diff --git a/spec/lib/gitlab/regex_requires_app_spec.rb b/spec/lib/gitlab/regex_requires_app_spec.rb index 780184cdfd2..bea5d25dbc8 100644 --- a/spec/lib/gitlab/regex_requires_app_spec.rb +++ b/spec/lib/gitlab/regex_requires_app_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' # Only specs that *cannot* be run with fast_spec_helper only # See regex_spec for tests that do not require the full spec_helper -RSpec.describe Gitlab::Regex do +RSpec.describe Gitlab::Regex, feature_category: :tooling do describe '.debian_architecture_regex' do subject { described_class.debian_architecture_regex } diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 5e58282ff92..c91b99caba2 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -65,13 +65,25 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do describe '.project_name_regex_message' do subject { described_class.project_name_regex_message } - it { is_expected.to eq("can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces. It must start with a letter, digit, emoji, or '_'.") } + it { is_expected.to eq("can contain only letters, digits, emoji, '_', '.', '+', dashes, or spaces. It must start with a letter, digit, emoji, or '_'.") } end describe '.group_name_regex_message' do subject { described_class.group_name_regex_message } - it { is_expected.to eq("can contain only letters, digits, emojis, '_', '.', dash, space, parenthesis. It must start with letter, digit, emoji or '_'.") } + it { is_expected.to eq("can contain only letters, digits, emoji, '_', '.', dash, space, parenthesis. It must start with letter, digit, emoji or '_'.") } + end + + describe '.slack_link_regex' do + subject { described_class.slack_link_regex } + + it { is_expected.not_to match('http://custom-url.com|click here') } + it { is_expected.not_to match('custom-url.com|any-Charact3r$') } + it { is_expected.not_to match("<custom-url.com|any-Charact3r$>") } + + it { is_expected.to match('<http://custom-url.com|click here>') } + it { is_expected.to match('<custom-url.com|any-Charact3r$>') } + it { is_expected.to match('<any-Charact3r$|any-Charact3r$>') } end describe '.bulk_import_destination_namespace_path_regex_message' do @@ -820,6 +832,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do it { is_expected.to match('1.2.3') } it { is_expected.to match('1.2.3-beta') } it { is_expected.to match('1.2.3-alpha.3') } + it { is_expected.to match('1.2.3-alpha.3+abcd') } it { is_expected.not_to match('1') } it { is_expected.not_to match('1.2') } it { is_expected.not_to match('1./2.3') } diff --git a/spec/lib/gitlab/repository_size_checker_spec.rb b/spec/lib/gitlab/repository_size_checker_spec.rb index 559f5fa66c6..15c05a07ebb 100644 --- a/spec/lib/gitlab/repository_size_checker_spec.rb +++ b/spec/lib/gitlab/repository_size_checker_spec.rb @@ -36,13 +36,14 @@ RSpec.describe Gitlab::RepositorySizeChecker do describe '#changes_will_exceed_size_limit?' do let(:current_size) { 49 } + let(:project) { double } it 'returns true when changes go over' do - expect(subject.changes_will_exceed_size_limit?(2.megabytes)).to eq(true) + expect(subject.changes_will_exceed_size_limit?(2.megabytes, project)).to eq(true) end it 'returns false when changes do not go over' do - expect(subject.changes_will_exceed_size_limit?(1.megabytes)).to eq(false) + expect(subject.changes_will_exceed_size_limit?(1.megabytes, project)).to eq(false) end end diff --git a/spec/lib/gitlab/safe_request_store_spec.rb b/spec/lib/gitlab/safe_request_store_spec.rb deleted file mode 100644 index accc491fbb7..00000000000 --- a/spec/lib/gitlab/safe_request_store_spec.rb +++ /dev/null @@ -1,257 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::SafeRequestStore do - describe '.store' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect(described_class.store).to eq(RequestStore) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect(described_class.store).to be_a(Gitlab::NullRequestStore) - end - end - end - - describe '.begin!' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect(RequestStore).to receive(:begin!) - - described_class.begin! - end - end - - context 'when RequestStore is NOT active' do - it 'uses RequestStore' do - expect(RequestStore).to receive(:begin!) - - described_class.begin! - end - end - end - - describe '.clear!' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect(RequestStore).to receive(:clear!).once.and_call_original - - described_class.clear! - end - end - - context 'when RequestStore is NOT active' do - it 'uses RequestStore' do - expect(RequestStore).to receive(:clear!).and_call_original - - described_class.clear! - end - end - end - - describe '.end!' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect(RequestStore).to receive(:end!).once.and_call_original - - described_class.end! - end - end - - context 'when RequestStore is NOT active' do - it 'uses RequestStore' do - expect(RequestStore).to receive(:end!).and_call_original - - described_class.end! - end - end - end - - describe '.write' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - described_class.write('foo', true) - end.to change { described_class.read('foo') }.from(nil).to(true) - end - - it 'does not pass the options hash to the underlying store implementation' do - expect(described_class.store).to receive(:write).with('foo', true) - - described_class.write('foo', true, expires_in: 15.seconds) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect do - described_class.write('foo', true) - end.not_to change { described_class.read('foo') }.from(nil) - end - - it 'does not pass the options hash to the underlying store implementation' do - expect(described_class.store).to receive(:write).with('foo', true) - - described_class.write('foo', true, expires_in: 15.seconds) - end - end - end - - describe '.[]=' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - described_class['foo'] = true - end.to change { described_class.read('foo') }.from(nil).to(true) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect do - described_class['foo'] = true - end.not_to change { described_class.read('foo') }.from(nil) - end - end - end - - describe '.read' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - RequestStore.write('foo', true) - end.to change { described_class.read('foo') }.from(nil).to(true) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect do - RequestStore.write('foo', true) - end.not_to change { described_class.read('foo') }.from(nil) - - RequestStore.clear! # Clean up - end - end - end - - describe '.[]' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - RequestStore.write('foo', true) - end.to change { described_class['foo'] }.from(nil).to(true) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect do - RequestStore.write('foo', true) - end.not_to change { described_class['foo'] }.from(nil) - - RequestStore.clear! # Clean up - end - end - end - - describe '.exist?' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - RequestStore.write('foo', 'not nil') - end.to change { described_class.exist?('foo') }.from(false).to(true) - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - expect do - RequestStore.write('foo', 'not nil') - end.not_to change { described_class.exist?('foo') }.from(false) - - RequestStore.clear! # Clean up - end - end - end - - describe '.fetch' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - expect do - described_class.fetch('foo') { 'block result' } # rubocop:disable Style/RedundantFetchBlock - end.to change { described_class.read('foo') }.from(nil).to('block result') - end - end - - context 'when RequestStore is NOT active' do - it 'does not use RequestStore' do - RequestStore.clear! # Ensure clean - - expect do - described_class.fetch('foo') { 'block result' } # rubocop:disable Style/RedundantFetchBlock - end.not_to change { described_class.read('foo') }.from(nil) - - RequestStore.clear! # Clean up - end - end - end - - describe '.delete' do - context 'when RequestStore is active', :request_store do - it 'uses RequestStore' do - described_class.write('foo', true) - - expect do - described_class.delete('foo') - end.to change { described_class.read('foo') }.from(true).to(nil) - end - - context 'when given a block and the key exists' do - it 'does not execute the block' do - described_class.write('foo', true) - - expect do |b| - described_class.delete('foo', &b) - end.not_to yield_control - end - end - - context 'when given a block and the key does not exist' do - it 'yields the key and returns the block result' do - result = described_class.delete('foo') { |key| "#{key} block result" } - - expect(result).to eq('foo block result') - end - end - end - - context 'when RequestStore is NOT active' do - before do - RequestStore.write('foo', true) - end - - after do - RequestStore.clear! # Clean up - end - - it 'does not use RequestStore' do - expect do - described_class.delete('foo') - end.not_to change { RequestStore.read('foo') }.from(true) - end - - context 'when given a block' do - it 'yields the key and returns the block result' do - result = described_class.delete('foo') { |key| "#{key} block result" } - - expect(result).to eq('foo block result') - end - end - end - end -end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 662eab11cc0..725b7901e68 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -187,11 +187,16 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do end context 'filtering' do + let_it_be(:unarchived_project) { create(:project, :public) } + let_it_be(:archived_project) { create(:project, :public, :archived) } let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') } let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') } + let(:unarchived_result) { create(:merge_request, source_project: unarchived_project, title: 'foo unarchived') } + let(:archived_result) { create(:merge_request, source_project: archived_project, title: 'foo archived') } let(:query) { 'foo' } include_examples 'search results filtered by state' + include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects' end context 'ordering' do @@ -266,25 +271,10 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do describe 'filtering' do let_it_be(:group) { create(:group) } - let_it_be(:unarchived_project) { create(:project, :public, group: group, name: 'Test1') } - let_it_be(:archived_project) { create(:project, :archived, :public, group: group, name: 'Test2') } + let_it_be(:unarchived_result) { create(:project, :public, group: group, name: 'Test1') } + let_it_be(:archived_result) { create(:project, :archived, :public, group: group, name: 'Test2') } - it_behaves_like 'search results filtered by archived' - - context 'when the search_projects_hide_archived feature flag is disabled' do - before do - stub_feature_flags(search_projects_hide_archived: false) - end - - context 'when filter not provided' do - let(:filters) { {} } - - it 'returns archived and unarchived results', :aggregate_failures do - expect(results.objects('projects')).to include unarchived_project - expect(results.objects('projects')).to include archived_project - end - end - end + it_behaves_like 'search results filtered by archived', 'search_projects_hide_archived' end end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 4e46a26e89f..4550ccc2fff 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -463,11 +463,12 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do let(:expected_end_payload) do end_payload.merge( 'urgency' => 'high', - 'target_duration_s' => 10 + 'target_duration_s' => 10, + 'target_scheduling_latency_s' => 10 ) end - it 'logs job done with urgency and target_duration_s fields' do + it 'logs job done with urgency, target_duration_s and target_scheduling_latency_s fields' do travel_to(timestamp) do expect(logger).to receive(:info).with(start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered diff --git a/spec/lib/gitlab/sidekiq_middleware/pause_control/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/pause_control/client_spec.rb new file mode 100644 index 00000000000..0a837f6f932 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/pause_control/client_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::PauseControl::Client, :clean_gitlab_redis_queues, feature_category: :global_search do + let(:worker_class) do + Class.new do + def self.name + 'TestPauseWorker' + end + + include ApplicationWorker + + pause_control :zoekt + + def perform(*); end + end + end + + before do + stub_const('TestPauseWorker', worker_class) + end + + describe '#call' do + context 'when strategy is enabled' do + before do + stub_feature_flags(zoekt_pause_indexing: true) + end + + it 'does not schedule the job' do + expect(Gitlab::SidekiqMiddleware::PauseControl::PauseControlService).to receive(:add_to_waiting_queue!).once + + TestPauseWorker.perform_async('args1') + + expect(TestPauseWorker.jobs.count).to eq(0) + end + end + + context 'when strategy is disabled' do + before do + stub_feature_flags(zoekt_pause_indexing: false) + end + + it 'schedules the job' do + expect(Gitlab::SidekiqMiddleware::PauseControl::PauseControlService).not_to receive(:add_to_waiting_queue!) + + TestPauseWorker.perform_async('args1') + + expect(TestPauseWorker.jobs.count).to eq(1) + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/pause_control/pause_control_service_spec.rb b/spec/lib/gitlab/sidekiq_middleware/pause_control/pause_control_service_spec.rb new file mode 100644 index 00000000000..1de8bd9f7ad --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/pause_control/pause_control_service_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::PauseControl::PauseControlService, :clean_gitlab_redis_shared_state, feature_category: :global_search do + let(:worker_class) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + end + end + + let(:worker_class_name) { worker_class.name } + + let(:worker_context) do + { 'correlation_id' => 'context_correlation_id', + 'meta.project' => 'gitlab-org/gitlab' } + end + + let(:stored_context) do + { "#{Gitlab::ApplicationContext::LOG_KEY}.project" => 'gitlab-org/gitlab' } + end + + let(:worker_args) { [1, 2] } + + subject { described_class.new(worker_class_name) } + + before do + stub_const(worker_class_name, worker_class) + end + + describe '.add_to_waiting_queue!' do + it 'calls an instance method' do + expect_next_instance_of(described_class) do |instance| + expect(instance).to receive(:add_to_waiting_queue!).with(worker_args, worker_context) + end + + described_class.add_to_waiting_queue!(worker_class_name, worker_args, worker_context) + end + end + + describe '.has_jobs_in_waiting_queue?' do + it 'calls an instance method' do + expect_next_instance_of(described_class) do |instance| + expect(instance).to receive(:has_jobs_in_waiting_queue?) + end + + described_class.has_jobs_in_waiting_queue?(worker_class_name) + end + end + + describe '.resume_processing!' do + it 'calls an instance method' do + expect_next_instance_of(described_class) do |instance| + expect(instance).to receive(:resume_processing!) + end + + described_class.resume_processing!(worker_class_name) + end + end + + describe '.queue_size' do + it 'reports the queue size' do + expect(described_class.queue_size(worker_class_name)).to eq(0) + + subject.add_to_waiting_queue!(worker_args, worker_context) + + expect(described_class.queue_size(worker_class_name)).to eq(1) + + expect { subject.resume_processing! }.to change { described_class.queue_size(worker_class_name) }.by(-1) + end + end + + describe '#add_to_waiting_queue!' do + it 'adds a job to the set' do + expect { subject.add_to_waiting_queue!(worker_args, worker_context) } + .to change { subject.queue_size } + .from(0).to(1) + end + + it 'adds only one unique job to the set' do + expect do + 2.times { subject.add_to_waiting_queue!(worker_args, worker_context) } + end.to change { subject.queue_size }.from(0).to(1) + end + + it 'only stores `project` context information' do + subject.add_to_waiting_queue!(worker_args, worker_context) + + subject.send(:with_redis) do |r| + set_key = subject.send(:redis_set_key) + stored_job = subject.send(:deserialize, r.zrange(set_key, 0, -1).first) + + expect(stored_job['context']).to eq(stored_context) + end + end + end + + describe '#has_jobs_in_waiting_queue?' do + it 'checks set existence' do + expect { subject.add_to_waiting_queue!(worker_args, worker_context) } + .to change { subject.has_jobs_in_waiting_queue? } + .from(false).to(true) + end + end + + describe '#resume_processing!' do + let(:jobs) { [[1], [2], [3]] } + + it 'puts jobs back into the queue and respects order' do + # We stub this const to test at least a couple of loop iterations + stub_const("#{described_class}::LIMIT", 2) + + jobs.each do |j| + subject.add_to_waiting_queue!(j, worker_context) + end + + expect(worker_class).to receive(:perform_async).with(1).ordered + expect(worker_class).to receive(:perform_async).with(2).ordered + expect(worker_class).not_to receive(:perform_async).with(3).ordered + + expect(Gitlab::SidekiqLogging::PauseControlLogger.instance).to receive(:resumed_log).with(worker_class_name, [1]) + expect(Gitlab::SidekiqLogging::PauseControlLogger.instance).to receive(:resumed_log).with(worker_class_name, [2]) + + subject.resume_processing! + end + + it 'drops a set after execution' do + jobs.each do |j| + subject.add_to_waiting_queue!(j, worker_context) + end + + expect(Gitlab::ApplicationContext).to receive(:with_raw_context) + .with(stored_context) + .exactly(jobs.count).times.and_call_original + expect(worker_class).to receive(:perform_async).exactly(jobs.count).times + + expect { subject.resume_processing! }.to change { subject.has_jobs_in_waiting_queue? }.from(true).to(false) + end + end + + context 'with concurrent changes to different queues' do + let(:second_worker_class) do + Class.new do + def self.name + 'SecondDummyIndexingWorker' + end + + include ApplicationWorker + end + end + + let(:other_subject) { described_class.new(second_worker_class.name) } + + before do + stub_const(second_worker_class.name, second_worker_class) + end + + it 'allows to use queues independently of each other' do + expect { subject.add_to_waiting_queue!(worker_args, worker_context) } + .to change { subject.queue_size } + .from(0).to(1) + + expect { other_subject.add_to_waiting_queue!(worker_args, worker_context) } + .to change { other_subject.queue_size } + .from(0).to(1) + + expect { subject.resume_processing! }.to change { subject.has_jobs_in_waiting_queue? } + .from(true).to(false) + + expect { other_subject.resume_processing! }.to change { other_subject.has_jobs_in_waiting_queue? } + .from(true).to(false) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/pause_control/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/pause_control/server_spec.rb new file mode 100644 index 00000000000..c577f9697b2 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/pause_control/server_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::PauseControl::Server, :clean_gitlab_redis_queues, feature_category: :global_search do + let(:worker_class) do + Class.new do + def self.name + 'TestPauseWorker' + end + + include ApplicationWorker + + pause_control :zoekt + + def perform(*) + self.class.work + end + + def self.work; end + end + end + + before do + stub_const('TestPauseWorker', worker_class) + end + + around do |example| + with_sidekiq_server_middleware do |chain| + chain.add described_class + Sidekiq::Testing.inline! { example.run } + end + end + + describe '#call' do + context 'when strategy is enabled' do + before do + stub_feature_flags(zoekt_pause_indexing: true) + end + + it 'puts the job to another queue without execution' do + bare_job = { 'class' => 'TestPauseWorker', 'args' => ['hello'] } + job_definition = Gitlab::SidekiqMiddleware::PauseControl::StrategyHandler.new(TestPauseWorker, bare_job.dup) + + expect(Gitlab::SidekiqMiddleware::PauseControl::StrategyHandler) + .to receive(:new).with(TestPauseWorker, a_hash_including(bare_job)) + .and_return(job_definition).once + + expect(TestPauseWorker).not_to receive(:work) + expect(Gitlab::SidekiqMiddleware::PauseControl::PauseControlService).to receive(:add_to_waiting_queue!).once + + TestPauseWorker.perform_async('hello') + end + end + + context 'when strategy is disabled' do + before do + stub_feature_flags(zoekt_pause_indexing: false) + end + + it 'executes the job' do + bare_job = { 'class' => 'TestPauseWorker', 'args' => ['hello'] } + job_definition = Gitlab::SidekiqMiddleware::PauseControl::StrategyHandler.new(TestPauseWorker, bare_job.dup) + + expect(Gitlab::SidekiqMiddleware::PauseControl::StrategyHandler) + .to receive(:new).with(TestPauseWorker, hash_including(bare_job)) + .and_return(job_definition).twice + + expect(TestPauseWorker).to receive(:work) + expect(Gitlab::SidekiqMiddleware::PauseControl::PauseControlService).not_to receive(:add_to_waiting_queue!) + + TestPauseWorker.perform_async('hello') + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/pause_control/strategy_handler_spec.rb b/spec/lib/gitlab/sidekiq_middleware/pause_control/strategy_handler_spec.rb new file mode 100644 index 00000000000..da53abec479 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/pause_control/strategy_handler_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::PauseControl::StrategyHandler, :clean_gitlab_redis_queues, feature_category: :global_search do + subject(:pause_control) do + described_class.new(TestPauseWorker, job) + end + + let(:worker_class) do + Class.new do + def self.name + 'TestPauseWorker' + end + + include ApplicationWorker + + pause_control :zoekt + + def perform(*); end + end + end + + let(:job) { { 'class' => 'TestPauseWorker', 'args' => [1], 'jid' => '123' } } + + before do + stub_const('TestPauseWorker', worker_class) + end + + describe '#schedule' do + shared_examples 'scheduling with pause control class' do |strategy_class| + it 'calls schedule on the strategy' do + expect do |block| + klass = "Gitlab::SidekiqMiddleware::PauseControl::Strategies::#{strategy_class}".constantize + expect_next_instance_of(klass) do |strategy| + expect(strategy).to receive(:schedule).with(job, &block) + end + + pause_control.schedule(&block) + end.to yield_control + end + end + + it_behaves_like 'scheduling with pause control class', 'Zoekt' + end + + describe '#perform' do + it 'calls perform on the strategy' do + expect do |block| + expect_next_instance_of(Gitlab::SidekiqMiddleware::PauseControl::Strategies::Zoekt) do |strategy| + expect(strategy).to receive(:perform).with(job, &block) + end + + pause_control.perform(&block) + end.to yield_control + end + + it 'pauses job' do + expect_next_instance_of(Gitlab::SidekiqMiddleware::PauseControl::Strategies::Zoekt) do |strategy| + expect(strategy).to receive(:should_pause?).and_return(true) + end + + expect { pause_control.perform }.to change { + Gitlab::SidekiqMiddleware::PauseControl::PauseControlService.queue_size('TestPauseWorker') + }.by(1) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/pause_control_spec.rb b/spec/lib/gitlab/sidekiq_middleware/pause_control_spec.rb new file mode 100644 index 00000000000..a0cce0f61a0 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/pause_control_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::SidekiqMiddleware::PauseControl, feature_category: :global_search do + describe '.for' do + it 'returns the right class for `zoekt`' do + expect(described_class.for(:zoekt)).to eq(::Gitlab::SidekiqMiddleware::PauseControl::Strategies::Zoekt) + end + + it 'returns the right class for `none`' do + expect(described_class.for(:none)).to eq(::Gitlab::SidekiqMiddleware::PauseControl::Strategies::None) + end + + it 'returns nil when passing an unknown key' do + expect(described_class.for(:unknown)).to eq(::Gitlab::SidekiqMiddleware::PauseControl::Strategies::None) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index bc69f232d9e..0cbf9eab3d8 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -59,30 +59,14 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do described_class.initialize_process_metrics end - shared_examples "initializes sidekiq SLIs for the workers in the current process" do + context 'when emit_sidekiq_histogram FF is disabled' do before do - allow(Gitlab::SidekiqConfig) - .to receive(:current_worker_queue_mappings) - .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default') - allow(completion_seconds_metric).to receive(:get) + stub_feature_flags(emit_sidekiq_histogram_metrics: false) + allow(Gitlab::SidekiqConfig).to receive(:current_worker_queue_mappings).and_return('MergeWorker' => 'merge') end - it "initializes the SLIs with labels" do - expect(Gitlab::Metrics::SidekiqSlis) - .to receive(initialize_sli_method).with([ - { - worker: 'MergeWorker', - urgency: 'high', - feature_category: 'source_code_management', - external_dependencies: 'no' - }, - { - worker: 'Ci::BuildFinishedWorker', - urgency: 'high', - feature_category: 'continuous_integration', - external_dependencies: 'no' - } - ]) + it 'does not initialize sidekiq_jobs_completion_seconds' do + expect(completion_seconds_metric).not_to receive(:get) described_class.initialize_process_metrics end @@ -97,35 +81,38 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do end end - context 'initializing execution SLIs' do - let(:initialize_sli_method) { :initialize_execution_slis! } - - context 'when sidekiq_execution_application_slis FF is turned on' do - it_behaves_like "initializes sidekiq SLIs for the workers in the current process" + context 'initializing execution and queueing SLIs' do + before do + allow(Gitlab::SidekiqConfig) + .to receive(:current_worker_queue_mappings) + .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default') + allow(completion_seconds_metric).to receive(:get) end - context 'when sidekiq_execution_application_slis FF is turned off' do - before do - stub_feature_flags(sidekiq_execution_application_slis: false) - end - - it_behaves_like "not initializing sidekiq SLIs" - end - end + it "initializes the execution and queueing SLIs with labels" do + expected_labels = [ + { + worker: 'MergeWorker', + urgency: 'high', + feature_category: 'source_code_management', + external_dependencies: 'no', + queue: 'merge' + }, + { + worker: 'Ci::BuildFinishedWorker', + urgency: 'high', + feature_category: 'continuous_integration', + external_dependencies: 'no', + queue: 'default' + } + ] - context 'initializing queueing SLIs' do - let(:initialize_sli_method) { :initialize_queueing_slis! } - - context 'when sidekiq_queueing_application_slis FF is turned on' do - it_behaves_like "initializes sidekiq SLIs for the workers in the current process" - end - - context 'when sidekiq_queueing_application_slis FF is turned off' do - before do - stub_feature_flags(sidekiq_queueing_application_slis: false) - end + expect(Gitlab::Metrics::SidekiqSlis) + .to receive(:initialize_execution_slis!).with(expected_labels) + expect(Gitlab::Metrics::SidekiqSlis) + .to receive(:initialize_queueing_slis!).with(expected_labels) - it_behaves_like "not initializing sidekiq SLIs" + described_class.initialize_process_metrics end end @@ -192,20 +179,26 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) expect(sidekiq_mem_total_bytes).to receive(:set).with(labels_with_job_status, mem_total_bytes) - expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_apdex).with(labels.slice(:worker, - :feature_category, - :urgency, - :external_dependencies), monotonic_time_duration) - expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker, - :feature_category, - :urgency, - :external_dependencies), false) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_apdex) + .with(labels.slice(:worker, + :feature_category, + :urgency, + :external_dependencies, + :queue), monotonic_time_duration) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error) + .with(labels.slice(:worker, + :feature_category, + :urgency, + :external_dependencies, + :queue), false) if queue_duration_for_job - expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_queueing_apdex).with(labels.slice(:worker, - :feature_category, - :urgency, - :external_dependencies), queue_duration_for_job) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_queueing_apdex) + .with(labels.slice(:worker, + :feature_category, + :urgency, + :external_dependencies, + :queue), queue_duration_for_job) end subject.call(worker, job, :test) { nil } @@ -260,10 +253,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do it 'records sidekiq SLI error but does not record sidekiq SLI apdex' do expect(failed_total_metric).to receive(:increment) expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_apdex) - expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker, - :feature_category, - :urgency, - :external_dependencies), true) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error) + .with(labels.slice(:worker, + :feature_category, + :urgency, + :external_dependencies, + :queue), true) expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") end @@ -288,31 +283,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do subject.call(worker, job, :test) { nil } end end - - context 'when sidekiq_execution_application_slis FF is turned off' do - before do - stub_feature_flags(sidekiq_execution_application_slis: false) - end - - it 'does not call record_execution_apdex nor record_execution_error' do - expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_apdex) - expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_error) - - subject.call(worker, job, :test) { nil } - end - end - - context 'when sidekiq_queueing_application_slis FF is turned off' do - before do - stub_feature_flags(sidekiq_queueing_application_slis: false) - end - - it 'does not call record_queueing_apdex' do - expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_queueing_apdex) - - subject.call(worker, job, :test) { nil } - end - end end end @@ -484,5 +454,53 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do end end end + + context 'when emit_sidekiq_histogram_metrics FF is disabled' do + include_context 'server metrics with mocked prometheus' + include_context 'server metrics call' do + let(:stub_subject) { false } + end + + subject(:middleware) { described_class.new } + + let(:job) { {} } + let(:queue) { :test } + let(:worker_class) do + Class.new do + def self.name + "TestWorker" + end + include ApplicationWorker + end + end + + let(:worker) { worker_class.new } + let(:labels) do + { queue: queue.to_s, + worker: worker.class.name, + boundary: "", + external_dependencies: "no", + feature_category: "", + urgency: "low" } + end + + before do + stub_feature_flags(emit_sidekiq_histogram_metrics: false) + end + + it 'does not emit histogram metrics' do + expect(completion_seconds_metric).not_to receive(:observe) + expect(queue_duration_seconds).not_to receive(:observe) + expect(failed_total_metric).not_to receive(:increment) + + middleware.call(worker, job, queue) { nil } + end + + it 'emits sidekiq_jobs_completion_seconds_sum metric' do + expect(completion_seconds_sum_metric).to receive(:increment).with(labels, monotonic_time_duration) + + middleware.call(worker, job, queue) { nil } + end + end end # rubocop: enable RSpec/MultipleMemoizedHelpers diff --git a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb index 4be21591a40..620de7e7671 100644 --- a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb @@ -115,7 +115,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalabili end context 'with worker opted for database health check' do - let(:health_signal_attrs) { { gitlab_schema: :gitlab_main, delay: 1.minute, tables: [:users] } } + let(:health_signal_attrs) { { gitlab_schema: :gitlab_main, tables: [:users], delay: 1.minute } } around do |example| with_sidekiq_server_middleware do |chain| diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb index 4203a76cbfb..aa755d64a7a 100644 --- a/spec/lib/gitlab/time_tracking_formatter_spec.rb +++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::TimeTrackingFormatter do +RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning do describe '#parse' do let(:keep_zero) { false } diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index e1ae362e797..c44cfdea1cd 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' RSpec.describe Gitlab::Tracking::StandardContext do - let_it_be(:project) { create(:project) } - let_it_be(:namespace) { create(:namespace) } - let_it_be(:user) { create(:user) } - let(:snowplow_context) { subject.to_context } describe '#to_context' do @@ -62,21 +58,27 @@ RSpec.describe Gitlab::Tracking::StandardContext do expect(snowplow_context.to_json.dig(:data, :context_generated_at)).to eq(Time.current) end - context 'plan' do - context 'when namespace is not available' do - it 'is nil' do - expect(snowplow_context.to_json.dig(:data, :plan)).to be_nil - end - end + it 'contains standard properties' do + standard_properties = [:user_id, :project_id, :namespace_id, :plan] + expect(snowplow_context.to_json[:data].keys).to include(*standard_properties) + end - context 'when namespace is available' do - let(:namespace) { create(:namespace) } + context 'with standard properties' do + let(:user_id) { 1 } + let(:project_id) { 2 } + let(:namespace_id) { 3 } + let(:plan_name) { "plan name" } - subject { described_class.new(namespace_id: namespace.id, plan_name: namespace.actual_plan_name) } + subject do + described_class.new(user_id: user_id, project_id: project_id, namespace_id: namespace_id, plan_name: plan_name) + end - it 'contains plan name' do - expect(snowplow_context.to_json.dig(:data, :plan)).to eq(Plan::DEFAULT) - end + it 'holds the correct values', :aggregate_failures do + json_data = snowplow_context.to_json.fetch(:data) + expect(json_data[:user_id]).to eq(user_id) + expect(json_data[:project_id]).to eq(project_id) + expect(json_data[:namespace_id]).to eq(namespace_id) + expect(json_data[:plan]).to eq(plan_name) end end @@ -95,24 +97,12 @@ RSpec.describe Gitlab::Tracking::StandardContext do end context 'with incorrect argument type' do - subject { described_class.new(project_id: create(:group)) } + subject { described_class.new(project_id: "a string") } it 'does call `track_and_raise_for_dev_exception`' do expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) snowplow_context end end - - it 'contains user id' do - expect(snowplow_context.to_json[:data].keys).to include(:user_id) - end - - it 'contains namespace and project ids' do - expect(snowplow_context.to_json[:data].keys).to include(:project_id, :namespace_id) - end - - it 'accepts just project id as integer' do - expect { described_class.new(project: 1).to_context }.not_to raise_error - end end end diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index d67bb477350..859f3f7a8d7 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -18,7 +18,6 @@ RSpec.describe Gitlab::Usage::MetricDefinition do data_source: 'database', distribution: %w(ee ce), tier: %w(free starter premium ultimate bronze silver gold), - name: 'uuid', data_category: 'standard', removed_by_url: 'http://gdk.test' } @@ -129,7 +128,6 @@ RSpec.describe Gitlab::Usage::MetricDefinition do :distribution | nil :distribution | 'test' :tier | %w(test ee) - :name | 'count_<adjective_describing>_boards' :repair_issue_url | nil :removed_by_url | 1 diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index d0ea4e7aa16..a4135b143dd 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -45,12 +45,6 @@ RSpec.describe Gitlab::Usage::Metric do end end - describe '#with_suggested_name' do - it 'returns key_path metric with the corresponding generated query' do - expect(described_class.new(issue_count_metric_definiton).with_suggested_name).to eq({ counts: { issues: 'count_issues' } }) - end - end - context 'unavailable metric' do let(:instrumentation_class) { "UnavailableMetric" } let(:issue_count_metric_definiton) do @@ -69,7 +63,7 @@ RSpec.describe Gitlab::Usage::Metric do stub_const("Gitlab::Usage::Metrics::Instrumentations::#{instrumentation_class}", unavailable_metric_class) end - [:with_value, :with_instrumentation, :with_suggested_name].each do |method_name| + [:with_value, :with_instrumentation].each do |method_name| describe "##{method_name}" do it 'returns an empty hash' do expect(described_class.new(issue_count_metric_definiton).public_send(method_name)).to eq({}) diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb new file mode 100644 index 00000000000..e66dd04b69b --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::BatchedBackgroundMigrationFailedJobsMetric, feature_category: :database do + let(:expected_value) do + [ + { + job_class_name: 'job', + number_of_failed_jobs: 1, + table_name: 'jobs' + }, + { + job_class_name: 'test', + number_of_failed_jobs: 2, + table_name: 'users' + } + ] + end + + let_it_be(:active_migration) do + create(:batched_background_migration, :active, table_name: 'users', job_class_name: 'test', created_at: 5.days.ago) + end + + let_it_be(:failed_migration) do + create(:batched_background_migration, :failed, table_name: 'jobs', job_class_name: 'job', created_at: 4.days.ago) + end + + let_it_be(:batched_job) { create(:batched_background_migration_job, :failed, batched_migration: active_migration) } + + let_it_be(:batched_job_2) { create(:batched_background_migration_job, :failed, batched_migration: active_migration) } + + let_it_be(:batched_job_3) { create(:batched_background_migration_job, :failed, batched_migration: failed_migration) } + + let_it_be(:old_migration) { create(:batched_background_migration, :failed, created_at: 99.days.ago) } + + let_it_be(:old_batched_job) { create(:batched_background_migration_job, :failed, batched_migration: old_migration) } + + it_behaves_like 'a correct instrumented metric value', { time_frame: '7d' } +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb index eee5396bdbf..0deb586d488 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb @@ -165,7 +165,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie end context 'with has_failures: true' do - before(:all) do + before_all do create_list(:bulk_import_entity, 3, :project_entity, :finished, created_at: 3.weeks.ago, has_failures: true) create_list(:bulk_import_entity, 2, :project_entity, :finished, created_at: 2.months.ago, has_failures: true) create_list(:bulk_import_entity, 3, :group_entity, :finished, created_at: 3.weeks.ago, has_failures: true) diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric_spec.rb index 538be7bbdc4..7fd5b135a4a 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_deployments_metric_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountDeploymentsMetric, feature_category: :service_ping do using RSpec::Parameterized::TableSyntax - before(:all) do + before_all do env = create(:environment) [3, 60].each do |n| deployment_options = { created_at: n.days.ago, project: env.project, environment: env } diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb deleted file mode 100644 index 35e5d7f2796..00000000000 --- a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggregatedMetric do - let(:metric_definition) do - { - data_source: 'redis_hll', - time_frame: time_frame, - options: { - aggregate: { - operator: 'OR' - }, - events: %w[ - users_creating_work_items - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels - users_updating_work_item_milestone - users_updating_work_item_iteration - ] - } - } - end - - around do |example| - freeze_time { example.run } - end - - where(:time_frame) { [['28d'], ['7d']] } - - with_them do - describe '#available?' do - it 'returns false without track_work_items_activity feature' do - stub_feature_flags(track_work_items_activity: false) - - expect(described_class.new(metric_definition).available?).to eq(false) - end - - it 'returns true with track_work_items_activity feature' do - stub_feature_flags(track_work_items_activity: true) - - expect(described_class.new(metric_definition).available?).to eq(true) - end - end - - describe '#value', :clean_gitlab_redis_shared_state do - let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter } - let(:author1_id) { 1 } - let(:author2_id) { 2 } - let(:event_time) { 1.week.ago } - - before do - counter.track_event(:users_creating_work_items, values: author1_id, time: event_time) - end - - it 'has correct value after events are tracked', :aggregate_failures do - expect do - counter.track_event(:users_updating_work_item_title, values: author1_id, time: event_time) - counter.track_event(:users_updating_work_item_dates, values: author1_id, time: event_time) - counter.track_event(:users_updating_work_item_labels, values: author1_id, time: event_time) - counter.track_event(:users_updating_work_item_milestone, values: author1_id, time: event_time) - end.to not_change { described_class.new(metric_definition).value } - - expect do - counter.track_event(:users_updating_work_item_iteration, values: author2_id, time: event_time) - counter.track_event(:users_updating_weight_estimate, values: author1_id, time: event_time) - end.to change { described_class.new(metric_definition).value }.from(1).to(2) - end - end - end -end diff --git a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb deleted file mode 100644 index 9dba64ff59f..00000000000 --- a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do - describe '#for' do - shared_examples 'name suggestion' do - it 'return correct name' do - expect(described_class.for(operation, relation: relation, column: column)).to match name_suggestion - end - end - - context 'for count with nil column' do - it_behaves_like 'name suggestion' do - let(:operation) { :count } - let(:relation) { Board } - let(:column) { nil } - let(:name_suggestion) { /count_boards/ } - end - end - - context 'for count with column :id' do - it_behaves_like 'name suggestion' do - let(:operation) { :count } - let(:relation) { Board } - let(:column) { :id } - let(:name_suggestion) { /count_boards/ } - end - end - - context 'for count distinct with column defined metrics' do - it_behaves_like 'name suggestion' do - let(:operation) { :distinct_count } - let(:relation) { ZoomMeeting } - let(:column) { :issue_id } - let(:name_suggestion) { /count_distinct_issue_id_from_zoom_meetings/ } - end - end - - context 'joined relations' do - context 'counted attribute comes from source relation' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id) - let(:operation) { :count } - let(:relation) { Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot) } - let(:column) { nil } - let(:name_suggestion) { /count_<adjective describing: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ } - end - end - end - - context 'strips off time period constraint' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with distinct_count(::Clusters::Cluster.aws_installed.enabled.where(time_period), :user_id) - let(:operation) { :distinct_count } - let(:relation) { ::Clusters::Cluster.aws_installed.enabled.where(created_at: 30.days.ago..2.days.ago ) } - let(:column) { :user_id } - let(:constraints) { /<adjective describing: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ } - let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ } - end - end - - context 'for sum metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count) - let(:operation) { :sum } - let(:relation) { JiraImportState.finished } - let(:column) { :imported_issues_count } - let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing: '\(jira_imports\.status = \d+\)'>_jira_imports/ } - end - end - - context 'for average metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with average(Ci::Pipeline, :duration) - let(:operation) { :average } - let(:relation) { Ci::Pipeline } - let(:column) { :duration } - let(:name_suggestion) { /average_duration_from_ci_pipelines/ } - end - end - - context 'for redis metrics' do - it_behaves_like 'name suggestion' do - let(:operation) { :redis } - let(:column) { nil } - let(:relation) { nil } - let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ } - end - end - - context 'for alt_usage_data metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with alt_usage_data(fallback: nil) { operating_system } - let(:operation) { :alt } - let(:column) { nil } - let(:relation) { nil } - let(:name_suggestion) { /<please fill metric name>/ } - end - end - - context 'for metrics with `having` keyword' do - it_behaves_like 'name suggestion' do - let(:operation) { :count } - let(:relation) { Issue.with_alert_management_alerts.having('COUNT(alert_management_alerts) > 1').group(:id) } - - let(:column) { nil } - let(:constraints) { /<adjective describing: '\(\(COUNT\(alert_management_alerts\) > 1\)\)'>/ } - let(:name_suggestion) { /count_#{constraints}_issues_<with>_alert_management_alerts/ } - end - end - end -end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb deleted file mode 100644 index 884d73a70f3..00000000000 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator, feature_category: :service_ping do - include UsageDataHelpers - - before do - stub_usage_data_connections - end - - describe '#generate' do - shared_examples 'name suggestion' do - it 'return correct name' do - expect(described_class.generate(key_path)).to match name_suggestion - end - end - - describe '#add_metric' do - let(:metric) { 'CountIssuesMetric' } - - it 'computes the suggested name for given metric' do - expect(described_class.add_metric(metric)).to eq('count_issues') - end - end - - context 'for count with default column metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with count(Board) - let(:key_path) { 'counts.issues' } - let(:name_suggestion) { /count_issues/ } - end - end - - context 'for count distinct with column defined metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with distinct_count(ZoomMeeting, :issue_id) - let(:key_path) { 'counts.issues_using_zoom_quick_actions' } - let(:name_suggestion) { /count_distinct_issue_id_from_zoom_meetings/ } - end - end - - context 'joined relations' do - context 'counted attribute comes from source relation' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with distinct_count(Release.with_milestones, :author_id) - let(:key_path) { 'usage_activity_by_stage.release.releases_with_milestones' } - let(:name_suggestion) { /count_distinct_author_id_from_releases_<with>_milestone_releases/ } - end - end - end - - context 'strips off time period constraint' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with distinct_count(::Clusters::Cluster.aws_installed.enabled.where(time_period), :user_id) - let(:key_path) { 'usage_activity_by_stage_monthly.configure.clusters_platforms_eks' } - let(:constraints) { /<adjective describing: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ } - let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ } - end - end - - context 'for sum metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count) - let(:key_path) { 'counts.jira_imports_total_imported_issues_count' } - let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing: '\(jira_imports\.status = \d+\)'>_jira_imports/ } - end - end - - context 'for add metrics' do - before do - pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/414887' - end - - it_behaves_like 'name suggestion' do - # corresponding metric is collected with add(data[:personal_snippets], data[:project_snippets]) - let(:key_path) { 'counts.snippets' } - let(:name_suggestion) { /add_count_<adjective describing: '\(snippets\.type = 'PersonalSnippet'\)'>_snippets_and_count_<adjective describing: '\(snippets\.type = 'ProjectSnippet'\)'>_snippets/ } - end - end - - context 'for redis metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/399421' do - it_behaves_like 'name suggestion' do - let(:key_path) { 'usage_activity_by_stage_monthly.create.merge_requests_users' } - let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ } - end - end - - context 'for alt_usage_data metrics' do - it_behaves_like 'name suggestion' do - # corresponding metric is collected with alt_usage_data { ApplicationRecord.database.version } - let(:key_path) { 'database.version' } - let(:name_suggestion) { /<please fill metric name>/ } - end - end - end -end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb deleted file mode 100644 index 492acf2a902..00000000000 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::HavingConstraints do - describe '#accept' do - let(:connection) { ApplicationRecord.connection } - let(:collector) { Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) } - - it 'builds correct constraints description' do - table = Arel::Table.new('records') - havings = table[:attribute].sum.eq(6).and(table[:attribute].count.gt(5)) - arel = table.from.project(table['id'].count).having(havings).group(table[:attribute2]) - described_class.new(connection).accept(arel, collector) - - expect(collector.value).to eql '(SUM(records.attribute) = 6 AND COUNT(records.attribute) > 5)' - end - end -end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb deleted file mode 100644 index 3e72d118ac6..00000000000 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins do - describe '#accept' do - let(:collector) do - Arel::Collectors::SubstituteBinds.new(ApplicationRecord.connection, Arel::Collectors::SQLString.new) - end - - context 'with join added via string' do - it 'collects join parts' do - arel = Issue.joins('LEFT JOIN projects ON projects.id = issue.project_id') - - arel = arel.arel - result = described_class.new(ApplicationRecord.connection).accept(arel) - - expect(result).to match_array [{ source: "projects", constraints: "projects.id = issue.project_id" }] - end - end - - context 'with join added via arel node' do - it 'collects join parts' do - source_table = Arel::Table.new('records') - joined_table = Arel::Table.new('joins') - second_level_joined_table = Arel::Table.new('second_level_joins') - - arel = source_table - .from - .project(source_table['id'].count) - .join(joined_table, Arel::Nodes::OuterJoin) - .on(source_table[:id].eq(joined_table[:records_id])) - .join(second_level_joined_table, Arel::Nodes::OuterJoin) - .on(joined_table[:id].eq(second_level_joined_table[:joins_id])) - - result = described_class.new(ApplicationRecord.connection).accept(arel) - - expect(result).to match_array [ - { source: "joins", constraints: "records.id = joins.records_id" }, - { source: "second_level_joins", constraints: "joins.id = second_level_joins.joins_id" } - ] - end - end - end -end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb deleted file mode 100644 index 42a776478a4..00000000000 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::WhereConstraints do - describe '#accept' do - let(:connection) { ApplicationRecord.connection } - let(:collector) { Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) } - - it 'builds correct constraints description' do - table = Arel::Table.new('records') - arel = table.from.project(table['id'].count).where(table[:attribute].eq(true).and(table[:some_value].gt(5))) - described_class.new(connection).accept(arel, collector) - - expect(collector.value).to eql '(records.attribute = true AND records.some_value > 5)' - end - end -end diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb index 19236cdbba0..ab92b59c845 100644 --- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb @@ -3,41 +3,31 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_redis_shared_state do - let(:user1) { build(:user, id: 1) } + let(:user) { build(:user, id: 1) } let(:user2) { build(:user, id: 2) } let(:user3) { build(:user, id: 3) } let(:project) { build(:project) } + let(:namespace) { project.namespace } let(:time) { Time.zone.now } shared_examples 'tracks and counts action' do + subject { track_action(author: user, project: project) } + before do stub_application_setting(usage_ping_enabled: true) end specify do aggregate_failures do - expect(track_action(author: user1, project: project)).to be_truthy + expect(track_action(author: user, project: project)).to be_truthy expect(track_action(author: user2, project: project)).to be_truthy - expect(track_action(author: user3, time: time.end_of_week - 3.days, project: project)).to be_truthy + expect(track_action(author: user3, project: project)).to be_truthy expect(count_unique(date_from: time.beginning_of_week, date_to: 1.week.from_now)).to eq(3) end end - it 'track snowplow event' do - track_action(author: user1, project: project) - - expect_snowplow_event( - category: described_class.name, - action: 'ide_edit', - label: 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit', - namespace: project.namespace, - property: event_name, - project: project, - user: user1, - context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_h] - ) - end + it_behaves_like 'internal event tracking' it 'does not track edit actions if author is not present' do expect(track_action(author: nil, project: project)).to be_nil @@ -45,7 +35,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for web IDE edit actions' do - let(:event_name) { described_class::EDIT_BY_WEB_IDE } + let(:action) { described_class::EDIT_BY_WEB_IDE } it_behaves_like 'tracks and counts action' do def track_action(params) @@ -59,7 +49,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for SFE edit actions' do - let(:event_name) { described_class::EDIT_BY_SFE } + let(:action) { described_class::EDIT_BY_SFE } it_behaves_like 'tracks and counts action' do def track_action(params) @@ -73,7 +63,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end context 'for snippet editor edit actions' do - let(:event_name) { described_class::EDIT_BY_SNIPPET_EDITOR } + let(:action) { described_class::EDIT_BY_SNIPPET_EDITOR } it_behaves_like 'tracks and counts action' do def track_action(params) 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 fc1d66d1d62..7bef14d5f7a 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 @@ -8,9 +8,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' } let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' } - let(:default_context) { 'default' } - let(:invalid_context) { 'invalid' } - around do |example| # We need to freeze to a reference time # because visits are grouped by the week number in the year @@ -27,62 +24,39 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.known_events' do let(:ce_event) { { "name" => "ce_event" } } - - context 'with use_metric_definitions_for_events_list disabled' do - let(:ce_temp_dir) { Dir.mktmpdir } - let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) } - - before do - stub_feature_flags(use_metric_definitions_for_events_list: false) - stub_const("#{described_class}::KNOWN_EVENTS_PATH", File.expand_path('*.yml', ce_temp_dir)) - File.open(ce_temp_file.path, "w+b") { |f| f.write [ce_event].to_yaml } - end - - after do - ce_temp_file.unlink - FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir) - end - - it 'returns ce events' do - expect(described_class.known_events).to include(ce_event) - end + let(:removed_ce_event) { { "name" => "removed_ce_event" } } + let(:metric_definition) do + Gitlab::Usage::MetricDefinition.new('ce_metric', + { + key_path: 'ce_metric_weekly', + status: 'active', + options: { + events: [ce_event['name']] + } + }) end - context 'with use_metric_definitions_for_events_list enabled' do - let(:removed_ce_event) { { "name" => "removed_ce_event" } } - let(:metric_definition) do - Gitlab::Usage::MetricDefinition.new('ce_metric', - { - key_path: 'ce_metric_weekly', - status: 'active', - options: { - events: [ce_event['name']] - } - }) - end - - let(:removed_metric_definition) do - Gitlab::Usage::MetricDefinition.new('removed_ce_metric', - { - key_path: 'removed_ce_metric_weekly', - status: 'removed', - options: { - events: [removed_ce_event['name']] - } - }) - end + let(:removed_metric_definition) do + Gitlab::Usage::MetricDefinition.new('removed_ce_metric', + { + key_path: 'removed_ce_metric_weekly', + status: 'removed', + options: { + events: [removed_ce_event['name']] + } + }) + end - before do - allow(Gitlab::Usage::MetricDefinition).to receive(:all).and_return([metric_definition, removed_metric_definition]) - end + before do + allow(Gitlab::Usage::MetricDefinition).to receive(:all).and_return([metric_definition, removed_metric_definition]) + end - it 'returns ce events' do - expect(described_class.known_events).to include(ce_event) - end + it 'returns ce events' do + expect(described_class.known_events).to include(ce_event) + end - it 'does not return removed events' do - expect(described_class.known_events).not_to include(removed_ce_event) - end + it 'does not return removed events' do + expect(described_class.known_events).not_to include(removed_ce_event) end end @@ -96,7 +70,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s let(:no_slot) { 'no_slot' } let(:different_aggregation) { 'different_aggregation' } let(:custom_daily_event) { 'g_analytics_custom' } - let(:context_event) { 'context_event' } let(:global_category) { 'global' } let(:compliance_category) { 'compliance' } @@ -111,8 +84,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s { name: category_productivity_event }, { name: compliance_slot_event }, { name: no_slot }, - { name: different_aggregation }, - { name: context_event } + { name: different_aggregation } ].map(&:with_indifferent_access) end @@ -214,43 +186,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end - describe '.track_event_in_context' do - context 'with valid contex' do - it 'increments context event counter' do - expect(Gitlab::Redis::HLL).to receive(:add) do |kwargs| - expect(kwargs[:key]).to match(/^#{default_context}_.*/) - end - - described_class.track_event_in_context(context_event, values: entity1, context: default_context) - end - - it 'tracks events with multiple values' do - values = [entity1, entity2] - expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, - value: values, - expiry: described_class::KEY_EXPIRY_LENGTH) - - described_class.track_event_in_context(:g_analytics_contribution, values: values, context: default_context) - end - end - - context 'with empty context' do - it 'does not increment a counter' do - expect(Gitlab::Redis::HLL).not_to receive(:add) - - described_class.track_event_in_context(context_event, values: entity1, context: '') - end - end - - context 'when sending invalid context' do - it 'does not increment a counter' do - expect(Gitlab::Redis::HLL).not_to receive(:add) - - described_class.track_event_in_context(context_event, values: entity1, context: invalid_context) - end - end - end - describe '.unique_events' do before do # events in current week, should not be counted as week is not complete @@ -360,48 +295,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end - describe 'context level tracking' do - using RSpec::Parameterized::TableSyntax - - let(:known_events) do - [ - { name: 'event_name_1' }, - { name: 'event_name_2' }, - { name: 'event_name_3' } - ].map(&:with_indifferent_access) - end - - before do - allow(described_class).to receive(:known_events).and_return(known_events) - allow(described_class).to receive(:categories).and_return(%w(category1 category2)) - - described_class.track_event_in_context('event_name_1', values: [entity1, entity3], context: default_context, time: 2.days.ago) - described_class.track_event_in_context('event_name_1', values: entity3, context: default_context, time: 2.days.ago) - described_class.track_event_in_context('event_name_1', values: entity3, context: invalid_context, time: 2.days.ago) - described_class.track_event_in_context('event_name_2', values: [entity1, entity2], context: '', time: 2.weeks.ago) - end - - subject(:unique_events) { described_class.unique_events(event_names: event_names, start_date: 4.weeks.ago, end_date: Date.current, context: context) } - - context 'with correct arguments' do - where(:event_names, :context, :value) do - ['event_name_1'] | 'default' | 2 - ['event_name_1'] | '' | 0 - ['event_name_2'] | '' | 0 - end - - with_them do - it { is_expected.to eq value } - end - end - - context 'with invalid context' do - it 'raise error' do - expect { described_class.unique_events(event_names: 'event_name_1', start_date: 4.weeks.ago, end_date: Date.current, context: invalid_context) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::InvalidContext) - end - end - end - describe '.calculate_events_union' do let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } } let(:known_events) do diff --git a/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb new file mode 100644 index 00000000000..274a3ffc843 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter, :clean_gitlab_redis_shared_state, feature_category: :editor_extensions do + let(:user1) { build(:user, id: 1) } + let(:user2) { build(:user, id: 2) } + let(:time) { Time.current } + let(:action) { described_class::NEOVIM_PLUGIN_API_REQUEST_ACTION } + let(:user_agent_string) do + 'code-completions-language-server-experiment (Neovim:0.9.0; gitlab.vim (v0.1.0); arch:amd64; os:darwin)' + end + + let(:user_agent) { { user_agent: user_agent_string } } + + context 'when tracking a neovim plugin api request' do + it_behaves_like 'a request from an extension' + end +end diff --git a/spec/lib/gitlab/usage_data_counters/visual_studio_extension_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/visual_studio_extension_activity_unique_counter_spec.rb new file mode 100644 index 00000000000..57cf173f793 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/visual_studio_extension_activity_unique_counter_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::VisualStudioExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state, feature_category: :editor_extensions do + let(:user1) { build(:user, id: 1) } + let(:user2) { build(:user, id: 2) } + let(:time) { Time.current } + let(:action) { described_class::VISUAL_STUDIO_EXTENSION_API_REQUEST_ACTION } + let(:user_agent_string) do + 'code-completions-language-server-experiment (gl-visual-studio-extension:1.0.0.0; arch:X64;)' + end + + let(:user_agent) { { user_agent: user_agent_string } } + + context 'when tracking a visual studio api request' do + it_behaves_like 'a request from an extension' + end +end diff --git a/spec/lib/gitlab/with_request_store_spec.rb b/spec/lib/gitlab/with_request_store_spec.rb deleted file mode 100644 index 353ad02fbd8..00000000000 --- a/spec/lib/gitlab/with_request_store_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'request_store' - -RSpec.describe Gitlab::WithRequestStore do - let(:fake_class) { Class.new { include Gitlab::WithRequestStore } } - - subject(:object) { fake_class.new } - - describe "#with_request_store" do - it 'starts a request store and yields control' do - expect(RequestStore).to receive(:begin!).ordered - expect(RequestStore).to receive(:end!).ordered - expect(RequestStore).to receive(:clear!).ordered - - expect { |b| object.with_request_store(&b) }.to yield_control - end - - it 'only starts a request store once when nested' do - expect(RequestStore).to receive(:begin!).ordered.once.and_call_original - expect(RequestStore).to receive(:end!).ordered.once.and_call_original - expect(RequestStore).to receive(:clear!).ordered.once.and_call_original - - object.with_request_store do - expect { |b| object.with_request_store(&b) }.to yield_control - end - end - end -end diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index d119a4e2b9d..e0823aa8153 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Gitlab::X509::Signature do it 'returns a verified signature if email does match' do expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) expect(signature.verified_signature).to be_truthy expect(signature.verification_status).to eq(:verified) @@ -55,6 +56,27 @@ RSpec.describe Gitlab::X509::Signature do expect(signature.verification_status).to eq(:verified) end + context 'when the certificate contains multiple emails' do + before do + allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(described_class).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("email:gitlab2@example.com, othername:<unsupported>, email:#{X509Helpers::User1.certificate_email}") + end + + context 'and the email matches one of them' do + it 'returns a verified signature' do + expect(signature.x509_certificate).to have_attributes(certificate_attributes.except(:email, :emails)) + expect(signature.x509_certificate.email).to eq('gitlab2@example.com') + expect(signature.x509_certificate.emails).to contain_exactly('gitlab2@example.com', X509Helpers::User1.certificate_email) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_truthy + expect(signature.verification_status).to eq(:verified) + end + end + end + context "if the email matches but isn't confirmed" do let!(:user) { create(:user, :unconfirmed, email: X509Helpers::User1.certificate_email) } @@ -106,6 +128,7 @@ RSpec.describe Gitlab::X509::Signature do subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier, subject: X509Helpers::User1.certificate_subject, email: X509Helpers::User1.certificate_email, + emails: [X509Helpers::User1.certificate_email], serial_number: X509Helpers::User1.certificate_serial } end @@ -248,15 +271,31 @@ RSpec.describe Gitlab::X509::Signature do .and_return("email:gitlab@example.com, othername:<unsupported>") end - it 'extracts email' do - signature = described_class.new( + let(:signature) do + described_class.new( X509Helpers::User1.signed_commit_signature, X509Helpers::User1.signed_commit_base_data, 'gitlab@example.com', X509Helpers::User1.signed_commit_time ) + end + it 'extracts email' do expect(signature.x509_certificate.email).to eq("gitlab@example.com") + expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com") + end + + context 'when there are multiple emails' do + before do + allow_any_instance_of(described_class).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("email:gitlab@example.com, othername:<unsupported>, email:gitlab2@example.com") + end + + it 'extracts all the emails' do + expect(signature.x509_certificate.email).to eq("gitlab@example.com") + expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com", "gitlab2@example.com") + end end end @@ -311,6 +350,7 @@ RSpec.describe Gitlab::X509::Signature do subject_key_identifier: X509Helpers::User1.tag_certificate_subject_key_identifier, subject: X509Helpers::User1.certificate_subject, email: X509Helpers::User1.certificate_email, + emails: [X509Helpers::User1.certificate_email], serial_number: X509Helpers::User1.tag_certificate_serial } end |