diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/lib/gitlab | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/lib/gitlab')
178 files changed, 6731 insertions, 2020 deletions
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 ec394bb9f05..34d5158a5ab 100644 --- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb +++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do describe '#serialized_records' do shared_context 'when records are loaded by maintainer' do before do - project.add_user(user, Gitlab::Access::DEVELOPER) + project.add_member(user, Gitlab::Access::DEVELOPER) end it 'returns all records' do @@ -72,7 +72,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do context 'when records are loaded by guest' do before do - project.add_user(user, Gitlab::Access::GUEST) + project.add_member(user, Gitlab::Access::GUEST) end it 'filters out confidential issues' do @@ -124,7 +124,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do end before do - project.add_user(user, Gitlab::Access::DEVELOPER) + project.add_member(user, Gitlab::Access::DEVELOPER) stub_const('Gitlab::Analytics::CycleAnalytics::RecordsFetcher::MAX_RECORDS', 2) end diff --git a/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb new file mode 100644 index 00000000000..b34ac538b24 --- /dev/null +++ b/spec/lib/gitlab/application_rate_limiter/base_strategy_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ApplicationRateLimiter::BaseStrategy do + describe '#increment' do + it 'raises NotImplementedError' do + expect { subject.increment('cache_key', 0) }.to raise_error(NotImplementedError) + end + end + + describe '#read' do + it 'raises NotImplementedError' do + expect { subject.read('cache_key') }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/application_rate_limiter/increment_per_action_spec.rb b/spec/lib/gitlab/application_rate_limiter/increment_per_action_spec.rb new file mode 100644 index 00000000000..b74d2360711 --- /dev/null +++ b/spec/lib/gitlab/application_rate_limiter/increment_per_action_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ApplicationRateLimiter::IncrementPerAction, :freeze_time, :clean_gitlab_redis_rate_limiting do + let(:cache_key) { 'test' } + let(:expiry) { 60 } + + subject(:counter) { described_class.new } + + def increment + counter.increment(cache_key, expiry) + end + + describe '#increment' do + it 'increments per call' do + expect(increment).to eq 1 + expect(increment).to eq 2 + expect(increment).to eq 3 + end + + it 'sets time to live (TTL) for the key' do + def ttl + Gitlab::Redis::RateLimiting.with { |r| r.ttl(cache_key) } + end + + key_does_not_exist = -2 + + expect(ttl).to eq key_does_not_exist + expect { increment }.to change { ttl }.by(a_value > 0) + end + end + + describe '#read' do + def read + counter.read(cache_key) + end + + it 'returns 0 when there is no data' do + expect(read).to eq 0 + end + + it 'returns the correct value', :aggregate_failures do + increment + expect(read).to eq 1 + + increment + expect(read).to eq 2 + end + end +end diff --git a/spec/lib/gitlab/application_rate_limiter/increment_per_actioned_resource_spec.rb b/spec/lib/gitlab/application_rate_limiter/increment_per_actioned_resource_spec.rb new file mode 100644 index 00000000000..1f3ae2d749a --- /dev/null +++ b/spec/lib/gitlab/application_rate_limiter/increment_per_actioned_resource_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ApplicationRateLimiter::IncrementPerActionedResource, + :freeze_time, :clean_gitlab_redis_rate_limiting do + let(:cache_key) { 'test' } + let(:expiry) { 60 } + + def increment(resource_key) + described_class.new(resource_key).increment(cache_key, expiry) + end + + describe '#increment' do + it 'increments per resource', :aggregate_failures do + expect(increment('resource_1')).to eq(1) + expect(increment('resource_1')).to eq(1) + expect(increment('resource_2')).to eq(2) + expect(increment('resource_2')).to eq(2) + expect(increment('resource_3')).to eq(3) + end + + it 'sets time to live (TTL) for the key' do + def ttl + Gitlab::Redis::RateLimiting.with { |r| r.ttl(cache_key) } + end + + key_does_not_exist = -2 + + expect(ttl).to eq key_does_not_exist + expect { increment('resource_1') }.to change { ttl }.by(a_value > 0) + end + end + + describe '#read' do + def read + described_class.new(nil).read(cache_key) + end + + it 'returns 0 when there is no data' do + expect(read).to eq 0 + end + + it 'returns the correct value', :aggregate_failures do + increment 'r1' + expect(read).to eq 1 + + increment 'r2' + expect(read).to eq 2 + end + end +end diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb index efe78cd3a35..177ce1134d8 100644 --- a/spec/lib/gitlab/application_rate_limiter_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter_spec.rb @@ -13,8 +13,8 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting interval: 2.minutes }, another_action: { - threshold: 2, - interval: 3.minutes + threshold: -> { 2 }, + interval: -> { 3.minutes } } } end @@ -70,6 +70,44 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting end end + describe 'counting actions once per unique resource' do + let(:scope) { [user, project] } + + let(:start_time) { Time.current.beginning_of_hour } + let(:project1) { instance_double(Project, id: '1') } + let(:project2) { instance_double(Project, id: '2') } + + it 'returns true when unique actioned resources count exceeds threshold' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope, resource: project1)).to eq(false) + end + + travel_to(start_time + 1.minute) do + expect(subject.throttled?(:test_action, scope: scope, resource: project2)).to eq(true) + end + end + + it 'returns false when unique actioned resource count does not exceed threshold' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope, resource: project1)).to eq(false) + end + + travel_to(start_time + 1.minute) do + expect(subject.throttled?(:test_action, scope: scope, resource: project1)).to eq(false) + end + end + + it 'returns false when interval has elapsed' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope, resource: project1)).to eq(false) + end + + travel_to(start_time + 2.minutes) do + expect(subject.throttled?(:test_action, scope: scope, resource: project2)).to eq(false) + end + end + end + shared_examples 'throttles based on key and scope' do let(:start_time) { Time.current.beginning_of_hour } @@ -91,7 +129,7 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting travel_to(start_time) do expect(subject.throttled?(:test_action, scope: scope)).to eq(false) - # another_action has a threshold of 3 so we simulate 2 requests + # another_action has a threshold of 2 so we simulate 2 requests expect(subject.throttled?(:another_action, scope: scope)).to eq(false) expect(subject.throttled?(:another_action, scope: scope)).to eq(false) end @@ -189,4 +227,20 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting end end end + + context 'when interval is 0' do + let(:rate_limits) { { test_action: { threshold: 1, interval: 0 } } } + let(:scope) { user } + let(:start_time) { Time.current.beginning_of_hour } + + it 'returns false' do + travel_to(start_time) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + end + + travel_to(start_time + 1.minute) do + expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index da0bb5fe675..b471a89b491 100644 --- a/spec/lib/gitlab/auth/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -49,6 +49,24 @@ RSpec.describe Gitlab::Auth::Ldap::User do end end + describe '#valid_sign_in?' do + before do + gl_user.save! + end + + it 'returns true' do + expect(Gitlab::Auth::Ldap::Access).to receive(:allowed?).and_return(true) + expect(ldap_user.valid_sign_in?).to be true + end + + it 'returns false if the GitLab user is not valid' do + gl_user.update_column(:username, nil) + + expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?) + expect(ldap_user.valid_sign_in?).to be false + end + end + describe 'find or create' do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f5a74956174..1e869df0988 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -481,6 +481,17 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end it_behaves_like 'with an invalid access token' + + context 'when the token belongs to a group via project share' do + let_it_be(:invited_group) { create(:group) } + + before do + invited_group.add_maintainer(project_bot_user) + create(:project_group_link, group: invited_group, project: project) + end + + it_behaves_like 'with a valid access token' + end end end end diff --git a/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb new file mode 100644 index 00000000000..7c78d8b0305 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillCiRunnerSemver, :migration, schema: 20220601151900 do + let(:ci_runners) { table(:ci_runners, database: :ci) } + + subject do + described_class.new( + start_id: 10, + end_id: 15, + batch_table: :ci_runners, + batch_column: :id, + sub_batch_size: 10, + pause_ms: 0, + connection: Ci::ApplicationRecord.connection) + end + + describe '#perform' do + it 'populates semver column on all runners in range' do + ci_runners.create!(id: 10, runner_type: 1, version: %q(HEAD-fd84d97)) + ci_runners.create!(id: 11, runner_type: 1, version: %q(v1.2.3)) + ci_runners.create!(id: 12, runner_type: 1, version: %q(2.1.0)) + ci_runners.create!(id: 13, runner_type: 1, version: %q(11.8.0~beta.935.g7f6d2abc)) + ci_runners.create!(id: 14, runner_type: 1, version: %q(13.2.2/1.1.0)) + ci_runners.create!(id: 15, runner_type: 1, version: %q('14.3.4')) + + subject.perform + + expect(ci_runners.all).to contain_exactly( + an_object_having_attributes(id: 10, semver: nil), + an_object_having_attributes(id: 11, semver: '1.2.3'), + an_object_having_attributes(id: 12, semver: '2.1.0'), + an_object_having_attributes(id: 13, semver: '11.8.0'), + an_object_having_attributes(id: 14, semver: '13.2.2'), + an_object_having_attributes(id: 15, semver: '14.3.4') + ) + end + + it 'skips runners that already have semver value' do + ci_runners.create!(id: 10, runner_type: 1, version: %q(1.2.4), semver: '1.2.3') + ci_runners.create!(id: 11, runner_type: 1, version: %q(1.2.5)) + ci_runners.create!(id: 12, runner_type: 1, version: %q(HEAD), semver: '1.2.4') + + subject.perform + + expect(ci_runners.all).to contain_exactly( + an_object_having_attributes(id: 10, semver: '1.2.3'), + an_object_having_attributes(id: 11, semver: '1.2.5'), + an_object_having_attributes(id: 12, semver: '1.2.4') + ) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb new file mode 100644 index 00000000000..e363a5a6b20 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData, + :migration, + schema: 20220707075300 do + let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let!(:issue_search_data_table) { table(:issue_search_data) } + + let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) } + let!(:project) do + table(:projects) + .create!( + namespace_id: namespace.id, + creator_id: user.id, + name: 'projecty', + path: 'path', + project_namespace_id: namespace.id) + end + + let!(:issue) do + table(:issues).create!( + project_id: project.id, + title: 'Patterson', + description: FFaker::HipsterIpsum.paragraph + ) + end + + let(:migration) do + described_class.new(start_id: issue.id, + end_id: issue.id + 30, + batch_table: :issues, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ApplicationRecord.connection) + end + + let(:perform_migration) { migration.perform } + + context 'when issue has search data record' do + let!(:issue_search_data) { issue_search_data_table.create!(project_id: project.id, issue_id: issue.id) } + + it 'does not create or update any search data records' do + expect { perform_migration } + .to not_change { issue_search_data_table.count } + .and not_change { issue_search_data } + + expect(issue_search_data_table.count).to eq(1) + end + end + + context 'when issue has no search data record' do + let(:title_node) { "'#{issue.title.downcase}':1A" } + + it 'creates search data records' do + expect { perform_migration } + .to change { issue_search_data_table.count }.from(0).to(1) + + expect(issue_search_data_table.find_by(project_id: project.id).issue_id) + .to eq(issue.id) + + expect(issue_search_data_table.find_by(project_id: project.id).search_vector) + .to include(title_node) + end + end + + context 'error handling' do + let!(:issue2) do + table(:issues).create!( + project_id: project.id, + title: 'Chatterton', + description: FFaker::HipsterIpsum.paragraph + ) + end + + before do + issue.update!(description: Array.new(30_000) { SecureRandom.hex }.join(' ')) + end + + let(:title_node2) { "'#{issue2.title.downcase}':1A" } + + it 'skips insertion for that issue but continues with migration' do + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger| + expect(logger) + .to receive(:error) + .with(a_hash_including(message: /string is too long for tsvector/, model_id: issue.id)) + end + + expect { perform_migration }.to change { issue_search_data_table.count }.from(0).to(1) + expect(issue_search_data_table.find_by(issue_id: issue.id)).to eq(nil) + expect(issue_search_data_table.find_by(issue_id: issue2.id).search_vector) + .to include(title_node2) + end + + it 're-raises exceptions' do + allow(migration) + .to receive(:update_search_data_individually) + .and_raise(ActiveRecord::StatementTimeout) + + expect { perform_migration }.to raise_error(ActiveRecord::StatementTimeout) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index cfa03db52fe..b5122af5cd4 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -47,10 +47,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat before do allow(snippet_with_repo).to receive(:disk_path).and_return(disk_path(snippet_with_repo)) - TestEnv.copy_repo(snippet_with_repo, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - + raw_repository(snippet_with_repo).create_from_bundle(TestEnv.factory_repo_bundle_path) raw_repository(snippet_with_empty_repo).create_repository end diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb index f8b3a8681f0..98866bb765f 100644 --- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb @@ -92,5 +92,69 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do end end end + + context 'when the subclass uses distinct each batch' do + let(:job_instance) do + job_class.new(start_id: 1, + end_id: 100, + batch_table: '_test_table', + batch_column: 'from_column', + sub_batch_size: 2, + pause_ms: 10, + connection: connection) + end + + let(:job_class) do + Class.new(described_class) do + def perform(*job_arguments) + distinct_each_batch(operation_name: :insert) do |sub_batch| + sub_batch.pluck(:from_column).each do |value| + connection.execute("INSERT INTO _test_insert_table VALUES (#{value})") + end + + sub_batch.size + end + end + end + end + + let(:test_table) { table(:_test_table) } + let(:test_insert_table) { table(:_test_insert_table) } + + before do + allow(job_instance).to receive(:sleep) + + connection.create_table :_test_table do |t| + t.timestamps_with_timezone null: false + t.integer :from_column, null: false + end + + connection.create_table :_test_insert_table, id: false do |t| + t.integer :to_column + t.index :to_column, unique: true + end + + test_table.create!(id: 1, from_column: 5) + test_table.create!(id: 2, from_column: 10) + test_table.create!(id: 3, from_column: 10) + test_table.create!(id: 4, from_column: 5) + test_table.create!(id: 5, from_column: 15) + end + + after do + connection.drop_table(:_test_table) + connection.drop_table(:_test_insert_table) + end + + it 'calls the operation for each distinct batch' do + expect { perform_job }.to change { test_insert_table.pluck(:to_column) }.from([]).to([5, 10, 15]) + end + + it 'stores the affected rows' do + perform_job + + expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1) + end + end end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb new file mode 100644 index 00000000000..94e9bcf9207 --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_statistics_with_container_registry_size_batching_strategy_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectStatisticsWithContainerRegistrySizeBatchingStrategy, '#next_batch' do # rubocop:disable Layout/LineLength + let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } + let(:namespace) { table(:namespaces) } + let(:project) { table(:projects) } + let(:container_repositories) { table(:container_repositories) } + + let!(:group) do + namespace.create!( + name: 'namespace1', type: 'Group', path: 'space1' + ) + end + + let!(:proj_namespace1) do + namespace.create!( + name: 'proj1', path: 'proj1', type: 'Project', parent_id: group.id + ) + end + + let!(:proj_namespace2) do + namespace.create!( + name: 'proj2', path: 'proj2', type: 'Project', parent_id: group.id + ) + end + + let!(:proj_namespace3) do + namespace.create!( + name: 'proj3', path: 'proj3', type: 'Project', parent_id: group.id + ) + end + + let!(:proj1) do + project.create!( + name: 'proj1', path: 'proj1', namespace_id: group.id, project_namespace_id: proj_namespace1.id + ) + end + + let!(:proj2) do + project.create!( + name: 'proj2', path: 'proj2', namespace_id: group.id, project_namespace_id: proj_namespace2.id + ) + end + + let!(:proj3) do + project.create!( + name: 'proj3', path: 'proj3', namespace_id: group.id, project_namespace_id: proj_namespace3.id + ) + end + + let!(:con1) do + container_repositories.create!( + project_id: proj1.id, + name: "ContReg_#{proj1.id}:1", + migration_state: 'import_done', + created_at: Date.new(2022, 01, 20) + ) + end + + let!(:con2) do + container_repositories.create!( + project_id: proj1.id, + name: "ContReg_#{proj1.id}:2", + migration_state: 'import_done', + created_at: Date.new(2022, 01, 20) + ) + end + + let!(:con3) do + container_repositories.create!( + project_id: proj2.id, + name: "ContReg_#{proj2.id}:1", + migration_state: 'import_done', + created_at: Date.new(2022, 01, 20) + ) + end + + let!(:con4) do + container_repositories.create!( + project_id: proj3.id, + name: "ContReg_#{proj3.id}:1", + migration_state: 'default', + created_at: Date.new(2022, 02, 20) + ) + end + + let!(:con5) do + container_repositories.create!( + project_id: proj3.id, + name: "ContReg_#{proj3.id}:2", + migration_state: 'default', + created_at: Date.new(2022, 02, 20) + ) + end + + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } + + context 'when starting on the first batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch( + :container_repositories, + :project_id, + batch_min_value: con1.project_id, + batch_size: 3, + job_arguments: [] + ) + expect(batch_bounds).to eq([con1.project_id, con4.project_id]) + end + end + + context 'when additional batches remain' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch( + :container_repositories, + :project_id, + batch_min_value: con3.project_id, + batch_size: 3, + job_arguments: [] + ) + + expect(batch_bounds).to eq([con3.project_id, con5.project_id]) + end + end + + context 'when no additional batches remain' do + it 'returns nil' do + batch_bounds = batching_strategy.next_batch(:container_repositories, + :project_id, + batch_min_value: con5.project_id + 1, + batch_size: 1, job_arguments: [] + ) + + expect(batch_bounds).to be_nil + end + end +end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb new file mode 100644 index 00000000000..f96c7de50f2 --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/dismissed_vulnerabilities_strategy_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::DismissedVulnerabilitiesStrategy, '#next_batch' do + let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:users) { table(:users) } + let(:user) { create_user! } + let(:project) do + table(:projects).create!( + namespace_id: namespace.id, + project_namespace_id: namespace.id, + packages_enabled: false) + end + + let(:vulnerabilities) { table(:vulnerabilities) } + + let!(:vulnerability1) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: Time.current + ) + end + + let!(:vulnerability2) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: Time.current + ) + end + + let!(:vulnerability3) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: Time.current + ) + end + + let!(:vulnerability4) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: nil + ) + end + + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } + + context 'when starting on the first batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch( + :vulnerabilities, + :id, + batch_min_value: vulnerability1.id, + batch_size: 2, + job_arguments: [] + ) + expect(batch_bounds).to eq([vulnerability1.id, vulnerability2.id]) + end + end + + context 'when additional batches remain' do + it 'returns the bounds of the next batch and skips the records that do not have `dismissed_at` set' do + batch_bounds = batching_strategy.next_batch( + :vulnerabilities, + :id, + batch_min_value: vulnerability3.id, + batch_size: 2, + job_arguments: [] + ) + + expect(batch_bounds).to eq([vulnerability3.id, vulnerability3.id]) + end + end + + context 'when no additional batches remain' do + it 'returns nil' do + batch_bounds = batching_strategy.next_batch( + :vulnerabilities, + :id, + batch_min_value: vulnerability4.id + 1, + batch_size: 1, + job_arguments: [] + ) + + expect(batch_bounds).to be_nil + end + end + + private + + def create_vulnerability!( + project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0, state: 1, dismissed_at: nil + ) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type, + state: state, + dismissed_at: dismissed_at + ) + end + + def create_user!(name: "Example User", email: "user@example.com", user_type: nil) + users.create!( + name: name, + email: email, + username: name, + projects_limit: 10 + ) + end +end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb new file mode 100644 index 00000000000..1a00fd7c8b3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::LooseIndexScanBatchingStrategy, '#next_batch' do + let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + let!(:namespace1) { namespaces.create!(name: 'ns1', path: 'ns1') } + let!(:namespace2) { namespaces.create!(name: 'ns2', path: 'ns2') } + let!(:namespace3) { namespaces.create!(name: 'ns3', path: 'ns3') } + let!(:namespace4) { namespaces.create!(name: 'ns4', path: 'ns4') } + let!(:namespace5) { namespaces.create!(name: 'ns5', path: 'ns5') } + let!(:project1) { projects.create!(name: 'p1', namespace_id: namespace1.id, project_namespace_id: namespace1.id) } + let!(:project2) { projects.create!(name: 'p2', namespace_id: namespace2.id, project_namespace_id: namespace2.id) } + let!(:project3) { projects.create!(name: 'p3', namespace_id: namespace3.id, project_namespace_id: namespace3.id) } + let!(:project4) { projects.create!(name: 'p4', namespace_id: namespace4.id, project_namespace_id: namespace4.id) } + let!(:project5) { projects.create!(name: 'p5', namespace_id: namespace5.id, project_namespace_id: namespace5.id) } + + let!(:issue1) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue2) { issues.create!(title: 'title', description: 'description', project_id: project1.id) } + let!(:issue3) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue4) { issues.create!(title: 'title', description: 'description', project_id: project3.id) } + let!(:issue5) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue6) { issues.create!(title: 'title', description: 'description', project_id: project4.id) } + let!(:issue7) { issues.create!(title: 'title', description: 'description', project_id: project5.id) } + + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::BaseStrategy } + + context 'when starting on the first batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project1.id, batch_size: 2, job_arguments: []) + + expect(batch_bounds).to eq([project1.id, project2.id]) + end + end + + context 'when additional batches remain' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project2.id, batch_size: 3, job_arguments: []) + + expect(batch_bounds).to eq([project2.id, project4.id]) + end + end + + context 'when on the final batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project4.id, batch_size: 3, job_arguments: []) + + expect(batch_bounds).to eq([project4.id, project5.id]) + end + end + + context 'when no additional batches remain' do + it 'returns nil' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project5.id + 1, batch_size: 1, job_arguments: []) + + expect(batch_bounds).to be_nil + end + end +end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb index 521e2067744..943b5744b64 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb @@ -45,10 +45,30 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi end end + context 'when job_class is provided with a batching_scope' do + let(:job_class) do + Class.new(described_class) do + def self.batching_scope(relation, job_arguments:) + min_id = job_arguments.first + + relation.where.not(type: 'Project').where('id >= ?', min_id) + end + end + end + + it 'applies the batching scope' do + expect(job_class).to receive(:batching_scope).and_call_original + + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1], job_class: job_class) + + expect(batch_bounds).to eq([namespace4.id, namespace4.id]) + end + end + context 'additional filters' do let(:strategy_with_filters) do Class.new(described_class) do - def apply_additional_filters(relation, job_arguments:) + def apply_additional_filters(relation, job_arguments:, job_class: nil) min_id = job_arguments.first relation.where.not(type: 'Project').where('id >= ?', min_id) diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb new file mode 100644 index 00000000000..f5a2dc91185 --- /dev/null +++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_inactive_public_projects_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForInactivePublicProjects, :migration do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } } + + let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') } + let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } + let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') } + let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-5', type: 'Project') } + + let(:project_1) do + projects_table + .create!( + name: 'proj-1', path: 'path-1', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_2.id, visibility_level: 0 + ) + end + + let(:project_2) do + projects_table + .create!( + name: 'proj-2', path: 'path-2', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_3.id, visibility_level: 10 + ) + end + + let(:project_3) do + projects_table + .create!( + name: 'proj-3', path: 'path-3', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_4.id, visibility_level: 20, last_activity_at: '2021-01-01' + ) + end + + let(:project_4) do + projects_table + .create!( + name: 'proj-4', path: 'path-4', namespace_id: namespace_1.id, + project_namespace_id: project_namespace_5.id, visibility_level: 20, last_activity_at: '2022-01-01' + ) + end + + before do + project_settings_table.create!(project_id: project_1.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_2.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_3.id, legacy_open_source_license_available: true) + project_settings_table.create!(project_id: project_4.id, legacy_open_source_license_available: true) + end + + it 'sets `legacy_open_source_license_available` attribute to false for inactive, public projects', + :aggregate_failures do + expect(queries.count).to eq(5) + + expect(migrated_attribute(project_1.id)).to be_truthy + expect(migrated_attribute(project_2.id)).to be_truthy + expect(migrated_attribute(project_3.id)).to be_falsey + expect(migrated_attribute(project_4.id)).to be_truthy + end + + def migrated_attribute(project_id) + project_settings_table.find(project_id).legacy_open_source_license_available + end +end diff --git a/spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb b/spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb new file mode 100644 index 00000000000..1ebdca136a3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateOperationVisibilityPermissionsFromOperations do + let(:namespaces) { table(:namespaces) } + let(:project_features) { table(:project_features) } + let(:projects) { table(:projects) } + + let(:namespace) { namespaces.create!(name: 'user', path: 'user') } + + let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace.id) } + let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace.id) } + let(:proj_namespace3) { namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: namespace.id) } + + let(:project1) { create_project('test1', proj_namespace1) } + let(:project2) { create_project('test2', proj_namespace2) } + let(:project3) { create_project('test3', proj_namespace3) } + + let!(:record1) { create_project_feature(project1) } + let!(:record2) { create_project_feature(project2, 20) } + let!(:record3) { create_project_feature(project3) } + + let(:sub_batch_size) { 2 } + let(:start_id) { record1.id } + let(:end_id) { record3.id } + let(:batch_table) { :project_features } + let(:batch_column) { :id } + let(:pause_ms) { 1 } + let(:connection) { ApplicationRecord.connection } + + let(:job) do + described_class.new( + start_id: start_id, + end_id: end_id, + batch_table: batch_table, + batch_column: batch_column, + sub_batch_size: sub_batch_size, + pause_ms: pause_ms, + connection: connection + ) + end + + subject(:perform) { job.perform } + + it 'updates all project settings records from their operations_access_level', :aggregate_failures do + perform + + expect_project_features_match_operations_access_level(record1) + expect_project_features_match_operations_access_level(record2) + expect_project_features_match_operations_access_level(record3) + end + + private + + def expect_project_features_match_operations_access_level(record) + record.reload + expect(record.monitor_access_level).to eq(record.operations_access_level) + expect(record.infrastructure_access_level).to eq(record.operations_access_level) + expect(record.feature_flags_access_level).to eq(record.operations_access_level) + expect(record.environments_access_level).to eq(record.operations_access_level) + end + + def create_project(proj_name, proj_namespace) + projects.create!( + namespace_id: namespace.id, + project_namespace_id: proj_namespace.id, + name: proj_name, + path: proj_name + ) + end + + def create_project_feature(project, operations_access_level = 10) + project_features.create!( + project_id: project.id, + pages_access_level: 10, + operations_access_level: operations_access_level + ) + end +end diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb index a54c840dd8e..8d71b117107 100644 --- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb +++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb @@ -73,26 +73,6 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence subject { described_class.new.perform(start_id, end_id) } - context 'when the migration is disabled by the feature flag' do - let(:start_id) { 1 } - let(:end_id) { 1001 } - - before do - stub_feature_flags(migrate_vulnerability_finding_uuids: false) - end - - it 'logs the info message and does not run the migration' do - expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance| - expect(instance).to receive(:info).once.with(message: 'Migration is disabled by the feature flag', - migrator: 'RecalculateVulnerabilitiesOccurrencesUuid', - start_id: start_id, - end_id: end_id) - end - - subject - end - end - context "when finding has a UUIDv4" do before do @uuid_v4 = create_finding!( @@ -474,6 +454,16 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception) expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(expected_error).once end + + it_behaves_like 'marks background migration job records' do + let(:arguments) { [1, 4] } + subject { described_class.new } + end + end + + it_behaves_like 'marks background migration job records' do + let(:arguments) { [1, 4] } + subject { described_class.new } end private diff --git a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb new file mode 100644 index 00000000000..d5b98e49a31 --- /dev/null +++ b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:users) { table(:users) } + let(:user) { create_user! } + let(:project) do + table(:projects).create!( + namespace_id: namespace.id, + project_namespace_id: namespace.id, + packages_enabled: false) + end + + let(:vulnerabilities) { table(:vulnerabilities) } + + let!(:vulnerability_with_dismissed_at) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: Time.current + ) + end + + let!(:vulnerability_without_dismissed_at) do + create_vulnerability!( + project_id: project.id, + author_id: user.id, + dismissed_at: nil + ) + end + + let(:detected_state) { 1 } + let(:dismissed_state) { 2 } + + subject(:perform_migration) do + described_class.new(start_id: vulnerability_with_dismissed_at.id, + end_id: vulnerability_without_dismissed_at.id, + batch_table: :vulnerabilities, + batch_column: :id, + sub_batch_size: 1, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'changes vulnerability state to `dismissed` when dismissed_at is not nil' do + expect { perform_migration }.to change { vulnerability_with_dismissed_at.reload.state }.to(dismissed_state) + end + + it 'does not change the state when dismissed_at is nil' do + expect { perform_migration }.not_to change { vulnerability_without_dismissed_at.reload.state } + end + + private + + def create_vulnerability!( + project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0, state: 1, dismissed_at: nil + ) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type, + state: state, + dismissed_at: dismissed_at + ) + end + + def create_user!(name: "Example User", email: "user@example.com", user_type: nil) + users.create!( + name: name, + email: email, + username: name, + projects_limit: 10 + ) + end +end diff --git a/spec/lib/gitlab/background_migration/update_delayed_project_removal_to_null_for_user_namespaces_spec.rb b/spec/lib/gitlab/background_migration/update_delayed_project_removal_to_null_for_user_namespaces_spec.rb new file mode 100644 index 00000000000..980a7771f4c --- /dev/null +++ b/spec/lib/gitlab/background_migration/update_delayed_project_removal_to_null_for_user_namespaces_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::UpdateDelayedProjectRemovalToNullForUserNamespaces, + :migration 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') + namespaces_table.create!(id: 2, name: 'user_namespace', path: 'path-2', type: 'User') + namespaces_table.create!(id: 3, name: 'user_three_namespace', path: 'path-3', type: 'User') + namespaces_table.create!(id: 4, name: 'group_four_namespace', path: 'path-4', type: 'Group') + namespaces_table.create!(id: 5, name: 'group_five_namespace', path: 'path-5', type: 'Group') + + namespace_settings_table.create!(namespace_id: 1, delayed_project_removal: false) + namespace_settings_table.create!(namespace_id: 2, delayed_project_removal: false) + namespace_settings_table.create!(namespace_id: 3, delayed_project_removal: nil) + namespace_settings_table.create!(namespace_id: 4, delayed_project_removal: true) + namespace_settings_table.create!(namespace_id: 5, delayed_project_removal: nil) + end + + it 'updates `delayed_project_removal` column to null for user namespaces', :aggregate_failures do + expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(7) + + expect(migrated_attribute(1)).to be_falsey + expect(migrated_attribute(2)).to be_nil + expect(migrated_attribute(3)).to be_nil + expect(migrated_attribute(4)).to be_truthy + expect(migrated_attribute(5)).to be_nil + end + + def migrated_attribute(namespace_id) + namespace_settings_table.find(namespace_id).delayed_project_removal + end +end diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index b0d721a74ce..8fb903154f3 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::BareRepositoryImport::Importer, :seed_helper do +RSpec.describe Gitlab::BareRepositoryImport::Importer do let!(:admin) { create(:admin) } let!(:base_dir) { Dir.mktmpdir + '/' } let(:bare_repository) { Gitlab::BareRepositoryImport::Repository.new(base_dir, File.join(base_dir, "#{project_path}.git")) } let(:gitlab_shell) { Gitlab::Shell.new } - let(:source_project) { TEST_REPO_PATH } + let(:source_project) { TestEnv.factory_repo_bundle_path } subject(:importer) { described_class.new(admin, bare_repository) } @@ -17,8 +17,6 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer, :seed_helper do after do FileUtils.rm_rf(base_dir) - TestEnv.clean_test_path - ensure_seeds end shared_examples 'importing a repository' do @@ -150,7 +148,6 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer, :seed_helper do end context 'with a repository already on disk' do - let!(:base_dir) { TestEnv.repos_path } # This is a quick way to get a valid repository instead of copying an # existing one. Since it's not persisted, the importer will try to # create the project. @@ -193,8 +190,6 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer, :seed_helper do def prepare_repository(project_path, source_project) repo_path = File.join(base_dir, project_path) - return create_bare_repository(repo_path) unless source_project - cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{source_project} #{repo_path}) system(git_env, *cmd, chdir: SEED_STORAGE_PATH, out: '/dev/null', err: '/dev/null') diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index bf115046744..d29447ee376 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -59,18 +59,15 @@ RSpec.describe ::Gitlab::BareRepositoryImport::Repository do let(:root_path) { TestEnv.repos_path } let(:repo_path) { File.join(root_path, "#{hashed_path}.git") } let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") } + let(:raw_repository) { Gitlab::Git::Repository.new('default', "#{hashed_path}.git", nil, nil) } before do - TestEnv.create_bare_repository(repo_path) - - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repository = Rugged::Repository.new(repo_path) - repository.config['gitlab.fullpath'] = 'to/repo' - end + raw_repository.create_repository + raw_repository.set_full_path(full_path: 'to/repo') end after do - FileUtils.rm_rf(repo_path) + raw_repository.remove end subject { described_class.new(root_path, repo_path) } diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index b723c31c4aa..e0a7044e5f9 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -328,6 +328,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer do expect(project.issues.where(state_id: Issue.available_states[:closed]).size).to eq(5) expect(project.issues.where(state_id: Issue.available_states[:opened]).size).to eq(2) + expect(project.issues.map(&:namespace_id).uniq).to match_array([project.project_namespace_id]) end describe 'wiki import' do @@ -362,6 +363,14 @@ RSpec.describe Gitlab::BitbucketImport::Importer do expect(project.issues.where("description LIKE ?", '%reporter3%').size).to eq(1) expect(importer.errors).to be_empty end + + it 'sets work item type on new issues' do + allow(importer).to receive(:import_wiki) + + importer.execute + + expect(project.issues.map(&:work_item_type_id).uniq).to contain_exactly(WorkItems::Type.default_issue_type.id) + end end context 'metrics' do diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb index 600682d30ad..92cad366cfd 100644 --- a/spec/lib/gitlab/changelog/config_spec.rb +++ b/spec/lib/gitlab/changelog/config_spec.rb @@ -20,6 +20,18 @@ RSpec.describe Gitlab::Changelog::Config do described_class.from_git(project) end + it "retrieves the specified configuration from git" do + allow(project.repository) + .to receive(:changelog_config).with('HEAD', 'specified_changelog_config.yml') + .and_return("---\ndate_format: '%Y'") + + expect(described_class) + .to receive(:from_hash) + .with(project, { 'date_format' => '%Y' }, nil) + + described_class.from_git(project, nil, 'specified_changelog_config.yml') + end + it 'returns the default configuration when no YAML file exists in Git' do allow(project.repository) .to receive(:changelog_config) diff --git a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb b/spec/lib/gitlab/ci/build/duration_parser_spec.rb index 889878cf3ef..7f5ff1eb0ee 100644 --- a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb +++ b/spec/lib/gitlab/ci/build/duration_parser_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Artifacts::ExpireInParser do +RSpec.describe Gitlab::Ci::Build::DurationParser do describe '.validate_duration', :request_store do subject { described_class.validate_duration(value) } diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 8f77a1f60ad..4895077a731 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -98,9 +98,11 @@ RSpec.describe Gitlab::Ci::Build::Image do let(:service_entrypoint) { '/bin/sh' } let(:service_alias) { 'db' } let(:service_command) { 'sleep 30' } + let(:pull_policy) { %w[always if-not-present] } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, - alias: service_alias, command: service_command, ports: [80] }] }) + alias: service_alias, command: service_command, ports: [80], + pull_policy: pull_policy }] }) end it 'fabricates an non-empty array of objects' do @@ -114,6 +116,7 @@ RSpec.describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + expect(subject.first.pull_policy).to eq(pull_policy) port = subject.first.ports.first expect(port.number).to eq 80 diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index 4ac8bf61738..3892b88598a 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -6,19 +6,43 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do describe '#satisfied_by?' do subject { described_class.new(globs).satisfied_by?(pipeline, context) } - it_behaves_like 'a glob matching rule' do + context 'a glob matching rule' do + using RSpec::Parameterized::TableSyntax + let(:pipeline) { build(:ci_pipeline) } let(:context) {} before do allow(pipeline).to receive(:modified_paths).and_return(files.keys) end + + # rubocop:disable Layout/LineLength + where(:case_name, :globs, :files, :satisfied) do + 'exact top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'exact top-level match' | { paths: ['Dockerfile'] } | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'exact top-level no match' | { paths: ['Dockerfile'] } | { 'Gemfile' => '' } | false + 'pattern top-level match' | { paths: ['Docker*'] } | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'pattern top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false + 'pattern top-level no match' | { paths: ['Docker*'] } | { 'Gemfile' => '' } | false + 'exact nested match' | { paths: ['project/build.properties'] } | { 'project/build.properties' => '' } | true + 'exact nested no match' | { paths: ['project/build.properties'] } | { 'project/README.md' => '' } | false + 'pattern nested match' | { paths: ['src/**/*.go'] } | { 'src/gitlab.com/goproject/goproject.go' => '' } | true + 'pattern nested no match' | { paths: ['src/**/*.go'] } | { 'src/gitlab.com/goproject/README.md' => '' } | false + 'ext top-level match' | { paths: ['*.go'] } | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | true + 'ext nested no match' | { paths: ['*.go'] } | { 'cmd/goproject/main.go' => '' } | false + 'ext slash no match' | { paths: ['/*.go'] } | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | false + end + # rubocop:enable Layout/LineLength + + with_them do + it { is_expected.to eq(satisfied) } + end end context 'when pipeline is nil' do let(:pipeline) {} let(:context) {} - let(:globs) { [] } + let(:globs) { { paths: [] } } it { is_expected.to be_truthy } end @@ -26,8 +50,8 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do context 'when using variable expansion' do let(:pipeline) { build(:ci_pipeline) } let(:modified_paths) { ['helm/test.txt'] } - let(:globs) { ['$HELM_DIR/**/*'] } - let(:context) { double('context') } + let(:globs) { { paths: ['$HELM_DIR/**/*'] } } + let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) } before do allow(pipeline).to receive(:modified_paths).and_return(modified_paths) @@ -58,7 +82,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do end context 'when variable expansion does not match' do - let(:globs) { ['path/with/$in/it/*'] } + let(:globs) { { paths: ['path/with/$in/it/*'] } } let(:modified_paths) { ['path/with/$in/it/file.txt'] } before do diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb index f905e229415..ac73b665f3a 100644 --- a/spec/lib/gitlab/ci/build/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb @@ -14,10 +14,14 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule do let(:ci_build) { build(:ci_build, pipeline: pipeline) } let(:rule) { described_class.new(rule_hash) } + before do + allow(pipeline).to receive(:modified_paths).and_return(['file.rb']) + end + describe '#matches?' do subject { rule.matches?(pipeline, seed) } - context 'with one matching clause' do + context 'with one matching clause if' do let(:rule_hash) do { if: '$VAR == null', when: 'always' } end @@ -25,9 +29,17 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule do it { is_expected.to eq(true) } end + context 'with one matching clause changes' do + let(:rule_hash) do + { changes: { paths: ['**/*'] }, when: 'always' } + end + + it { is_expected.to eq(true) } + end + context 'with two matching clauses' do let(:rule_hash) do - { if: '$VAR == null', changes: '**/*', when: 'always' } + { if: '$VAR == null', changes: { paths: ['**/*'] }, when: 'always' } end it { is_expected.to eq(true) } @@ -35,7 +47,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule do context 'with a matching and non-matching clause' do let(:rule_hash) do - { if: '$VAR != null', changes: '$VAR == null', when: 'always' } + { if: '$VAR != null', changes: { paths: ['invalid.xyz'] }, when: 'always' } end it { is_expected.to eq(false) } @@ -43,7 +55,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule do context 'with two non-matching clauses' do let(:rule_hash) do - { if: '$VAR != null', changes: 'README', when: 'always' } + { if: '$VAR != null', changes: { paths: ['README'] }, when: 'always' } end it { is_expected.to eq(false) } diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index bd1ab5d8c41..0fa6d4f8804 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do before do stub_feature_flags(ci_docker_image_pull_policy: true) + + entry.compose! end let(:entry) { described_class.new(config) } @@ -129,19 +131,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#valid?' do it 'is valid' do - entry.compose! - expect(entry).to be_valid end context 'when the feature flag ci_docker_image_pull_policy is disabled' do before do stub_feature_flags(ci_docker_image_pull_policy: false) + entry.compose! end it 'is not valid' do - entry.compose! - expect(entry).not_to be_valid expect(entry.errors).to include('image config contains unknown keys: pull_policy') end @@ -150,8 +149,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#value' do it "returns value" do - entry.compose! - expect(entry.value).to eq( name: 'image:1.0', pull_policy: ['if-not-present'] @@ -161,11 +158,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do context 'when the feature flag ci_docker_image_pull_policy is disabled' do before do stub_feature_flags(ci_docker_image_pull_policy: false) + entry.compose! end it 'is not valid' do - entry.compose! - expect(entry.value).to eq( name: 'image:1.0' ) diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb index 3ed4a9f263f..295561b3c4d 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do it { is_expected.not_to be_valid } it 'reports an error about invalid policy' do - expect(entry.errors).to include(/should be an array of strings/) + expect(entry.errors).to include(/should be an array or a hash/) end end @@ -64,7 +64,59 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do it 'returns information about errors' do expect(entry.errors) - .to include(/should be an array of strings/) + .to include(/should be an array or a hash/) + end + end + + context 'with paths' do + context 'when paths is an array of strings' do + let(:config) { { paths: %w[app/ lib/] } } + + it { is_expected.to be_valid } + end + + context 'when paths is not an array' do + let(:config) { { paths: 'string' } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be an array of strings/) + end + end + + context 'when paths is an array of integers' do + let(:config) { { paths: [1, 2] } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be an array of strings/) + end + end + + context 'when paths is an array of long strings' do + let(:config) { { paths: ['a'] * 51 } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/has too many entries \(maximum 50\)/) + end + end + + context 'when paths is nil' do + let(:config) { { paths: nil } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be an array of strings/) + end end end end @@ -75,6 +127,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do context 'when using a string array' do let(:config) { %w[app/ lib/ spec/ other/* paths/**/*.rb] } + it { is_expected.to eq(paths: config) } + end + + context 'with paths' do + let(:config) do + { paths: ['app/', 'lib/'] } + end + it { is_expected.to eq(config) } end end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 89d349efe8f..93f4a66bfb6 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -115,7 +115,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it { is_expected.not_to be_valid } it 'reports an error about invalid policy' do - expect(subject.errors).to include(/should be an array of strings/) + expect(subject.errors).to include(/should be an array or a hash/) end end @@ -411,7 +411,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do context 'when using a changes: clause' do let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } - it { is_expected.to eq(config) } + it { is_expected.to eq(changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb] }) } + + context 'when using changes with paths' do + let(:config) { { changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } } + + it { is_expected.to eq(config) } + end end context 'when default value has been provided' do @@ -426,7 +432,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do end it 'does not add to provided configuration' do - expect(entry.value).to eq(config) + expect(entry.value).to eq(changes: { paths: %w[app/**/*.rb] }) end end diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb index cfec33003e4..b0871f2345e 100644 --- a/spec/lib/gitlab/ci/config/entry/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'fast_spec_helper' -require 'support/helpers/stub_feature_flags' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Rules do @@ -12,13 +11,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do end let(:metadata) { { allowed_when: %w[always never] } } - let(:entry) { factory.create! } - describe '.new' do - subject { entry } + subject(:entry) { factory.create! } + describe '.new' do before do - subject.compose! + entry.compose! end context 'with a list of rule rule' do @@ -73,7 +71,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do end describe '#value' do - subject { entry.value } + subject(:value) { entry.value } + + before do + entry.compose! + end context 'with a list of rule rule' do let(:config) do @@ -99,7 +101,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do { if: '$SKIP', when: 'never' } end - it { is_expected.to eq([config]) } + it { is_expected.to eq([]) } end context 'with nested rules' do diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 2795cc9dddf..3c000fd09ed 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -1,14 +1,19 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' RSpec.describe Gitlab::Ci::Config::Entry::Service do - let(:entry) { described_class.new(config) } + include StubFeatureFlags before do + stub_feature_flags(ci_docker_image_pull_policy: true) entry.compose! end + subject(:entry) { described_class.new(config) } + context 'when configuration is a string' do let(:config) { 'postgresql:9.5' } @@ -90,6 +95,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do end end + describe '#pull_policy' do + it "returns nil" do + expect(entry.pull_policy).to be_nil + end + end + context 'when configuration has ports' do let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:config) do @@ -134,6 +145,49 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do end end end + + context 'when configuration has pull_policy' do + let(:config) { { name: 'postgresql:9.5', pull_policy: 'if-not-present' } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + entry.compose! + end + + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors).to include('service config contains unknown keys: pull_policy') + end + end + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'postgresql:9.5', + pull_policy: ['if-not-present'] + ) + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'is not valid' do + expect(entry.value).to eq( + name: 'postgresql:9.5' + ) + end + end + end + end end context 'when entry value is not correct' do diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 800c563cd0b..40702e75404 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::Context do - let(:project) { double('Project') } + let(:project) { build(:project) } let(:user) { double('User') } let(:sha) { '12345' } let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'a', 'value' => 'b' }]) } @@ -126,7 +126,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context do end context 'with attributes' do - let(:new_attributes) { { project: double, user: double, sha: '56789' } } + let(:new_attributes) { { project: build(:project), user: double, sha: '56789' } } it_behaves_like 'a mutated context' end diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 77e542cf933..72a85c9b03d 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -177,6 +177,22 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do expect(project_file.error_message).to include("Project `xxxxxxxxxxxxxxxxxxxxxxx` not found or access denied!") end end + + context 'when a project contained in an array is used with a masked variable' do + let(:variables) do + Gitlab::Ci::Variables::Collection.new([ + { key: 'VAR1', value: 'a_secret_variable_value', masked: true } + ]) + end + + let(:params) do + { project: ['a_secret_variable_value'], file: '/file.yml' } + end + + it 'does not raise an error' do + expect { valid? }.not_to raise_error + end + end end describe '#expand_context' do diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 7e1b31fea6a..e74fdc2071b 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -232,11 +232,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do image: 'image:1.0' } end - before do - stub_const("#{described_class}::MAX_INCLUDES", 2) - end - it 'does not raise an exception' do + allow(context).to receive(:max_includes).and_return(2) + expect { subject }.not_to raise_error end end @@ -250,11 +248,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do image: 'image:1.0' } end - before do - stub_const("#{described_class}::MAX_INCLUDES", 1) - end - it 'raises an exception' do + allow(context).to receive(:max_includes).and_return(1) + expect { subject }.to raise_error(described_class::TooManyIncludesError) end @@ -264,6 +260,8 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do end it 'raises an exception' do + allow(context).to receive(:max_includes).and_return(1) + expect { subject }.to raise_error(described_class::TooManyIncludesError) 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 15a0ff40aa4..841a46e197d 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -323,11 +323,9 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do end context 'when too many includes is included' do - before do - stub_const('Gitlab::Ci::Config::External::Mapper::MAX_INCLUDES', 1) - end - it 'raises an error' do + allow(context).to receive(:max_includes).and_return(1) + expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /Maximum of 1 nested/) end end diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 179e2efc0c7..147801b6217 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -48,6 +48,7 @@ RSpec.describe Gitlab::Ci::Jwt do expect(payload[:ref_protected]).to eq(build.protected.to_s) expect(payload[:environment]).to be_nil expect(payload[:environment_protected]).to be_nil + expect(payload[:deployment_tier]).to be_nil end end @@ -96,7 +97,7 @@ RSpec.describe Gitlab::Ci::Jwt do end describe 'environment' do - let(:environment) { build_stubbed(:environment, project: project, name: 'production') } + let(:environment) { build_stubbed(:environment, project: project, name: 'production', tier: 'production') } let(:build) do build_stubbed( :ci_build, @@ -114,6 +115,19 @@ RSpec.describe Gitlab::Ci::Jwt do it 'has correct values for environment attributes' do expect(payload[:environment]).to eq('production') expect(payload[:environment_protected]).to eq('false') + expect(payload[:deployment_tier]).to eq('production') + end + + describe 'deployment_tier' do + context 'when build options specifies a different deployment_tier' do + before do + build.options[:environment] = { name: environment.name, deployment_tier: 'development' } + end + + it 'uses deployment_tier from build options' do + expect(payload[:deployment_tier]).to eq('development') + end + end end end end @@ -121,8 +135,8 @@ RSpec.describe Gitlab::Ci::Jwt do describe '.for_build' do shared_examples 'generating JWT for build' do context 'when signing key is present' do - let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) } - let(:rsa_key_data) { rsa_key.to_s } + let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(3072) } + let_it_be(:rsa_key_data) { rsa_key.to_s } it 'generates JWT with key id' do _payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb index 375841ce236..cbf92f8fa83 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:stage) { build(:ci_stage, project: project, statuses: [job]) } let(:pipeline) { create(:ci_pipeline, project: project, stages: [stage]) } let(:command) do diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb index 9057c4e99df..eba0db0adfb 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do context 'tags persistence' do let(:stage) do - build(:ci_stage_entity, pipeline: pipeline, project: project) + build(:ci_stage, pipeline: pipeline, project: project) end let(:job) do @@ -77,7 +77,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do context 'without tags' do it 'extracts an empty tag list' do - expect(CommitStatus) + expect(Gitlab::Ci::Tags::BulkInsert) .to receive(:bulk_insert_tags!) .with([job]) .and_call_original @@ -95,7 +95,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do end it 'bulk inserts tags' do - expect(CommitStatus) + expect(Gitlab::Ci::Tags::BulkInsert) .to receive(:bulk_insert_tags!) .with([job]) .and_call_original diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb index 6a7d9b58a05..e07a3ca9033 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do let(:project) { create(:project) } let(:user) { create(:user) } - let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:stage) { build(:ci_stage, project: project, statuses: [job]) } let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) } let(:command) do diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb index 571455d6279..f14dd70a753 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups do let(:project) { create(:project) } let(:user) { create(:user) } - let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:stage) { build(:ci_stage, project: project, statuses: [job]) } let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) } let!(:environment) { create(:environment, name: 'production', project: project) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index cebc4c02d11..eeac0c85a77 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end - it 'respects the defined payload schema', :saas do + it 'respects the defined payload schema' do expect(::Gitlab::HTTP).to receive(:post) do |_url, params| expect(params[:body]).to match_schema('/external_validation') expect(params[:timeout]).to eq(described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT) @@ -235,6 +235,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end it 'logs the authorization' do + allow(Gitlab::AppLogger).to receive(:info) + expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id) perform! diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 49505d397c2..040f3ab5830 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -858,14 +858,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do context 'with an explicit `when: never`' do where(:rule_set) do [ - [[{ changes: %w[*/**/*.rb], when: 'never' }, { changes: %w[*/**/*.rb], when: 'always' }]], - [[{ changes: %w[app/models/ci/pipeline.rb], when: 'never' }, { changes: %w[app/models/ci/pipeline.rb], when: 'always' }]], - [[{ changes: %w[spec/**/*.rb], when: 'never' }, { changes: %w[spec/**/*.rb], when: 'always' }]], - [[{ changes: %w[*.yml], when: 'never' }, { changes: %w[*.yml], when: 'always' }]], - [[{ changes: %w[.*.yml], when: 'never' }, { changes: %w[.*.yml], when: 'always' }]], - [[{ changes: %w[**/*], when: 'never' }, { changes: %w[**/*], when: 'always' }]], - [[{ changes: %w[*/**/*.rb *.yml], when: 'never' }, { changes: %w[*/**/*.rb *.yml], when: 'always' }]], - [[{ changes: %w[.*.yml **/*], when: 'never' }, { changes: %w[.*.yml **/*], when: 'always' }]] + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]] ] end @@ -881,14 +881,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do context 'with an explicit `when: always`' do where(:rule_set) do [ - [[{ changes: %w[*/**/*.rb], when: 'always' }, { changes: %w[*/**/*.rb], when: 'never' }]], - [[{ changes: %w[app/models/ci/pipeline.rb], when: 'always' }, { changes: %w[app/models/ci/pipeline.rb], when: 'never' }]], - [[{ changes: %w[spec/**/*.rb], when: 'always' }, { changes: %w[spec/**/*.rb], when: 'never' }]], - [[{ changes: %w[*.yml], when: 'always' }, { changes: %w[*.yml], when: 'never' }]], - [[{ changes: %w[.*.yml], when: 'always' }, { changes: %w[.*.yml], when: 'never' }]], - [[{ changes: %w[**/*], when: 'always' }, { changes: %w[**/*], when: 'never' }]], - [[{ changes: %w[*/**/*.rb *.yml], when: 'always' }, { changes: %w[*/**/*.rb *.yml], when: 'never' }]], - [[{ changes: %w[.*.yml **/*], when: 'always' }, { changes: %w[.*.yml **/*], when: 'never' }]] + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]] ] end @@ -904,14 +904,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do context 'without an explicit when: value' do where(:rule_set) do [ - [[{ changes: %w[*/**/*.rb] }]], - [[{ changes: %w[app/models/ci/pipeline.rb] }]], - [[{ changes: %w[spec/**/*.rb] }]], - [[{ changes: %w[*.yml] }]], - [[{ changes: %w[.*.yml] }]], - [[{ changes: %w[**/*] }]], - [[{ changes: %w[*/**/*.rb *.yml] }]], - [[{ changes: %w[.*.yml **/*] }]] + [[{ changes: { paths: %w[*/**/*.rb] } }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]], + [[{ changes: { paths: %w[spec/**/*.rb] } }]], + [[{ changes: { paths: %w[*.yml] } }]], + [[{ changes: { paths: %w[.*.yml] } }]], + [[{ changes: { paths: %w[**/*] } }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]], + [[{ changes: { paths: %w[.*.yml **/*] } }]] ] end diff --git a/spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb b/spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb index eec218346c2..f116b175fc7 100644 --- a/spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb +++ b/spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb @@ -75,16 +75,6 @@ RSpec.describe Gitlab::Ci::Reports::CoverageReportGenerator, factory_default: :k end it_behaves_like 'having a coverage report' - - context 'when feature flag ci_child_pipeline_coverage_reports is disabled' do - before do - stub_feature_flags(ci_child_pipeline_coverage_reports: false) - end - - it 'returns empty coverage reports' do - expect(subject).to be_empty - end - end end context 'when both parent and child pipeline have builds with coverage reports' do diff --git a/spec/lib/gitlab/ci/reports/test_reports_spec.rb b/spec/lib/gitlab/ci/reports/test_report_spec.rb index 24c00de3731..539510bca9e 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_report_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Reports::TestReports do +RSpec.describe Gitlab::Ci::Reports::TestReport do include TestReportsHelper let(:test_reports) { described_class.new } diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb index 3483dddca3a..ac64e4699fe 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Gitlab::Ci::Reports::TestReportsComparer do include TestReportsHelper let(:comparer) { described_class.new(base_reports, head_reports) } - let(:base_reports) { Gitlab::Ci::Reports::TestReports.new } - let(:head_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:base_reports) { Gitlab::Ci::Reports::TestReport.new } + let(:head_reports) { Gitlab::Ci::Reports::TestReport.new } describe '#suite_comparers' do subject { comparer.suite_comparers } diff --git a/spec/lib/gitlab/ci/runner/metrics_spec.rb b/spec/lib/gitlab/ci/runner/metrics_spec.rb new file mode 100644 index 00000000000..3c459271092 --- /dev/null +++ b/spec/lib/gitlab/ci/runner/metrics_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Runner::Metrics, :prometheus do + subject { described_class.new } + + describe '#increment_runner_authentication_success_counter' do + it 'increments count for same type' do + expect { subject.increment_runner_authentication_success_counter(runner_type: 'instance_type') } + .to change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') }.by(1) + end + + it 'does not increment count for different type' do + expect { subject.increment_runner_authentication_success_counter(runner_type: 'group_type') } + .to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'project_type') } + end + + it 'does not increment failure count' do + expect { subject.increment_runner_authentication_success_counter(runner_type: 'project_type') } + .to not_change { described_class.runner_authentication_failure_counter.get } + end + + it 'throws ArgumentError for invalid runner type' do + expect { subject.increment_runner_authentication_success_counter(runner_type: 'unknown_type') } + .to raise_error(ArgumentError, 'unknown runner type: unknown_type') + end + end + + describe '#increment_runner_authentication_failure_counter' do + it 'increments count' do + expect { subject.increment_runner_authentication_failure_counter } + .to change { described_class.runner_authentication_failure_counter.get }.by(1) + end + + it 'does not increment success count' do + expect { subject.increment_runner_authentication_failure_counter } + .to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') } + end + end +end diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index 9e4a8739c0f..576eb02ad83 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -5,16 +5,25 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::RunnerReleases do subject { described_class.instance } - describe '#releases' do - before do - subject.reset! + let(:runner_releases_url) { 'the release API URL' } - stub_application_setting(public_runner_releases_url: 'the release API URL') - allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(response) } - end + def releases + subject.releases + end + + def releases_by_minor + subject.releases_by_minor + end + + before do + subject.reset_backoff! - def releases - subject.releases + stub_application_setting(public_runner_releases_url: runner_releases_url) + end + + describe 'caching behavior', :use_clean_rails_memory_store_caching do + before do + allow(Gitlab::HTTP).to receive(:get).with(runner_releases_url, anything).once { mock_http_response(response) } end shared_examples 'requests that follow cache status' do |validity_period| @@ -25,9 +34,14 @@ RSpec.describe Gitlab::Ci::RunnerReleases do releases travel followup_request_interval do - expect(Gitlab::HTTP).not_to receive(:try_get) + expect(Gitlab::HTTP).not_to receive(:get) - expect(releases).to eq(expected_result) + if expected_releases + expected_result_by_minor = expected_releases.group_by(&:without_patch).transform_values(&:max) + end + + expect(releases).to eq(expected_releases) + expect(releases_by_minor).to eq(expected_result_by_minor) end end end @@ -40,75 +54,189 @@ RSpec.describe Gitlab::Ci::RunnerReleases do releases travel followup_request_interval do - expect(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(followup_response) } - - expect(releases).to eq((expected_result || []) + [Gitlab::VersionInfo.new(14, 9, 2)]) + expect(Gitlab::HTTP).to receive(:get) + .with(runner_releases_url, anything) + .once { mock_http_response(followup_response) } + + new_releases = (expected_releases || []) + [Gitlab::VersionInfo.new(14, 9, 2)] + new_releases_by_minor_version = (expected_releases_by_minor || {}).merge( + Gitlab::VersionInfo.new(14, 9, 0) => Gitlab::VersionInfo.new(14, 9, 2) + ) + expect(releases).to eq(new_releases) + expect(releases_by_minor).to eq(new_releases_by_minor_version) end end end end - context 'when response is nil' do - let(:response) { nil } - let(:expected_result) { nil } - - it 'returns nil' do - expect(releases).to be_nil - end - - it_behaves_like 'requests that follow cache status', 5.seconds - + shared_examples 'a service implementing exponential backoff' do |opts| it 'performs exponential backoff on requests', :aggregate_failures do start_time = Time.now.utc.change(usec: 0) http_call_timestamp_offsets = [] - allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL') do + allow(Gitlab::HTTP).to receive(:get).with(runner_releases_url, anything) do http_call_timestamp_offsets << Time.now.utc - start_time + + raise Net::OpenTimeout if opts&.dig(:raise_timeout) + mock_http_response(response) end # An initial HTTP request fails travel_to(start_time) - subject.reset! + subject.reset_backoff! expect(releases).to be_nil + expect(releases_by_minor).to be_nil # Successive failed requests result in HTTP requests only after specific backoff periods backoff_periods = [5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600].map(&:seconds) backoff_periods.each do |period| travel(period - 1.second) expect(releases).to be_nil + expect(releases_by_minor).to be_nil travel 1.second expect(releases).to be_nil + expect(releases_by_minor).to be_nil end expect(http_call_timestamp_offsets).to eq([0, 5, 15, 35, 75, 155, 315, 635, 1275, 2555, 5115, 8715]) # Finally a successful HTTP request results in releases being returned - allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response([{ 'name' => 'v14.9.1' }]) } + allow(Gitlab::HTTP).to receive(:get) + .with(runner_releases_url, anything) + .once { mock_http_response([{ 'name' => 'v14.9.1-beta1-ee' }]) } travel 1.hour expect(releases).not_to be_nil + expect(releases_by_minor).not_to be_nil end end + context 'when request results in timeout' do + let(:response) { } + let(:expected_releases) { nil } + let(:expected_releases_by_minor) { nil } + + it_behaves_like 'requests that follow cache status', 5.seconds + it_behaves_like 'a service implementing exponential backoff', raise_timeout: true + end + + context 'when response is nil' do + let(:response) { nil } + let(:expected_releases) { nil } + let(:expected_releases_by_minor) { nil } + + it_behaves_like 'requests that follow cache status', 5.seconds + it_behaves_like 'a service implementing exponential backoff' + end + context 'when response is not nil' do - let(:response) { [{ 'name' => 'v14.9.1' }, { 'name' => 'v14.9.0' }] } - let(:expected_result) { [Gitlab::VersionInfo.new(14, 9, 0), Gitlab::VersionInfo.new(14, 9, 1)] } + let(:response) { [{ 'name' => 'v14.9.1-beta1-ee' }, { 'name' => 'v14.9.0' }] } + let(:expected_releases) do + [ + Gitlab::VersionInfo.new(14, 9, 0), + Gitlab::VersionInfo.new(14, 9, 1, '-beta1-ee') + ] + end + + let(:expected_releases_by_minor) do + { + Gitlab::VersionInfo.new(14, 9, 0) => Gitlab::VersionInfo.new(14, 9, 1, '-beta1-ee') + } + end + + it_behaves_like 'requests that follow cache status', 1.day + end + end + + describe '#releases', :use_clean_rails_memory_store_caching do + before do + allow(Gitlab::HTTP).to receive(:get).with(runner_releases_url, anything).once { mock_http_response(response) } + end + + context 'when response is nil' do + let(:response) { nil } + let(:expected_result) { nil } + + it 'returns nil' do + expect(releases).to be_nil + end + end + + context 'when response is not nil' do + let(:response) { [{ 'name' => 'v14.9.1-beta1-ee' }, { 'name' => 'v14.9.0' }] } + let(:expected_result) do + [ + Gitlab::VersionInfo.new(14, 9, 0), + Gitlab::VersionInfo.new(14, 9, 1, '-beta1-ee') + ] + end it 'returns parsed and sorted Gitlab::VersionInfo objects' do expect(releases).to eq(expected_result) end + end - it_behaves_like 'requests that follow cache status', 1.day + context 'when response contains unexpected input type' do + let(:response) { 'error' } + + it { expect(releases).to be_nil } + end + + context 'when response contains unexpected input array' do + let(:response) { ['error'] } + + it { expect(releases).to be_nil } + end + end + + describe '#releases_by_minor', :use_clean_rails_memory_store_caching do + before do + allow(Gitlab::HTTP).to receive(:get).with(runner_releases_url, anything).once { mock_http_response(response) } end - def mock_http_response(response) - http_response = instance_double(HTTParty::Response) + context 'when response is nil' do + let(:response) { nil } + let(:expected_result) { nil } - allow(http_response).to receive(:success?).and_return(response.present?) - allow(http_response).to receive(:parsed_response).and_return(response) + it 'returns nil' do + expect(releases_by_minor).to be_nil + end + end - http_response + context 'when response is not nil' do + let(:response) { [{ 'name' => 'v14.9.1-beta1-ee' }, { 'name' => 'v14.9.0' }, { 'name' => 'v14.8.1' }] } + let(:expected_result) do + { + Gitlab::VersionInfo.new(14, 8, 0) => Gitlab::VersionInfo.new(14, 8, 1), + Gitlab::VersionInfo.new(14, 9, 0) => Gitlab::VersionInfo.new(14, 9, 1, '-beta1-ee') + } + end + + it 'returns parsed and grouped Gitlab::VersionInfo objects' do + expect(releases_by_minor).to eq(expected_result) + end end + + context 'when response contains unexpected input type' do + let(:response) { 'error' } + + it { expect(releases_by_minor).to be_nil } + end + + context 'when response contains unexpected input array' do + let(:response) { ['error'] } + + it { expect(releases_by_minor).to be_nil } + end + end + + def mock_http_response(response) + http_response = instance_double(HTTParty::Response) + + allow(http_response).to receive(:success?).and_return(!response.nil?) + allow(http_response).to receive(:parsed_response).and_return(response) + + http_response end end diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index 0353432741b..f2507a24b10 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -3,84 +3,156 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do - include StubVersion using RSpec::Parameterized::TableSyntax describe '#check_runner_upgrade_status' do subject(:result) { described_class.instance.check_runner_upgrade_status(runner_version) } + let(:gitlab_version) { '14.1.1' } + let(:parsed_runner_version) { ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true) } + before do - runner_releases_double = instance_double(Gitlab::Ci::RunnerReleases) + allow(described_class.instance).to receive(:gitlab_version) + .and_return(::Gitlab::VersionInfo.parse(gitlab_version)) + end + + context 'with failing Gitlab::Ci::RunnerReleases request' do + let(:runner_version) { '14.1.123' } + let(:runner_releases_double) { instance_double(Gitlab::Ci::RunnerReleases) } + + before do + allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double) + allow(runner_releases_double).to receive(:releases).and_return(nil) + end - allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double) - allow(runner_releases_double).to receive(:releases).and_return(available_runner_releases.map { |v| ::Gitlab::VersionInfo.parse(v) }) + it 'returns :error' do + is_expected.to eq({ error: parsed_runner_version }) + end end - context 'with available_runner_releases configured up to 14.1.1' do - let(:available_runner_releases) { %w[13.9.0 13.9.1 13.9.2 13.10.0 13.10.1 14.0.0 14.0.1 14.0.2 14.1.0 14.1.1 14.1.1-rc3] } + context 'with available_runner_releases configured' do + before do + url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url - context 'with nil runner_version' do - let(:runner_version) { nil } + WebMock.stub_request(:get, url).to_return( + body: available_runner_releases.map { |v| { name: v } }.to_json, + status: 200, + headers: { 'Content-Type' => 'application/json' } + ) + end - it 'returns :invalid' do - is_expected.to eq(:invalid) + context 'with no available runner releases' do + let(:available_runner_releases) do + %w[] end - end - context 'with invalid runner_version' do - let(:runner_version) { 'junk' } + context 'with Gitlab::VERSION set to 14.1.1' do + let(:gitlab_version) { '14.1.1' } - it 'raises ArgumentError' do - expect { subject }.to raise_error(ArgumentError) + context 'with runner_version from last minor release' do + let(:runner_version) { 'v14.0.1' } + + it 'returns :not_available' do + is_expected.to eq({ not_available: parsed_runner_version }) + end + end end end - context 'with Gitlab::VERSION set to 14.1.123' do - before do - stub_version('14.1.123', 'deadbeef') + context 'up to 14.1.1' do + let(:available_runner_releases) do + %w[13.9.0 13.9.1 13.9.2 13.10.0 13.10.1 14.0.0 14.0.1 14.0.2-rc1 14.0.2 14.1.0 14.1.1] + end + + context 'with nil runner_version' do + let(:runner_version) { nil } - described_class.instance.reset! + it 'returns :invalid_version' do + is_expected.to match({ invalid_version: anything }) + end end - context 'with a runner_version that is too recent' do - let(:runner_version) { 'v14.2.0' } + context 'with invalid runner_version' do + let(:runner_version) { 'junk' } - it 'returns :not_available' do - is_expected.to eq(:not_available) + it 'returns :invalid_version' do + is_expected.to match({ invalid_version: anything }) end end - end - context 'with Gitlab::VERSION set to 14.0.1' do - before do - stub_version('14.0.1', 'deadbeef') + context 'with Gitlab::VERSION set to 14.1.123' do + let(:gitlab_version) { '14.1.123' } + + context 'with a runner_version that is too recent' do + let(:runner_version) { 'v14.2.0' } - described_class.instance.reset! + it 'returns :not_available' do + is_expected.to eq({ not_available: parsed_runner_version }) + end + end + end + + context 'with Gitlab::VERSION set to 14.0.1' do + let(:gitlab_version) { '14.0.1' } + + context 'with valid params' do + where(:runner_version, :expected_result, :expected_suggested_version) do + 'v15.0.0' | :not_available | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available + 'v14.1.0-rc3' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes + 'v14.1.0~beta.1574.gf6ea9389' | :recommended | '14.1.1' # suffixes are correctly handled + 'v14.1.0/1.1.0' | :recommended | '14.1.1' # suffixes are correctly handled + 'v14.1.0' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes + 'v14.0.1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available + 'v14.0.2-rc1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate + 'v14.0.2' | :not_available | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version + 'v13.10.1' | :available | '14.0.2' # available upgrade: 14.0.2 + 'v13.10.1~beta.1574.gf6ea9389' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available + 'v13.10.1/1.1.0' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available + 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available + 'v13.9.2' | :recommended | '14.0.2' # recommended upgrade since backports are no longer released for this version + 'v13.9.0' | :recommended | '14.0.2' # recommended upgrade since backports are no longer released for this version + 'v13.8.1' | :recommended | '14.0.2' # recommended upgrade since build is too old (missing in records) + 'v11.4.1' | :recommended | '14.0.2' # recommended upgrade since build is too old (missing in records) + end + + with_them do + it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) } + end + end end - context 'with valid params' do - where(:runner_version, :expected_result) do - 'v15.0.0' | :not_available # not available since the GitLab instance is still on 14.x and a major version might be incompatible - 'v14.1.0-rc3' | :recommended # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes - 'v14.1.0~beta.1574.gf6ea9389' | :recommended # suffixes are correctly handled - 'v14.1.0/1.1.0' | :recommended # suffixes are correctly handled - 'v14.1.0' | :recommended # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes - 'v14.0.1' | :recommended # recommended upgrade since 14.0.2 is available - 'v14.0.2' | :not_available # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version - 'v13.10.1' | :available # available upgrade: 14.1.1 - 'v13.10.1~beta.1574.gf6ea9389' | :available # suffixes are correctly handled - 'v13.10.1/1.1.0' | :available # suffixes are correctly handled - 'v13.10.0' | :recommended # recommended upgrade since 13.10.1 is available - 'v13.9.2' | :recommended # recommended upgrade since backports are no longer released for this version - 'v13.9.0' | :recommended # recommended upgrade since backports are no longer released for this version - 'v13.8.1' | :recommended # recommended upgrade since build is too old (missing in records) - 'v11.4.1' | :recommended # recommended upgrade since build is too old (missing in records) + context 'with Gitlab::VERSION set to 13.9.0' do + let(:gitlab_version) { '13.9.0' } + + context 'with valid params' do + where(:runner_version, :expected_result, :expected_suggested_version) do + 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible + 'v13.10.1' | :not_available | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x + 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available + 'v13.9.2' | :not_available | '13.9.2' # not_available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version + 'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version + 'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) + 'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) + end + + with_them do + it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) } + end end + end + end + + context 'up to 15.1.0' do + let(:available_runner_releases) { %w[14.9.1 14.9.2 14.10.0 14.10.1 15.0.0 15.1.0] } + + context 'with Gitlab::VERSION set to 15.2.0-pre' do + let(:gitlab_version) { '15.2.0-pre' } + + context 'with unknown runner version' do + let(:runner_version) { '14.11.0~beta.29.gd0c550e3' } - with_them do - it 'returns symbol representing expected upgrade status' do - is_expected.to be_a(Symbol) - is_expected.to eq(expected_result) + it 'recommends 15.1.0 since 14.11 is an unknown release and 15.1.0 is available' do + is_expected.to eq({ recommended: Gitlab::VersionInfo.new(15, 1, 0) }) end end end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index e0f5531f370..35d44281072 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -7,9 +7,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let(:stage) do - build(:ci_stage, pipeline: pipeline, name: 'test') - end + let(:stage) { create(:ci_stage, pipeline: pipeline) } subject do described_class.new(stage, user) @@ -26,11 +24,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do context 'when stage has a core status' do (Ci::HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status| context "when core status is #{core_status}" do - before do - create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) - create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) - create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed) - end + let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } it "fabricates a core status #{core_status}" do expect(status).to be_a( @@ -48,12 +42,12 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do context 'when stage has warnings' do let(:stage) do - build(:ci_stage, name: 'test', status: :success, pipeline: pipeline) + create(:ci_stage, status: :success, pipeline: pipeline) end before do create(:ci_build, :allowed_to_fail, :failed, - stage: 'test', pipeline: stage.pipeline) + stage_id: stage.id, pipeline: stage.pipeline) end it 'fabricates extended "success with warnings" status' do @@ -70,11 +64,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do context 'when stage has manual builds' do (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| context "when status is #{core_status}" do - before do - create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) - create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) - create(:ci_build, pipeline: pipeline, stage: 'build', status: :manual) - end + let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } it 'fabricates a play manual status' do expect(status).to be_a(Gitlab::Ci::Status::Stage::PlayManual) 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 25b79ff2099..9fdaddc083e 100644 --- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do end describe '#action_path' do - let(:stage) { create(:ci_stage_entity, status: 'manual') } + let(:stage) { create(:ci_stage, status: 'manual') } let(:pipeline) { stage.pipeline } let(:play_manual) { stage.detailed_status(create(:user)) } @@ -46,25 +46,25 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do subject { described_class.matches?(stage, user) } context 'when stage is skipped' do - let(:stage) { create(:ci_stage_entity, status: :skipped) } + let(:stage) { create(:ci_stage, status: :skipped) } it { is_expected.to be_truthy } end context 'when stage is manual' do - let(:stage) { create(:ci_stage_entity, status: :manual) } + let(:stage) { create(:ci_stage, status: :manual) } it { is_expected.to be_truthy } end context 'when stage is scheduled' do - let(:stage) { create(:ci_stage_entity, status: :scheduled) } + let(:stage) { create(:ci_stage, status: :scheduled) } it { is_expected.to be_truthy } end context 'when stage is success' do - let(:stage) { create(:ci_stage_entity, status: :success) } + let(:stage) { create(:ci_stage, status: :success) } context 'and does not have manual builds' do it { is_expected.to be_falsy } diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb index 6c4f69fb036..5ab859241c6 100644 --- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb +++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do let(:error_message) do <<~MESSAGE A mechanism depending on internals of 'act-as-taggable-on` has been designed - to bulk insert tags for Ci::Build records. + to bulk insert tags for Ci::Build/Ci::Runner records. Please review the code carefully before updating the gem version https://gitlab.com/gitlab-org/gitlab/-/issues/350053 MESSAGE @@ -27,6 +27,21 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do it { expect(ActsAsTaggableOn::VERSION).to eq(acceptable_version), error_message } end + describe '.bulk_insert_tags!' do + let(:inserter) { instance_double(described_class) } + + it 'delegates to bulk insert class' do + expect(Gitlab::Ci::Tags::BulkInsert) + .to receive(:new) + .with(statuses) + .and_return(inserter) + + expect(inserter).to receive(:insert!) + + described_class.bulk_insert_tags!(statuses) + end + end + describe '#insert!' do context 'without tags' do it { expect(service.insert!).to be_falsey } @@ -44,6 +59,50 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do expect(job.reload.tag_list).to match_array(%w[tag1 tag2]) expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4]) end + + it 'persists taggings' do + service.insert! + + expect(job.taggings.size).to eq(2) + expect(other_job.taggings.size).to eq(3) + + expect(Ci::Build.tagged_with('tag1')).to include(job) + expect(Ci::Build.tagged_with('tag2')).to include(job, other_job) + expect(Ci::Build.tagged_with('tag3')).to include(other_job) + end + + it 'strips tags' do + job.tag_list = [' taga', 'tagb ', ' tagc '] + + service.insert! + expect(job.tags.map(&:name)).to match_array(%w[taga tagb tagc]) + end + + context 'when batching inserts for tags' do + before do + stub_const("#{described_class}::TAGS_BATCH_SIZE", 2) + end + + it 'inserts tags in batches' do + recorder = ActiveRecord::QueryRecorder.new { service.insert! } + count = recorder.log.count { |query| query.include?('INSERT INTO "tags"') } + + expect(count).to eq(2) + end + end + + context 'when batching inserts for taggings' do + before do + stub_const("#{described_class}::TAGGINGS_BATCH_SIZE", 2) + end + + it 'inserts taggings in batches' do + recorder = ActiveRecord::QueryRecorder.new { service.insert! } + count = recorder.log.count { |query| query.include?('INSERT INTO "taggings"') } + + expect(count).to eq(3) + end + end end context 'with tags for only one job' do @@ -57,6 +116,15 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do expect(job.reload.tag_list).to match_array(%w[tag1 tag2]) expect(other_job.reload.tag_list).to be_empty end + + it 'persists taggings' do + service.insert! + + expect(job.taggings.size).to eq(2) + + expect(Ci::Build.tagged_with('tag1')).to include(job) + expect(Ci::Build.tagged_with('tag2')).to include(job) + end end end end diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb index 27de8324206..65fd2b016ac 100644 --- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb @@ -34,6 +34,16 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do expect(build_names).to include('production_ecs') end + context 'when the DAST template is also included' do + let(:dast_template) { Gitlab::Template::GitlabCiYmlTemplate.find('Security/DAST') } + + before do + stub_ci_pipeline_yaml_file(template.content + dast_template.content) + end + + include_examples 'no pipeline yaml error' + end + context 'when running a pipeline for a branch' do let(:pipeline_branch) { 'test_branch' } diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index b0704ad7f50..8ec0846bdca 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -166,9 +166,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder do allow(builder).to receive(:secret_instance_variables) { [var('J', 10), var('K', 10)] } allow(builder).to receive(:secret_group_variables) { [var('K', 11), var('L', 11)] } allow(builder).to receive(:secret_project_variables) { [var('L', 12), var('M', 12)] } - allow(job).to receive(:trigger_request) { double(user_variables: [var('M', 13), var('N', 13)]) } - allow(pipeline).to receive(:variables) { [var('N', 14), var('O', 14)] } - allow(pipeline).to receive(:pipeline_schedule) { double(job_variables: [var('O', 15), var('P', 15)]) } + allow(pipeline).to receive(:variables) { [var('M', 13), var('N', 13)] } + allow(pipeline).to receive(:pipeline_schedule) { double(job_variables: [var('N', 14), var('O', 14)]) } end it 'returns variables in order depending on resource hierarchy' do @@ -185,8 +184,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do var('K', 11), var('L', 11), var('L', 12), var('M', 12), var('M', 13), var('N', 13), - var('N', 14), var('O', 14), - var('O', 15), var('P', 15)]) + var('N', 14), var('O', 14)]) end it 'overrides duplicate keys depending on resource hierarchy' do @@ -198,7 +196,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do 'I' => '9', 'J' => '10', 'K' => '11', 'L' => '12', 'M' => '13', 'N' => '14', - 'O' => '15', 'P' => '15') + 'O' => '14') end end diff --git a/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb new file mode 100644 index 00000000000..0bd9563d191 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor/feature_flags_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::YamlProcessor::FeatureFlags do + let(:feature_flag) { :my_feature_flag } + + context 'when the actor is set' do + let(:actor) { double } + let(:another_actor) { double } + + it 'checks the feature flag using the given actor' do + described_class.with_actor(actor) do + expect(Feature).to receive(:enabled?).with(feature_flag, actor) + + described_class.enabled?(feature_flag) + end + end + + it 'returns the value of the block' do + result = described_class.with_actor(actor) do + :test + end + + expect(result).to eq(:test) + end + + it 'restores the existing actor if any' do + described_class.with_actor(actor) do + described_class.with_actor(another_actor) do + expect(Feature).to receive(:enabled?).with(feature_flag, another_actor) + + described_class.enabled?(feature_flag) + end + + expect(Feature).to receive(:enabled?).with(feature_flag, actor) + described_class.enabled?(feature_flag) + end + end + + it 'restores the actor to nil after the block' do + described_class.with_actor(actor) do + expect(Thread.current[described_class::ACTOR_KEY]).to eq(actor) + end + + expect(Thread.current[described_class::ACTOR_KEY]).to be nil + end + end + + context 'when feature flag is checked outside the "with_actor" block' do + it 'raises an error on dev/test environment' do + expect { described_class.enabled?(feature_flag) }.to raise_error(described_class::NoActorError) + end + + context 'when on production' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + it 'checks the feature flag without actor' do + expect(Feature).to receive(:enabled?).with(feature_flag, nil) + expect(Gitlab::ErrorTracking) + .to receive(:track_and_raise_for_dev_exception) + .and_call_original + + described_class.enabled?(feature_flag) + end + end + end + + context 'when actor is explicitly nil' do + it 'checks the feature flag without actor' do + described_class.with_actor(nil) do + expect(Feature).to receive(:enabled?).with(feature_flag, nil) + + described_class.enabled?(feature_flag) + 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 3dd9ca35881..22bc6b0db59 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -70,7 +70,7 @@ module Gitlab options: { script: ['rspec'] }, rules: [ { if: '$CI_COMMIT_REF_NAME == "master"' }, - { changes: %w[README.md] } + { changes: { paths: %w[README.md] } } ], allow_failure: false, when: 'on_success', @@ -980,7 +980,7 @@ module Gitlab it { is_expected.to be_valid } - it "returns image and service when defined" do + it "returns with image" do expect(processor.stage_builds_attributes("test")).to contain_exactly({ stage: "test", stage_idx: 2, @@ -1010,6 +1010,51 @@ module Gitlab end end end + + context 'when a service has pull_policy' do + let(:config) do + <<~YAML + services: + - name: postgres:11.9 + pull_policy: if-not-present + + test: + script: exit 0 + YAML + end + + it { is_expected.to be_valid } + + it "returns with service" do + expect(processor.stage_builds_attributes("test")).to contain_exactly({ + stage: "test", + stage_idx: 2, + name: "test", + only: { refs: %w[branches tags] }, + options: { + script: ["exit 0"], + services: [{ name: "postgres:11.9", pull_policy: ["if-not-present"] }] + }, + allow_failure: false, + when: "on_success", + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :stage + }) + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it { is_expected.not_to be_valid } + + it "returns no job" do + expect(processor.jobs).to eq({}) + end + end + end end describe 'Variables' do @@ -2848,6 +2893,51 @@ module Gitlab end end + describe 'Rules' do + context 'changes' do + let(:config) do + <<~YAML + rspec: + script: exit 0 + rules: + - changes: [README.md] + YAML + end + + it 'returns builds with correct rules' do + expect(processor.builds.size).to eq(1) + expect(processor.builds[0]).to match( + hash_including( + name: "rspec", + rules: [{ changes: { paths: ["README.md"] } }] + ) + ) + end + + context 'with paths' do + let(:config) do + <<~YAML + rspec: + script: exit 0 + rules: + - changes: + paths: [README.md] + YAML + end + + it 'returns builds with correct rules' do + expect(processor.builds.size).to eq(1) + expect(processor.builds[0]).to match( + hash_including( + name: "rspec", + rules: [{ changes: { paths: ["README.md"] } }] + ) + ) + end + end + end + end + describe '#execute' do subject { Gitlab::Ci::YamlProcessor.new(content).execute } 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 109e83be294..616fe15c1a6 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -92,11 +92,11 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do context 'when sentry is configured' do before do stub_sentry_settings - stub_config_setting(host: 'example.com') + stub_config_setting(host: 'gitlab.example.com') end it 'adds sentry path to CSP without user' do - expect(directives['connect_src']).to eq("'self' ws://example.com dummy://example.com/43") + expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://example.com") end end @@ -146,7 +146,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do let(:snowplow_micro_url) { "http://#{snowplow_micro_hostname}/" } before do - stub_env('SNOWPLOW_MICRO_ENABLE', 1) + stub_config(snowplow_micro: { enabled: true }) allow(Gitlab::Tracking).to receive(:collector_hostname).and_return(snowplow_micro_hostname) end @@ -169,9 +169,9 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url)) end - context 'when not enabled using ENV[SNOWPLOW_MICRO_ENABLE]' do + context 'when not enabled using config' do before do - stub_env('SNOWPLOW_MICRO_ENABLE', nil) + stub_config(snowplow_micro: { enabled: false }) end it 'does not add Snowplow Micro URL to connect-src' do @@ -220,10 +220,11 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do expect(policy.directives['base-uri']).to be_nil end - it 'returns default values for directives not defined by the user' do + it 'returns default values for directives not defined by the user or with <default_value> and disables directives set to false' do # Explicitly disabling script_src and setting report_uri csp_config[:directives] = { script_src: false, + style_src: '<default_value>', report_uri: 'https://example.org' } diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index e8fe80f75cb..8ee57542d43 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do it 'returns the object kind for a deployment' do deployment = build(:deployment, deployable: nil, environment: create(:environment)) - data = described_class.build(deployment, Time.current) + data = described_class.build(deployment, 'success', Time.current) expect(data[:object_kind]).to eq('deployment') end @@ -23,7 +23,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do expected_commit_url = Gitlab::UrlBuilder.build(commit) status_changed_at = Time.current - data = described_class.build(deployment, status_changed_at) + data = described_class.build(deployment, 'failed', status_changed_at) expect(data[:status]).to eq('failed') expect(data[:status_changed_at]).to eq(status_changed_at) @@ -42,7 +42,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do it 'does not include the deployable URL when there is no deployable' do deployment = create(:deployment, status: :failed, deployable: nil) - data = described_class.build(deployment, Time.current) + data = described_class.build(deployment, 'failed', Time.current) expect(data[:deployable_url]).to be_nil end @@ -51,7 +51,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do let_it_be(:project) { create(:project, :repository) } let_it_be(:deployment) { create(:deployment, project: project) } - subject(:data) { described_class.build(deployment, Time.current) } + subject(:data) { described_class.build(deployment, 'created', Time.current) } before(:all) do project.repository.remove @@ -69,7 +69,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do context 'when deployed_by is nil' do let_it_be(:deployment) { create(:deployment, user: nil, deployable: nil) } - subject(:data) { described_class.build(deployment, Time.current) } + subject(:data) { described_class.build(deployment, 'created', Time.current) } before(:all) do deployment.user = nil diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index c2bd20798f1..469812c80fc 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do expect(build_data).to be_a(Hash) expect(build_data[:id]).to eq(build.id) expect(build_data[:status]).to eq(build.status) + expect(build_data[:failure_reason]).to be_nil expect(build_data[:allow_failure]).to eq(build.allow_failure) expect(build_data[:environment]).to be_nil expect(runner_data).to eq(nil) @@ -197,4 +198,14 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do end end end + + describe '.build failed' do + let(:build) { create(:ci_build, :failed, pipeline: pipeline, failure_reason: :script_failure) } + let(:data) { described_class.build(pipeline) } + let(:build_data) { data[:builds].last } + + it 'has failure_reason' do + expect(build_data[:failure_reason]).to eq(build.failure_reason) + end + end end diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb index c39f6a78e93..a7b3670da7c 100644 --- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb @@ -220,6 +220,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d expect(described_class.created_since(fixed_time)).to contain_exactly(stuck_job, failed_job, max_attempts_failed_job) end end + + describe '.blocked_by_max_attempts' do + it 'returns blocked jobs' do + expect(described_class.blocked_by_max_attempts).to contain_exactly(max_attempts_failed_job) + end + end end describe 'delegated batched_migration attributes' do diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb index 97459d4a7be..b8ff78be333 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb @@ -14,6 +14,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end end + before do + allow(Gitlab::Database::BackgroundMigration::HealthStatus).to receive(:evaluate) + .and_return(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal) + end + describe '#run_migration_job' do shared_examples_for 'it has completed the migration' do it 'does not create and run a migration job' do @@ -59,13 +64,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do sub_batch_size: migration.sub_batch_size) end - it 'optimizes the migration after executing the job' do - migration.update!(min_value: event1.id, max_value: event2.id) + context 'migration health' do + let(:health_status) { Gitlab::Database::BackgroundMigration::HealthStatus } + let(:stop_signal) { health_status::Signals::Stop.new(:indicator, reason: 'Take a break') } + let(:normal_signal) { health_status::Signals::Normal.new(:indicator, reason: 'All good') } + let(:not_available_signal) { health_status::Signals::NotAvailable.new(:indicator, reason: 'Indicator is disabled') } + let(:unknown_signal) { health_status::Signals::Unknown.new(:indicator, reason: 'Something went wrong') } - expect(migration_wrapper).to receive(:perform).ordered - expect(migration).to receive(:optimize!).ordered + before do + migration.update!(min_value: event1.id, max_value: event2.id) + expect(migration_wrapper).to receive(:perform) + end - runner.run_migration_job(migration) + it 'puts migration on hold on stop signal' do + expect(health_status).to receive(:evaluate).and_return(stop_signal) + + expect { runner.run_migration_job(migration) }.to change { migration.on_hold? } + .from(false).to(true) + end + + it 'optimizes migration on normal signal' do + expect(health_status).to receive(:evaluate).and_return(normal_signal) + + expect(migration).to receive(:optimize!) + + expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? } + end + + it 'optimizes migration on no signal' do + expect(health_status).to receive(:evaluate).and_return(not_available_signal) + + expect(migration).to receive(:optimize!) + + expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? } + end + + it 'optimizes migration on unknown signal' do + expect(health_status).to receive(:evaluate).and_return(unknown_signal) + + expect(migration).to receive(:optimize!) + + expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? } + end end end @@ -362,6 +402,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do .with(gitlab_schemas, 'CopyColumnUsingBackgroundMigrationJob', table_name, column_name, job_arguments) .and_return(batched_migration) + expect(batched_migration).to receive(:reset_attempts_of_blocked_jobs!).and_call_original + expect(batched_migration).to receive(:finalize!).and_call_original expect do @@ -380,8 +422,15 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end context 'when migration fails to complete' do + let(:error_message) do + "Batched migration #{batched_migration.job_class_name} could not be completed and a manual action is required."\ + "Check the admin panel at (`/admin/background_migrations`) for more details." + end + it 'raises an error' do - batched_migration.batched_jobs.with_status(:failed).update_all(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS) + allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:find_for_configuration).and_return(batched_migration) + + allow(batched_migration).to receive(:finished?).and_return(false) expect do runner.finalize( @@ -390,7 +439,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do column_name, job_arguments ) - end.to raise_error described_class::FailedToFinalize + end.to raise_error(described_class::FailedToFinalize, error_message) end end end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb index 8819171cfd0..55f607c0cb0 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -157,6 +157,27 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '#reset_attempts_of_blocked_jobs!' do + let!(:migration) { create(:batched_background_migration) } + let(:max_attempts) { Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS } + + before do + create(:batched_background_migration_job, attempts: max_attempts - 1, batched_migration: migration) + create(:batched_background_migration_job, attempts: max_attempts + 1, batched_migration: migration) + create(:batched_background_migration_job, attempts: max_attempts + 1, batched_migration: migration) + end + + it 'sets the number of attempts to zero for blocked jobs' do + migration.reset_attempts_of_blocked_jobs! + + expect(migration.batched_jobs.size).to eq(3) + + migration.batched_jobs.blocked_by_max_attempts.each do |job| + expect(job.attempts).to be_zero + end + end + end + describe '#interval_elapsed?' do context 'when the migration has no last_job' do let(:batched_migration) { build(:batched_background_migration) } @@ -322,6 +343,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m describe '#retry_failed_jobs!' do let(:batched_migration) { create(:batched_background_migration, status: 'failed') } + let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob } subject(:retry_failed_jobs) { batched_migration.retry_failed_jobs! } @@ -335,7 +357,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m anything, batch_min_value: 6, batch_size: 5, - job_arguments: batched_migration.job_arguments + job_arguments: batched_migration.job_arguments, + job_class: job_class ).and_return([6, 10]) end end @@ -570,6 +593,30 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '#on_hold?', :freeze_time do + subject { migration.on_hold? } + + let(:migration) { create(:batched_background_migration) } + + it 'returns false if no on_hold_until is set' do + migration.on_hold_until = nil + + expect(subject).to be_falsey + end + + it 'returns false if on_hold_until has passed' do + migration.on_hold_until = 1.minute.ago + + expect(subject).to be_falsey + end + + it 'returns true if on_hold_until is in the future' do + migration.on_hold_until = 1.minute.from_now + + expect(subject).to be_truthy + end + end + describe '.for_configuration' do let!(:attributes) do { diff --git a/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb b/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb new file mode 100644 index 00000000000..21204814f17 --- /dev/null +++ b/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::AutovacuumActiveOnTable do + include Database::DatabaseHelpers + + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + around do |example| + Gitlab::Database::SharedModel.using_connection(connection) do + example.run + end + end + + describe '#evaluate' do + subject { described_class.new(context).evaluate } + + before do + swapout_view_for_table(:postgres_autovacuum_activity) + end + + let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(tables) } + let(:tables) { [table] } + let(:table) { 'users' } + + context 'without autovacuum activity' do + it 'returns Normal signal' do + expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal) + end + + it 'remembers the indicator class' do + expect(subject.indicator_class).to eq(described_class) + end + end + + context 'with autovacuum activity' do + before do + create(:postgres_autovacuum_activity, table: table, table_identifier: "public.#{table}") + end + + it 'returns Stop signal' do + expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Stop) + end + + it 'explains why' do + expect(subject.reason).to include('autovacuum running on: table public.users') + end + + it 'remembers the indicator class' do + expect(subject.indicator_class).to eq(described_class) + end + + it 'returns NoSignal signal in case the feature flag is disabled' do + stub_feature_flags(batched_migrations_health_status_autovacuum: false) + + expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::NotAvailable) + end + end + end +end diff --git a/spec/lib/gitlab/database/background_migration/health_status_spec.rb b/spec/lib/gitlab/database/background_migration/health_status_spec.rb new file mode 100644 index 00000000000..6d0430dcbbb --- /dev/null +++ b/spec/lib/gitlab/database/background_migration/health_status_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + around do |example| + Gitlab::Database::SharedModel.using_connection(connection) do + example.run + end + end + + describe '.evaluate' do + subject(:evaluate) { described_class.evaluate(migration, indicator_class) } + + let(:migration) { build(:batched_background_migration, :active) } + + let(:health_status) { 'Gitlab::Database::BackgroundMigration::HealthStatus' } + let(:indicator_class) { class_double("#{health_status}::Indicators::AutovacuumActiveOnTable") } + let(:indicator) { instance_double("#{health_status}::Indicators::AutovacuumActiveOnTable") } + + before do + allow(indicator_class).to receive(:new).with(migration.health_context).and_return(indicator) + end + + it 'returns a signal' do + signal = instance_double("#{health_status}::Signals::Normal", log_info?: false) + + expect(indicator).to receive(:evaluate).and_return(signal) + + expect(evaluate).to eq(signal) + end + + it 'logs interesting signals' do + signal = instance_double("#{health_status}::Signals::Stop", log_info?: true) + + expect(indicator).to receive(:evaluate).and_return(signal) + expect(described_class).to receive(:log_signal).with(signal, migration) + + evaluate + end + + it 'does not log signals of no interest' do + signal = instance_double("#{health_status}::Signals::Normal", log_info?: false) + + expect(indicator).to receive(:evaluate).and_return(signal) + expect(described_class).not_to receive(:log_signal) + + evaluate + end + + context 'on indicator error' do + let(:error) { RuntimeError.new('everything broken') } + + before do + expect(indicator).to receive(:evaluate).and_raise(error) + end + + it 'does not fail' do + expect { evaluate }.not_to raise_error + end + + it 'returns Unknown signal' do + expect(evaluate).to be_an_instance_of(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Unknown) + expect(evaluate.reason).to eq("unexpected error: everything broken (RuntimeError)") + end + + it 'reports the exception to error tracking' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(error, migration_id: migration.id, job_class_name: migration.job_class_name) + + evaluate + end + end + end +end diff --git a/spec/lib/gitlab/database/each_database_spec.rb b/spec/lib/gitlab/database/each_database_spec.rb index 8345cdfb8fb..2a6eb8f779d 100644 --- a/spec/lib/gitlab/database/each_database_spec.rb +++ b/spec/lib/gitlab/database/each_database_spec.rb @@ -4,9 +4,10 @@ require 'spec_helper' RSpec.describe Gitlab::Database::EachDatabase do describe '.each_database_connection', :add_ci_connection do + let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access } + before do - allow(Gitlab::Database).to receive(:database_base_models) - .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access) + allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared).and_return(database_base_models) end it 'yields each connection after connecting SharedModel' do @@ -60,12 +61,20 @@ RSpec.describe Gitlab::Database::EachDatabase do end context 'when shared connections are not included' do + def clear_memoization(key) + Gitlab::Database.remove_instance_variable(key) if Gitlab::Database.instance_variable_defined?(key) + end + + before do + allow(Gitlab::Database).to receive(:database_base_models).and_return(database_base_models) + + # Clear the memoization because the return of Gitlab::Database#schemas_to_base_models depends stubbed value + clear_memoization(:@schemas_to_base_models) + clear_memoization(:@schemas_to_base_models_ee) + end + it 'only yields the unshared connections' do - if Gitlab::Database.has_config?(:ci) - expect(Gitlab::Database).to receive(:db_config_share_with).exactly(3).times.and_return(nil, 'main', 'main') - else - expect(Gitlab::Database).to receive(:db_config_share_with).twice.and_return(nil, 'main') - end + expect(Gitlab::Database).to receive(:db_config_share_with).exactly(3).times.and_return(nil, 'main', 'main') expect { |b| described_class.each_database_connection(include_shared: false, &b) } .to yield_successive_args([ActiveRecord::Base.connection, 'main']) @@ -79,7 +88,7 @@ RSpec.describe Gitlab::Database::EachDatabase do let(:model2) { Class.new(Gitlab::Database::SharedModel) } before do - allow(Gitlab::Database).to receive(:database_base_models) + allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared) .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access) end @@ -136,7 +145,7 @@ RSpec.describe Gitlab::Database::EachDatabase do let(:ci_model) { Class.new(Ci::ApplicationRecord) } before do - allow(Gitlab::Database).to receive(:database_base_models) + allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared) .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access) allow(main_model).to receive_message_chain('connection_db_config.name').and_return('main') diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb index 611b2fbad72..72950895022 100644 --- a/spec/lib/gitlab/database/gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb @@ -3,26 +3,27 @@ require 'spec_helper' RSpec.describe Gitlab::Database::GitlabSchema do describe '.tables_to_schema' do - subject { described_class.tables_to_schema } - it 'all tables have assigned a known gitlab_schema' do - is_expected.to all( - match([be_a(String), be_in([:gitlab_internal, :gitlab_shared, :gitlab_main, :gitlab_ci])]) + expect(described_class.tables_to_schema).to all( + match([be_a(String), be_in(Gitlab::Database.schemas_to_base_models.keys.map(&:to_sym))]) ) end # This being run across different databases indirectly also tests # a general consistency of structure across databases - Gitlab::Database.database_base_models.each do |db_config_name, db_class| - let(:db_data_sources) { db_class.connection.data_sources } - + Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }.each do |db_config_name, db_class| context "for #{db_config_name} using #{db_class}" do + let(:db_data_sources) { db_class.connection.data_sources } + + # The Geo database does not share the same structure as all decomposed databases + subject { described_class.tables_to_schema.select { |_, v| v != :gitlab_geo } } + it 'new data sources are added' do missing_tables = db_data_sources.to_set - subject.keys expect(missing_tables).to be_empty, \ "Missing table(s) #{missing_tables.to_a} not found in #{described_class}.tables_to_schema. " \ - "Any new tables must be added to lib/gitlab/database/gitlab_schemas.yml." + "Any new tables must be added to #{described_class::GITLAB_SCHEMAS_FILE}." end it 'non-existing data sources are removed' do @@ -30,7 +31,7 @@ RSpec.describe Gitlab::Database::GitlabSchema do expect(extra_tables).to be_empty, \ "Extra table(s) #{extra_tables.to_a} found in #{described_class}.tables_to_schema. " \ - "Any removed or renamed tables must be removed from lib/gitlab/database/gitlab_schemas.yml." + "Any removed or renamed tables must be removed from #{described_class::GITLAB_SCHEMAS_FILE}." end end end diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb index ed11699e494..87a3e0f81e4 100644 --- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb @@ -63,19 +63,22 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do Gitlab::Database.schemas_to_base_models.fetch(parent_table_schema) end - it 'all `to_table` tables are present' do + it 'all `to_table` tables are present', :aggregate_failures do definitions.each do |definition| base_models_for(definition.to_table).each do |model| - expect(model.connection).to be_table_exist(definition.to_table) + expect(model.connection).to be_table_exist(definition.to_table), + "Table #{definition.from_table} does not exist" end end end - it 'all `from_table` tables are present' do + it 'all `from_table` tables are present', :aggregate_failures do definitions.each do |definition| base_models_for(definition.from_table).each do |model| - expect(model.connection).to be_table_exist(definition.from_table) - expect(model.connection).to be_column_exist(definition.from_table, definition.column) + expect(model.connection).to be_table_exist(definition.from_table), + "Table #{definition.from_table} does not exist" + expect(model.connection).to be_column_exist(definition.from_table, definition.column), + "Column #{definition.column} in #{definition.from_table} does not exist" end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index e09016b2b2b..3ccc3a17862 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2477,6 +2477,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do describe '#backfill_iids' do include MigrationsHelpers + let_it_be(:issue_base_type_enum) { 0 } + let_it_be(:issue_type) { table(:work_item_types).find_by(base_type: issue_base_type_enum) } + let(:issue_class) do Class.new(ActiveRecord::Base) do include AtomicInternalId @@ -2490,6 +2493,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do scope: :project, init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) }, presence: false + + before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id } end end @@ -2515,7 +2520,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'generates iids properly for models created after the migration when iids are backfilled' do project = setup - issue_a = issues.create!(project_id: project.id) + issue_a = issues.create!(project_id: project.id, work_item_type_id: issue_type.id) model.backfill_iids('issues') @@ -2528,14 +2533,14 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'generates iids properly for models created after the migration across multiple projects' do project_a = setup project_b = setup - issues.create!(project_id: project_a.id) - issues.create!(project_id: project_b.id) - issues.create!(project_id: project_b.id) + issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id) + issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id) + issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id) model.backfill_iids('issues') - issue_a = issue_class.create!(project_id: project_a.id) - issue_b = issue_class.create!(project_id: project_b.id) + issue_a = issue_class.create!(project_id: project_a.id, work_item_type_id: issue_type.id) + issue_b = issue_class.create!(project_id: project_b.id, work_item_type_id: issue_type.id) expect(issue_a.iid).to eq(2) expect(issue_b.iid).to eq(3) @@ -2545,7 +2550,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'generates an iid' do project_a = setup project_b = setup - issue_a = issues.create!(project_id: project_a.id) + issue_a = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id) model.backfill_iids('issues') @@ -2559,8 +2564,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do context 'when a row already has an iid set in the database' do it 'backfills iids' do project = setup - issue_a = issues.create!(project_id: project.id, iid: 1) - issue_b = issues.create!(project_id: project.id, iid: 2) + issue_a = issues.create!(project_id: project.id, work_item_type_id: issue_type.id, iid: 1) + issue_b = issues.create!(project_id: project.id, work_item_type_id: issue_type.id, iid: 2) model.backfill_iids('issues') @@ -2571,9 +2576,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'backfills for multiple projects' do project_a = setup project_b = setup - issue_a = issues.create!(project_id: project_a.id, iid: 1) - issue_b = issues.create!(project_id: project_b.id, iid: 1) - issue_c = issues.create!(project_id: project_a.id, iid: 2) + issue_a = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id, iid: 1) + issue_b = issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id, iid: 1) + issue_c = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id, iid: 2) model.backfill_iids('issues') 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 f3414727245..5bfb2516ba1 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 @@ -173,17 +173,6 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_finished end - - context 'when within transaction' do - before do - allow(migration).to receive(:transaction_open?).and_return(true) - end - - it 'does raise an exception' do - expect { migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes)} - .to raise_error /`queue_batched_background_migration` cannot be run inside a transaction./ - end - end end end @@ -301,12 +290,8 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end describe '#delete_batched_background_migration' do - let(:transaction_open) { false } - before do expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!) - - allow(migration).to receive(:transaction_open?).and_return(transaction_open) end context 'when migration exists' do @@ -360,15 +345,6 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } end end - - context 'when within transaction' do - let(:transaction_open) { true } - - it 'raises an exception' do - expect { migration.delete_batched_background_migration('MyJobClass', :projects, :id, [[:id], [:id_convert_to_bigint]]) } - .to raise_error /`#delete_batched_background_migration` cannot be run inside a transaction./ - end - end end describe '#gitlab_schema_from_context' do diff --git a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb index d197f39be40..c6327de98d1 100644 --- a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb +++ b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do end describe '#with_restored_connection_stack' do - Gitlab::Database.database_base_models.each do |db_config_name, _| + Gitlab::Database.database_base_models_with_gitlab_shared.each do |db_config_name, _| context db_config_name do it_behaves_like "reconfigures connection stack", db_config_name do it 'does restore connection hierarchy' do diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb index 2f3d44f6f8f..f1f72d71e1a 100644 --- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb @@ -68,10 +68,10 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez end context 'with multiple jobs to run' do - it 'runs all jobs created within the last 48 hours' do + it 'runs all jobs created within the last 3 hours' do old_migration = define_background_migration(migration_name) - travel 3.days + travel 4.hours new_migration = define_background_migration('NewMigration') { travel 1.second } migration.queue_batched_background_migration('NewMigration', table_name, :id, diff --git a/spec/lib/gitlab/database/postgres_autovacuum_activity_spec.rb b/spec/lib/gitlab/database/postgres_autovacuum_activity_spec.rb new file mode 100644 index 00000000000..c1ac8f0c9cd --- /dev/null +++ b/spec/lib/gitlab/database/postgres_autovacuum_activity_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresAutovacuumActivity, type: :model do + include Database::DatabaseHelpers + + it { is_expected.to be_a Gitlab::Database::SharedModel } + + describe '.for_tables' do + subject { described_class.for_tables(tables) } + + let(:tables) { %w[foo test] } + + before do + swapout_view_for_table(:postgres_autovacuum_activity) + + # unrelated + create(:postgres_autovacuum_activity, table: 'bar') + + tables.each do |table| + create(:postgres_autovacuum_activity, table: table) + end + + expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary).and_yield + end + + it 'returns autovacuum activity for queries tables' do + expect(subject.map(&:table).sort).to eq(tables) + end + end +end diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index 0c576505e07..976b9896dfa 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Database::Reindexing do include Database::DatabaseHelpers describe '.invoke' do - let(:databases) { Gitlab::Database.database_base_models } + let(:databases) { Gitlab::Database.database_base_models_with_gitlab_shared } let(:databases_count) { databases.count } it 'cleans up any leftover indexes' do diff --git a/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb b/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb index e676e5fe034..68c29bad287 100644 --- a/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb +++ b/spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb @@ -38,7 +38,11 @@ RSpec.describe Gitlab::DatabaseImporters::InstanceAdministrators::CreateGroup do end end - context 'with application settings and admin users', :do_not_mock_admin_mode_setting do + context( + 'with application settings and admin users', + :do_not_mock_admin_mode_setting, + :do_not_stub_snowplow_by_default + ) do let(:group) { result[:group] } let(:application_setting) { Gitlab::CurrentSettings.current_application_settings } @@ -109,7 +113,7 @@ RSpec.describe Gitlab::DatabaseImporters::InstanceAdministrators::CreateGroup do admin2 = create(:user, :admin) existing_group.add_owner(user) - existing_group.add_users([admin1, admin2], Gitlab::Access::MAINTAINER) + existing_group.add_members([admin1, admin2], Gitlab::Access::MAINTAINER) application_setting.instance_administrators_group_id = existing_group.id end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 064613074cd..452a662bdcb 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -337,7 +337,7 @@ RSpec.describe Gitlab::Database do let(:model2) { Class.new(base_model) } before do - allow(described_class).to receive(:database_base_models) + allow(described_class).to receive(:database_base_models_using_load_balancing) .and_return({ model1: model1, model2: model2 }.with_indifferent_access) end diff --git a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb index 678d4a90e8d..2811bc859da 100644 --- a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do it 'only converts valid links' do expect(subject).to eq( <<~CONTENT - <span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span> + <span><span>#{link('http://', url: nil)}</span><span>#{link('\n', url: nil)}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span> <span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span> CONTENT ) diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 34f4bdde3b5..28557aab830 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -129,6 +129,14 @@ RSpec.describe Gitlab::Diff::File do expect(diff_file.rendered).to be_kind_of(Gitlab::Diff::Rendered::Notebook::DiffFile) end + context 'when collapsed' do + it 'is nil' do + expect(diff).to receive(:collapsed?).and_return(true) + + expect(diff_file.rendered).to be_nil + end + end + context 'when too large' do it 'is nil' do expect(diff).to receive(:too_large?).and_return(true) diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb index 579776d44aa..73c0d0dba88 100644 --- a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb +++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb @@ -10,7 +10,6 @@ RSpec.describe Gitlab::Diff::Formatters::ImageFormatter do head_sha: 789, old_path: 'old_image.png', new_path: 'new_image.png', - file_identifier_hash: '777', position_type: 'image' } end diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb index b6bdc5ff493..290585d0991 100644 --- a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb +++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb @@ -10,7 +10,6 @@ RSpec.describe Gitlab::Diff::Formatters::TextFormatter do head_sha: 789, old_path: 'old_path.txt', new_path: 'new_path.txt', - file_identifier_hash: '777', line_range: nil } end diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index e643b58ee32..5350dda5fb2 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do - let(:merge_request) { create(:merge_request_with_diffs) } + let_it_be(:merge_request) { create(:merge_request_with_diffs) } + let(:diff_hash) do { ".gitignore-false-false-false" => [{ line_code: nil, rich_text: nil, text: "@@ -17,3 +17,4 @@ rerun.txt", type: "match", index: 0, old_pos: 17, new_pos: 17 }, @@ -229,10 +230,10 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end describe 'metrics' do - let(:transaction) { Gitlab::Metrics::WebTransaction.new({} ) } + let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) } before do - allow(cache).to receive(:current_transaction).and_return(transaction) + allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction) end it 'observes :gitlab_redis_diff_caching_memory_usage_bytes' do @@ -241,6 +242,18 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do cache.write_if_empty end + + it 'records hit ratio metrics' do + expect(transaction) + .to receive(:increment).with(:gitlab_redis_diff_caching_requests_total).exactly(5).times + expect(transaction) + .to receive(:increment).with(:gitlab_redis_diff_caching_hits_total).exactly(4).times + + 5.times do + cache = described_class.new(merge_request.diffs) + cache.write_if_empty + end + end end describe '#key' do diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index c9a20f40462..bb3522eb579 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -574,86 +574,6 @@ RSpec.describe Gitlab::Diff::Position do end end - describe '#find_diff_file_from' do - context "position for a diff file that has changed from symlink to regular file" do - let(:commit) { project.commit("81e6355ce4e1544a3524b230952c12455de0777b") } - - let(:old_symlink_file_identifier_hash) { "bfa430463f33619872d52a6b85ced59c973e42dc" } - let(:new_regular_file_identifier_hash) { "e25b60c2e5ffb977d2b1431b96c6f7800c3c3529" } - let(:file_identifier_hash) { new_regular_file_identifier_hash } - - let(:args) do - { - file_identifier_hash: file_identifier_hash, - old_path: "symlink", - new_path: "symlink", - old_line: nil, - new_line: 1, - diff_refs: commit.diff_refs - } - end - - let(:diffable) { commit.diff_refs.compare_in(project) } - - subject(:diff_file) { described_class.new(args).find_diff_file_from(diffable) } - - context 'when file_identifier_hash is disabled' do - before do - stub_feature_flags(file_identifier_hash: false) - end - - it "returns the first diff file" do - expect(diff_file.file_identifier_hash).to eq(old_symlink_file_identifier_hash) - end - end - - context 'when file_identifier_hash is enabled' do - before do - stub_feature_flags(file_identifier_hash: true) - end - - context 'for new regular file' do - it "returns the correct diff file" do - expect(diff_file.file_identifier_hash).to eq(new_regular_file_identifier_hash) - end - end - - context 'for old symlink file' do - let(:args) do - { - file_identifier_hash: old_symlink_file_identifier_hash, - old_path: "symlink", - new_path: "symlink", - old_line: 1, - new_line: nil, - diff_refs: commit.diff_refs - } - end - - it "returns the correct diff file" do - expect(diff_file.file_identifier_hash).to eq(old_symlink_file_identifier_hash) - end - end - - context 'when file_identifier_hash is missing' do - let(:file_identifier_hash) { nil } - - it "returns the first diff file" do - expect(diff_file.file_identifier_hash).to eq(old_symlink_file_identifier_hash) - end - end - - context 'when file_identifier_hash cannot be found' do - let(:file_identifier_hash) { "missingidentifier" } - - it "returns nil" do - expect(diff_file).to be_nil - end - end - end - end - end - describe '#==' do let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb index 1414056ad6a..563480d214b 100644 --- a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb @@ -234,118 +234,5 @@ RSpec.describe Gitlab::Diff::PositionTracer::ImageStrategy do end end end - - describe 'symlink scenarios' do - let(:new_file) { old_file_status == :new } - let(:deleted_file) { old_file_status == :deleted } - let(:renamed_file) { old_file_status == :renamed } - - let(:file_identifier) { "#{file_name}-#{new_file}-#{deleted_file}-#{renamed_file}" } - let(:file_identifier_hash) { Digest::SHA1.hexdigest(file_identifier) } - let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image', file_identifier_hash: file_identifier_hash) } - - let(:update_file_commit) do - initial_commit - - update_file( - branch_name, - file_name, - Base64.encode64('morecontent') - ) - end - - let(:delete_file_commit) do - initial_commit - - delete_file(branch_name, file_name) - end - - let(:create_second_file_commit) do - initial_commit - - create_file( - branch_name, - second_file_name, - Base64.encode64('morecontent') - ) - end - - before do - stub_feature_flags(file_identifier_hash: true) - end - - describe 'from symlink to image' do - let(:initial_commit) { project.commit('a19c7f9a147e35e535c797cf148d29c24dac5544') } - let(:symlink_to_image_commit) { project.commit('8cfca8420812e5bd7479aa32cf33e0c95a3ca576') } - let(:branch_name) { 'diff-files-symlink-to-image' } - let(:file_name) { 'symlink-to-image.png' } - - context "when the old position is on the new image file" do - let(:old_file_status) { :new } - - context "when the image file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: file_name - ) - end - end - - context "when the image file's content was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) } - let(:change_diff_refs) { diff_refs(symlink_to_image_commit, update_file_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name - ) - end - end - - context "when the image file was removed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(symlink_to_image_commit, delete_file_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name - ) - end - end - end - end - - describe 'from image to symlink' do - let(:initial_commit) { project.commit('d10dcdfbbb2b59a959a5f5d66a4adf28f0ea4008') } - let(:image_to_symlink_commit) { project.commit('3e94fdaa60da8aed38401b91bc56be70d54ca424') } - let(:branch_name) { 'diff-files-image-to-symlink' } - let(:file_name) { 'image-to-symlink.png' } - - context "when the old position is on the added image file" do - let(:old_file_status) { :new } - - context "when the image file gets changed to a symlink between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit.parent, initial_commit) } - let(:new_diff_refs) { diff_refs(initial_commit.parent, image_to_symlink_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, image_to_symlink_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name - ) - end - end - end - end - end end end diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb index ea56a87dec2..2b21084d8e5 100644 --- a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb @@ -1860,143 +1860,5 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c end end end - - describe 'symlink scenarios' do - let(:new_file) { old_file_status == :new } - let(:deleted_file) { old_file_status == :deleted } - let(:renamed_file) { old_file_status == :renamed } - - let(:file_identifier) { "#{file_name}-#{new_file}-#{deleted_file}-#{renamed_file}" } - let(:file_identifier_hash) { Digest::SHA1.hexdigest(file_identifier) } - - let(:update_line_commit) do - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - CONTENT - ) - end - - let(:delete_file_commit) do - delete_file(branch_name, file_name) - end - - let(:create_second_file_commit) do - create_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - E - CONTENT - ) - end - - before do - stub_feature_flags(file_identifier_hash: true) - end - - describe 'from symlink to text' do - let(:initial_commit) { project.commit('0e5b363105e9176a77bac94d7ff6d8c4fb35c3eb') } - let(:symlink_to_text_commit) { project.commit('689815e617abc6889f1fded4834d2dd7d942a58e') } - let(:branch_name) { 'diff-files-symlink-to-text' } - let(:file_name) { 'symlink-to-text.txt' } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 3, file_identifier_hash: file_identifier_hash) } - - before do - create_branch('diff-files-symlink-to-text-test', branch_name) - end - - context "when the old position is on the new text file" do - let(:old_file_status) { :new } - - context "when the text file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the text file's content has change, but the line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the text file's line was changed between the old and the new diff" do - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2, file_identifier_hash: file_identifier_hash) } - - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(symlink_to_text_commit, update_line_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the text file was removed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(symlink_to_text_commit, delete_file_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - - describe 'from text to symlink' do - let(:initial_commit) { project.commit('3db7bd90bab8ce8f02c9818590b84739a2e97230') } - let(:text_to_symlink_commit) { project.commit('5e2c2708c2e403dece5dd25759369150aac51644') } - let(:branch_name) { 'diff-files-text-to-symlink' } - let(:file_name) { 'text-to-symlink.txt' } - - context "when the position is on the added text file" do - let(:old_file_status) { :new } - - context "when the text file gets changed to a symlink between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit.parent, initial_commit) } - let(:new_diff_refs) { diff_refs(initial_commit.parent, text_to_symlink_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, text_to_symlink_commit) } - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - end end end diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb index cb046548880..42ab2d1d063 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFileHelper do where(:case, :transformed_blocks, :result) do 'if transformed diff is empty' | [] | 0 'if the transformed line does not map to any in the original file' | [{ source_line: nil }] | 0 - 'if the transformed line maps to a line in the source file' | [{ source_line: 2 }] | 3 + 'if the transformed line maps to a line in the source file' | [{ source_line: 3 }] | 3 end with_them do @@ -81,8 +81,8 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFileHelper do let(:blocks) do { - from: [0, 2, 1, nil, nil, 3].map { |i| { source_line: i } }, - to: [0, 1, nil, 2, nil, 3].map { |i| { source_line: i } } + from: [1, 3, 2, nil, nil, 4].map { |i| { source_line: i } }, + to: [1, 2, nil, 3, nil, 4].map { |i| { source_line: i } } } end diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb index c38684a6dc3..b5137f9db6b 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb @@ -144,7 +144,7 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do context 'has image' do it 'replaces rich text with img to the embedded image' do - expect(nb_file.highlighted_diff_lines[58].rich_text).to include('<img') + expect(nb_file.highlighted_diff_lines[56].rich_text).to include('<img') end it 'adds image to src' do @@ -159,11 +159,11 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do let(:commit) { project.commit("4963fefc990451a8ad34289ce266b757456fc88c") } it 'prevents injected html to be rendered as html' do - expect(nb_file.highlighted_diff_lines[45].rich_text).not_to include('<div>Hello') + expect(nb_file.highlighted_diff_lines[43].rich_text).not_to include('<div>Hello') end it 'keeps the injected html as part of the string' do - expect(nb_file.highlighted_diff_lines[45].rich_text).to end_with('/div>">') + expect(nb_file.highlighted_diff_lines[43].rich_text).to end_with('/div>">') end end end diff --git a/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb b/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb deleted file mode 100644 index f93c1aa1974..00000000000 --- a/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Elasticsearch::Logs::Lines do - let(:client) { Elasticsearch::Transport::Client } - - let(:es_message_1) { { timestamp: "2019-12-13T14:35:34.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [25/Oct/2019:08:03:22 UTC] \"GET / HTTP/1.1\" 200 13" } } - let(:es_message_2) { { timestamp: "2019-12-13T14:35:35.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [27/Oct/2019:23:49:54 UTC] \"GET / HTTP/1.1\" 200 13" } } - let(:es_message_3) { { timestamp: "2019-12-13T14:35:36.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [04/Nov/2019:23:09:24 UTC] \"GET / HTTP/1.1\" 200 13" } } - let(:es_message_4) { { timestamp: "2019-12-13T14:35:37.034Z", pod: "production-6866bc8974-m4sk4", message: "- -\u003e /" } } - - let(:es_response) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/logs_response.json')) } - - subject { described_class.new(client) } - - let(:namespace) { "autodevops-deploy-9-production" } - let(:pod_name) { "production-6866bc8974-m4sk4" } - let(:container_name) { "auto-deploy-app" } - let(:search) { "foo +bar "} - let(:start_time) { "2019-12-13T14:35:34.034Z" } - let(:end_time) { "2019-12-13T14:35:34.034Z" } - let(:cursor) { "9999934,1572449784442" } - - let(:body) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query.json')) } - let(:body_with_container) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_container.json')) } - let(:body_with_search) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_search.json')) } - let(:body_with_times) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_times.json')) } - let(:body_with_start_time) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_start_time.json')) } - let(:body_with_end_time) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_end_time.json')) } - let(:body_with_cursor) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_cursor.json')) } - let(:body_with_filebeat_6) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_filebeat_6.json')) } - - RSpec::Matchers.define :a_hash_equal_to_json do |expected| - match do |actual| - actual.as_json == expected - end - end - - describe '#pod_logs' do - it 'returns the logs as an array' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can further filter the logs by container name' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_container)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, container_name: container_name) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can further filter the logs by search' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_search)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, search: search) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can further filter the logs by start_time and end_time' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_times)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, start_time: start_time, end_time: end_time) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can further filter the logs by only start_time' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_start_time)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, start_time: start_time) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can further filter the logs by only end_time' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_end_time)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, end_time: end_time) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can search after a cursor' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_cursor)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, cursor: cursor) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - - it 'can search on filebeat 6' do - expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_filebeat_6)).and_return(es_response) - - result = subject.pod_logs(namespace, pod_name: pod_name, chart_above_v2: false) - expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor) - end - end -end diff --git a/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb b/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb deleted file mode 100644 index 07fa0980d36..00000000000 --- a/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Elasticsearch::Logs::Pods do - let(:client) { Elasticsearch::Transport::Client } - - let(:es_query) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/pods_query.json'), symbolize_names: true) } - let(:es_response) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/pods_response.json')) } - let(:namespace) { "autodevops-deploy-9-production" } - - subject { described_class.new(client) } - - describe '#pods' do - it 'returns the pods' do - expect(client).to receive(:search).with(body: es_query).and_return(es_response) - - result = subject.pods(namespace) - expect(result).to eq([ - { - name: "runner-gitlab-runner-7bbfb5dcb5-p6smb", - container_names: %w[runner-gitlab-runner] - }, - { - name: "elastic-stack-elasticsearch-master-1", - container_names: %w[elasticsearch chown sysctl] - }, - { - name: "ingress-nginx-ingress-controller-76449bcc8d-8qgl6", - container_names: %w[nginx-ingress-controller] - } - ]) - end - end -end diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index 6e7806c5d53..d0aba70081b 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -52,14 +52,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do expect(new_issue.issue_email_participants.first.email).to eq(author_email) end - it 'attaches existing CRM contact' do - contact = create(:contact, group: group, email: author_email) - receiver.execute - new_issue = Issue.last - - expect(new_issue.issue_customer_relations_contacts.last.contact).to eq(contact) - end - it 'sends thank you email' do expect { receiver.execute }.to have_enqueued_job.on_queue('mailers') end @@ -77,6 +69,16 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do context 'when everything is fine' do it_behaves_like 'a new issue request' + it 'attaches existing CRM contacts' do + contact = create(:contact, group: group, email: author_email) + contact2 = create(:contact, group: group, email: "cc@example.com") + contact3 = create(:contact, group: group, email: "kk@example.org") + receiver.execute + new_issue = Issue.last + + expect(new_issue.issue_customer_relations_contacts.map(&:contact)).to contain_exactly(contact, contact2, contact3) + end + context 'with legacy incoming email address' do let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') } diff --git a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb index dfa18c27d5e..ab6b1cd6171 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb @@ -99,7 +99,6 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do :verify | true :trial | true :team | true - :experience | true end with_them do diff --git a/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb deleted file mode 100644 index 8cd2345822e..00000000000 --- a/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do - let_it_be(:group) { build(:group) } - let_it_be(:user) { build(:user) } - - subject(:message) { described_class.new(group: group, user: user, series: series)} - - describe 'public methods' do - context 'with series 0' do - let(:series) { 0 } - - it 'returns value for series', :aggregate_failures do - expect(message.subject_line).to be_present - expect(message.tagline).to be_nil - expect(message.title).to be_present - expect(message.subtitle).to be_present - expect(message.body_line1).to be_present - expect(message.body_line2).to be_present - expect(message.cta_text).to be_nil - end - - describe 'feedback URL' do - before do - allow(message).to receive(:onboarding_progress).and_return(1) - allow(message).to receive(:show_invite_link).and_return(true) - end - - subject do - message.feedback_link(1) - end - - it { is_expected.to start_with(Gitlab::Saas.com_url) } - - context 'when in development' do - let(:root_url) { 'http://example.com' } - - before do - allow(message).to receive(:root_url).and_return(root_url) - stub_rails_env('development') - end - - it { is_expected.to start_with(root_url) } - end - end - - describe 'feedback URL show_invite_link query param' do - let(:user_access) { GroupMember::DEVELOPER } - let(:preferred_language) { 'en' } - - before do - allow(message).to receive(:onboarding_progress).and_return(1) - allow(group).to receive(:max_member_access_for_user).and_return(user_access) - allow(user).to receive(:preferred_language).and_return(preferred_language) - end - - subject do - uri = URI.parse(message.feedback_link(1)) - Rack::Utils.parse_query(uri.query).with_indifferent_access[:show_invite_link] - end - - it { is_expected.to eq('true') } - - context 'with less than developer access' do - let(:user_access) { GroupMember::GUEST } - - it { is_expected.to eq('false') } - end - - context 'with preferred language other than English' do - let(:preferred_language) { 'nl' } - - it { is_expected.to eq('false') } - end - end - - describe 'feedback URL show_incentive query param' do - let(:show_invite_link) { true } - let(:member_count) { 2 } - let(:query) do - uri = URI.parse(message.feedback_link(1)) - Rack::Utils.parse_query(uri.query).with_indifferent_access - end - - before do - allow(message).to receive(:onboarding_progress).and_return(1) - allow(message).to receive(:show_invite_link).and_return(show_invite_link) - allow(group).to receive(:member_count).and_return(member_count) - end - - subject { query[:show_incentive] } - - it { is_expected.to eq('true') } - - context 'with only one member' do - let(:member_count) { 1 } - - it "is not present" do - expect(query).not_to have_key(:show_incentive) - end - end - - context 'show_invite_link is false' do - let(:show_invite_link) { false } - - it "is not present" do - expect(query).not_to have_key(:show_incentive) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/email/message/in_product_marketing_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing_spec.rb index 40351bef8b9..1c59d9c8208 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing_spec.rb @@ -17,7 +17,6 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing do :verify | described_class::Verify :trial | described_class::Trial :team | described_class::Team - :experience | described_class::Experience end with_them do diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 4c1fbb93c13..b0c67cdafe1 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -45,16 +45,26 @@ RSpec.describe Gitlab::EncodingHelper do end context 'with corrupted diff' do + let(:project) { create(:project, :empty_repo) } + let(:repository) { project.repository } + let(:content) { fixture_file('encoding/Japanese.md') } let(:corrupted_diff) do - with_empty_bare_repository do |repo| - content = File.read(Rails.root.join( - 'spec/fixtures/encoding/Japanese.md').to_s) - commit_a = commit(repo, 'Japanese.md', content) - commit_b = commit(repo, 'Japanese.md', - content.sub('[TODO: Link]', '[現在作業中です: Link]')) - - repo.diff(commit_a, commit_b).each_line.map(&:content).join - end + commit_a = repository.create_file( + project.creator, + 'Japanese.md', + content, + branch_name: 'HEAD', + message: 'Create Japanese.md' + ) + commit_b = repository.update_file( + project.creator, + 'Japanese.md', + content.sub('[TODO: Link]', '[現在作業中です: Link]'), + branch_name: 'HEAD', + message: 'Update Japanese.md' + ) + + repository.diff(commit_a, commit_b).map(&:diff).join end let(:cleaned_diff) do @@ -69,26 +79,6 @@ RSpec.describe Gitlab::EncodingHelper do it 'does not corrupt data but remove invalid characters' do expect(encoded_diff).to eq(cleaned_diff) end - - def commit(repo, path, content) - oid = repo.write(content, :blob) - index = repo.index - - index.read_tree(repo.head.target.tree) unless repo.empty? - - index.add(path: path, oid: oid, mode: 0100644) - user = { name: 'Test', email: 'test@example.com' } - - Rugged::Commit.create( - repo, - tree: index.write_tree(repo), - author: user, - committer: user, - message: "Update #{path}", - parents: repo.empty? ? [] : [repo.head.target].compact, - update_ref: 'HEAD' - ) - end end end diff --git a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb new file mode 100644 index 00000000000..81e2a410962 --- /dev/null +++ b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb @@ -0,0 +1,436 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do + include AfterNextHelpers + + let(:project) { build_stubbed(:project) } + let(:api_exception) { ErrorTrackingOpenAPI::ApiError.new(code: 500, response_body: 'b' * 101) } + + subject(:repository) { Gitlab::ErrorTracking::ErrorRepository.build(project) } + + before do + # Disabled in spec_helper by default thus we need to enable it here. + stub_feature_flags(use_click_house_database_for_error_tracking: true) + end + + shared_examples 'exception logging' do + it 'logs error' do + expect(Gitlab::AppLogger).to receive(:error).with({ + 'open_api.http_code' => api_exception.code, + 'open_api.response_body' => api_exception.response_body.truncate(100) + }) + + subject + end + end + + shared_examples 'no logging' do + it 'does not log anything' do + expect(Gitlab::AppLogger).not_to receive(:debug) + expect(Gitlab::AppLogger).not_to receive(:info) + expect(Gitlab::AppLogger).not_to receive(:error) + end + end + + describe '#report_error' do + let(:params) do + { + name: 'anything', + description: 'anything', + actor: 'anything', + platform: 'anything', + environment: 'anything', + level: 'anything', + occurred_at: Time.zone.now, + payload: {} + } + end + + subject { repository.report_error(**params) } + + it 'is not implemented' do + expect { subject }.to raise_error(NotImplementedError, 'Use ingestion endpoint') + end + end + + describe '#find_error' do + let(:error) { build(:error_tracking_open_api_error, project_id: project.id) } + + subject { repository.find_error(error.fingerprint) } + + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(error) + + allow(open_api).to receive(:list_events) + .with(project.id, error.fingerprint, { sort: 'occurred_at_asc', limit: 1 }) + .and_return(list_events_asc) + + allow(open_api).to receive(:list_events) + .with(project.id, error.fingerprint, { sort: 'occurred_at_desc', limit: 1 }) + .and_return(list_events_desc) + end + end + + context 'when request succeeds' do + context 'without events returned' do + let(:list_events_asc) { [] } + let(:list_events_desc) { [] } + + include_examples 'no logging' + + it 'returns detailed error' do + is_expected.to have_attributes( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at.to_s, + last_seen: error.last_seen_at.to_s, + count: error.event_count, + user_count: error.approximated_user_count, + project_id: error.project_id, + status: error.status, + tags: { level: nil, logger: nil }, + external_url: "http://localhost/#{project.full_path}/-/error_tracking/#{error.fingerprint}/details", + external_base_url: "http://localhost/#{project.full_path}", + integrated: true + ) + end + + it 'returns no first and last release version' do + is_expected.to have_attributes( + first_release_version: nil, + last_release_version: nil + ) + end + end + + context 'with events returned' do + let(:first_event) { build(:error_tracking_open_api_error_event, project_id: project.id) } + let(:first_release) { parse_json(first_event.payload).fetch('release') } + let(:last_event) { build(:error_tracking_open_api_error_event, :golang, project_id: project.id) } + let(:last_release) { parse_json(last_event.payload).fetch('release') } + + let(:list_events_asc) { [first_event] } + let(:list_events_desc) { [last_event] } + + include_examples 'no logging' + + it 'returns first and last release version' do + expect(first_release).to be_present + expect(last_release).to be_present + + is_expected.to have_attributes( + first_release_version: first_release, + last_release_version: last_release + ) + end + + def parse_json(content) + Gitlab::Json.parse(content) + end + end + end + + context 'when request fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:get_error) + .with(project.id, error.fingerprint) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + end + + describe '#list_errors' do + let(:errors) { [] } + let(:response_with_info) { [errors, 200, headers] } + let(:result_errors) { result.first } + let(:result_pagination) { result.last } + + let(:headers) do + { + 'link' => [ + '<url?cursor=next_cursor¶m>; rel="next"', + '<url?cursor=prev_cursor¶m>; rel="prev"' + ].join(', ') + } + end + + subject(:result) { repository.list_errors(**params) } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, kind_of(Hash)) + .and_return(response_with_info) + end + + context 'with errors' do + let(:limit) { 3 } + let(:params) { { limit: limit } } + let(:errors_size) { limit } + let(:errors) { build_list(:error_tracking_open_api_error, errors_size, project_id: project.id) } + + include_examples 'no logging' + + it 'maps errors to models' do + # All errors are identical + error = errors.first + + expect(result_errors).to all( + have_attributes( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at, + last_seen: error.last_seen_at, + status: error.status, + count: error.event_count, + user_count: error.approximated_user_count + )) + end + + context 'when n errors are returned' do + let(:errors_size) { limit } + + include_examples 'no logging' + + it 'returns the amount of errors' do + expect(result_errors.size).to eq(3) + end + + it 'cursor links are preserved' do + expect(result_pagination).to have_attributes( + prev: 'prev_cursor', + next: 'next_cursor' + ) + end + end + + context 'when less errors than requested are returned' do + let(:errors_size) { limit - 1 } + + include_examples 'no logging' + + it 'returns the amount of errors' do + expect(result_errors.size).to eq(2) + end + + it 'cursor link for next is removed' do + expect(result_pagination).to have_attributes( + prev: 'prev_cursor', + next: nil + ) + end + end + end + + context 'with params' do + let(:params) do + { + filters: { status: 'resolved', something: 'different' }, + query: 'search term', + sort: 'first_seen', + limit: 2, + cursor: 'abc' + } + end + + include_examples 'no logging' + + it 'passes provided params to client' do + passed_params = { + sort: 'first_seen_desc', + status: 'resolved', + query: 'search term', + cursor: 'abc', + limit: 2 + } + + expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, passed_params) + .and_return(response_with_info) + + subject + end + end + + context 'without explicit params' do + let(:params) { {} } + + include_examples 'no logging' + + it 'passes default params to client' do + passed_params = { + sort: 'last_seen_desc', + limit: 20, + cursor: {} + } + + expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, passed_params) + .and_return(response_with_info) + + subject + end + end + + context 'when request fails' do + let(:params) { {} } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, kind_of(Hash)) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + specify do + expect(result_errors).to eq([]) + expect(result_pagination).to have_attributes( + next: nil, + prev: nil + ) + end + end + end + + describe '#last_event_for' do + let(:params) { { sort: 'occurred_at_desc', limit: 1 } } + let(:event) { build(:error_tracking_open_api_error_event, project_id: project.id) } + let(:error) { build(:error_tracking_open_api_error, project_id: project.id, fingerprint: event.fingerprint) } + + subject { repository.last_event_for(error.fingerprint) } + + context 'when both event and error is returned' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(error) + end + end + + include_examples 'no logging' + + it 'returns mapped error event' do + is_expected.to have_attributes( + issue_id: event.fingerprint.to_s, + date_received: error.last_seen_at, + stack_trace_entries: kind_of(Array) + ) + end + end + + context 'when event is not returned' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events) + .with(project.id, event.fingerprint, params) + .and_return([]) + end + + include_examples 'no logging' + + it { is_expected.to be_nil } + end + + context 'when list_events request fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events) + .with(project.id, event.fingerprint, params) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + + context 'when error is not returned' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(nil) + end + end + + include_examples 'no logging' + + it { is_expected.to be_nil } + end + + context 'when get_error request fails' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_raise(api_exception) + end + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + end + + describe '#update_error' do + let(:error) { build(:error_tracking_open_api_error, project_id: project.id) } + let(:update_params) { { status: 'resolved' } } + let(:passed_body) { ErrorTrackingOpenAPI::ErrorUpdatePayload.new(update_params) } + + subject { repository.update_error(error.fingerprint, **update_params) } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error) + .with(project.id, error.fingerprint, passed_body) + .and_return(:anything) + end + + context 'when update succeeds' do + include_examples 'no logging' + + it { is_expected.to eq(true) } + end + + context 'when update fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error) + .with(project.id, error.fingerprint, passed_body) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to eq(false) } + end + end + + describe '#dsn_url' do + let(:public_key) { 'abc' } + let(:config) { ErrorTrackingOpenAPI::Configuration.default } + + subject { repository.dsn_url(public_key) } + + it do + is_expected + .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/api/#{project.id}") + end + end +end diff --git a/spec/lib/gitlab/error_tracking/processor/sanitizer_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sanitizer_processor_spec.rb new file mode 100644 index 00000000000..9673bfc5cd3 --- /dev/null +++ b/spec/lib/gitlab/error_tracking/processor/sanitizer_processor_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ErrorTracking::Processor::SanitizerProcessor, :sentry do + describe '.call' do + let(:event) { Sentry.get_current_client.event_from_exception(exception) } + let(:result_hash) { described_class.call(event).to_hash } + + before do + data.each do |key, value| + event.send("#{key}=", value) + end + end + + after do + Sentry.get_current_scope.clear + end + + context 'when event attributes contains sensitive information' do + let(:exception) { RuntimeError.new } + let(:data) do + { + contexts: { + jwt: 'abcdef', + controller: 'GraphController#execute' + }, + tags: { + variables: %w[some sensitive information'], + deep_hash: { + sharedSecret: 'secret123' + } + }, + user: { + email: 'a@a.com', + password: 'nobodyknows' + }, + extra: { + issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1', + my_token: '[FILTERED]', + another_token: '[FILTERED]' + } + } + end + + it 'filters sensitive attributes' do + expect_next_instance_of(ActiveSupport::ParameterFilter) do |instance| + expect(instance).to receive(:filter).exactly(4).times.and_call_original + end + + expect(result_hash).to include( + contexts: { + jwt: '[FILTERED]', + controller: 'GraphController#execute' + }, + tags: { + variables: '[FILTERED]', + deep_hash: { + sharedSecret: '[FILTERED]' + } + }, + user: { + email: 'a@a.com', + password: '[FILTERED]' + }, + extra: { + issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1', + my_token: '[FILTERED]', + another_token: '[FILTERED]' + } + ) + end + end + + context 'when request contains sensitive information' do + let(:exception) { RuntimeError.new } + let(:data) { {} } + + before do + event.rack_env = { + 'HTTP_AUTHORIZATION' => 'Bearer 123456', + 'HTTP_PRIVATE_TOKEN' => 'abcdef', + 'HTTP_JOB_TOKEN' => 'secret123', + 'HTTP_GITLAB_WORKHORSE_PROXY_START' => 123456, + 'HTTP_COOKIE' => 'yummy_cookie=choco; tasty_cookie=strawberry', + 'QUERY_STRING' => 'token=secret&access_token=secret&job_token=secret&private_token=secret', + 'Content-Type' => 'application/json', + 'rack.input' => StringIO.new('{"name":"new_project", "some_token":"value"}') + } + end + + it 'filters sensitive headers' do + expect(result_hash[:request][:headers]).to include( + 'Authorization' => '[FILTERED]', + 'Private-Token' => '[FILTERED]', + 'Job-Token' => '[FILTERED]', + 'Gitlab-Workhorse-Proxy-Start' => '123456' + ) + end + + it 'filters query string parameters' do + expect(result_hash[:request][:query_string]).not_to include('secret') + end + + it 'removes cookies' do + expect(result_hash[:request][:cookies]).to be_empty + end + + it 'removes data' do + expect(result_hash[:request][:data]).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index 1ade3a51c55..fd859ae40fb 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -424,5 +424,25 @@ RSpec.describe Gitlab::ErrorTracking do end end end + + context 'when request contains sensitive information' do + before do + Sentry.get_current_scope.set_rack_env({ + 'HTTP_AUTHORIZATION' => 'Bearer 123456', + 'HTTP_PRIVATE_TOKEN' => 'abcdef', + 'HTTP_JOB_TOKEN' => 'secret123' + }) + end + + it 'filters sensitive data' do + track_exception + + expect(sentry_event.to_hash[:request][:headers]).to include( + 'Authorization' => '[FILTERED]', + 'Private-Token' => '[FILTERED]', + 'Job-Token' => '[FILTERED]' + ) + end + end end end diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb index 96cd70b4ff1..a5115989e6b 100644 --- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Git::AttributesAtRefParser, :seed_helper do +RSpec.describe Gitlab::Git::AttributesAtRefParser do let(:project) { create(:project, :repository) } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index 4bc39921e85..295d62fa052 100644 --- a/spec/lib/gitlab/git/attributes_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -2,9 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::Git::AttributesParser, :seed_helper do - let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') } - let(:data) { File.read(attributes_path) } +RSpec.describe Gitlab::Git::AttributesParser do + let(:data) { fixture_file('gitlab/git/gitattributes') } subject { described_class.new(data) } @@ -141,11 +140,12 @@ RSpec.describe Gitlab::Git::AttributesParser, :seed_helper do expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) end - it 'does not yield when the attributes file has an unsupported encoding' do - path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes') - attrs = described_class.new(File.read(path)) + context 'unsupported encoding' do + let(:data) { fixture_file('gitlab/git/gitattributes_invalid') } - expect { |b| attrs.each_line(&b) }.not_to yield_control + it 'does not yield' do + expect { |b| subject.each_line(&b) }.not_to yield_control + end end end end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 7dd7460b142..e514e128785 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -2,10 +2,10 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Blame, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - - let(:sha) { SeedRepo::Commit::ID } +RSpec.describe Gitlab::Git::Blame do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:sha) { TestEnv::BRANCH_SHA['master'] } let(:path) { 'CONTRIBUTING.md' } let(:range) { nil } @@ -37,19 +37,17 @@ RSpec.describe Gitlab::Git::Blame, :seed_helper do end context "ISO-8859 encoding" do - let(:sha) { SeedRepo::EncodingCommit::ID } let(:path) { 'encoding/iso8859.txt' } it 'converts to UTF-8' do expect(result.size).to eq(1) expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(result.first[:line]).to eq("Ä ü") + expect(result.first[:line]).to eq("Äü") expect(result.first[:line]).to be_utf8 end end context "unknown encoding" do - let(:sha) { SeedRepo::EncodingCommit::ID } let(:path) { 'encoding/iso8859.txt' } it 'converts to UTF-8' do @@ -59,14 +57,12 @@ RSpec.describe Gitlab::Git::Blame, :seed_helper do expect(result.size).to eq(1) expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(result.first[:line]).to eq(" ") + expect(result.first[:line]).to eq("") expect(result.first[:line]).to be_utf8 end end context "renamed file" do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw_repository } let(:commit) { project.commit('blame-on-renamed') } let(:sha) { commit.id } let(:path) { 'files/plain_text/renamed' } diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 3cc52863976..97cd4777b4d 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -4,9 +4,6 @@ require "spec_helper" RSpec.describe Gitlab::Git::Branch, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - let(:rugged) do - Rugged::Repository.new(File.join(TestEnv.repos_path, repository.relative_path)) - end subject { repository.branches } @@ -81,20 +78,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do end let(:user) { create(:user) } - let(:committer) { { email: user.email, name: user.name } } - let(:params) do - parents = [rugged.head.target] - tree = parents.first.tree - - { - message: +'commit message', - author: committer, - committer: committer, - tree: tree, - parents: parents - } - end - let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } let(:future_sha) { travel_to(100.days.since) { create_commit } } @@ -137,7 +120,11 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } def create_commit - params[:message].delete!(+"\r") - Rugged::Commit.create(rugged, params.merge(committer: committer.merge(time: Time.now))) + repository.multi_action( + user, + branch_name: 'HEAD', + message: 'commit message', + actions: [] + ).newrev end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index de342444c15..da77d8ee5d6 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -3,68 +3,8 @@ require "spec_helper" RSpec.describe Gitlab::Git::Commit, :seed_helper do - include GitHelpers - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - let(:rugged_repo) do - Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH)) - end - let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } - let(:rugged_commit) { rugged_repo.lookup(SeedRepo::Commit::ID) } - - describe "Commit info" do - before do - @committer = { - email: 'mike@smith.com', - name: "Mike Smith", - time: Time.new(2000, 1, 1, 0, 0, 0, "+08:00") - } - - @author = { - email: 'john@smith.com', - name: "John Smith", - time: Time.new(2000, 1, 1, 0, 0, 0, "-08:00") - } - - @parents = [rugged_repo.head.target] - @gitlab_parents = @parents.map { |c| described_class.find(repository, c.oid) } - @tree = @parents.first.tree - - sha = Rugged::Commit.create( - rugged_repo, - author: @author, - committer: @committer, - tree: @tree, - parents: @parents, - message: "Refactoring specs", - update_ref: "HEAD" - ) - - @raw_commit = rugged_repo.lookup(sha) - @commit = described_class.find(repository, sha) - end - - it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } - it { expect(@commit.id).to eq(@raw_commit.oid) } - it { expect(@commit.sha).to eq(@raw_commit.oid) } - it { expect(@commit.safe_message).to eq(@raw_commit.message) } - it { expect(@commit.created_at).to eq(@raw_commit.committer[:time]) } - it { expect(@commit.date).to eq(@raw_commit.committer[:time]) } - it { expect(@commit.author_email).to eq(@author[:email]) } - it { expect(@commit.author_name).to eq(@author[:name]) } - it { expect(@commit.committer_name).to eq(@committer[:name]) } - it { expect(@commit.committer_email).to eq(@committer[:email]) } - it { expect(@commit.different_committer?).to be_truthy } - it { expect(@commit.parents).to eq(@gitlab_parents) } - it { expect(@commit.parent_id).to eq(@parents.first.oid) } - it { expect(@commit.no_commit_message).to eq("No commit message") } - - after do - # Erase the new commit so other tests get the original repo - rugged_repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) - end - end describe "Commit info from gitaly commit" do let(:subject) { (+"My commit").force_encoding('ASCII-8BIT') } @@ -132,7 +72,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do shared_examples '.find' do it "returns first head commit if without params" do expect(described_class.last(repository).id).to eq( - rugged_repo.head.target.oid + repository.commit.sha ) end @@ -622,19 +562,6 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do end end - skip 'move this test to gitaly-ruby' do - RSpec.describe '#init_from_rugged' do - let(:gitlab_commit) { described_class.new(repository, rugged_commit) } - - subject { gitlab_commit } - - describe '#id' do - subject { super().id } - it { is_expected.to eq(SeedRepo::Commit::ID) } - end - end - end - describe '#init_from_hash' do let(:commit) { described_class.new(repository, sample_commit_hash) } diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb index 02b00f711b4..7d81af92412 100644 --- a/spec/lib/gitlab/git/conflict/parser_spec.rb +++ b/spec/lib/gitlab/git/conflict/parser_spec.rb @@ -86,43 +86,68 @@ RSpec.describe Gitlab::Git::Conflict::Parser do CONFLICT end - let(:lines) do - described_class.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') - end + shared_examples_for 'successful parsing' do + let(:lines) do + described_class.parse(content, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') + end - let(:old_line_numbers) do - lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] } - end + let(:old_line_numbers) do + lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] } + end - let(:new_line_numbers) do - lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] } - end + let(:new_line_numbers) do + lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] } + end + + let(:line_indexes) { lines.map { |line| line[:line_obj_index] } } - let(:line_indexes) { lines.map { |line| line[:line_obj_index] } } + it 'sets our lines as new lines' do + expect(lines[8..13]).to all(include(type: 'new')) + expect(lines[26..27]).to all(include(type: 'new')) + expect(lines[56..57]).to all(include(type: 'new')) + end - it 'sets our lines as new lines' do - expect(lines[8..13]).to all(include(type: 'new')) - expect(lines[26..27]).to all(include(type: 'new')) - expect(lines[56..57]).to all(include(type: 'new')) + it 'sets their lines as old lines' do + expect(lines[14..19]).to all(include(type: 'old')) + expect(lines[28..29]).to all(include(type: 'old')) + expect(lines[58..59]).to all(include(type: 'old')) + end + + it 'sets non-conflicted lines as both' do + expect(lines[0..7]).to all(include(type: nil)) + expect(lines[20..25]).to all(include(type: nil)) + expect(lines[30..55]).to all(include(type: nil)) + expect(lines[60..62]).to all(include(type: nil)) + end + + it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do + expect(line_indexes).to eq(0.upto(62).to_a) + expect(old_line_numbers).to eq(1.upto(53).to_a) + expect(new_line_numbers).to eq(1.upto(53).to_a) + end end - it 'sets their lines as old lines' do - expect(lines[14..19]).to all(include(type: 'old')) - expect(lines[28..29]).to all(include(type: 'old')) - expect(lines[58..59]).to all(include(type: 'old')) + context 'content has LF endings' do + let(:content) { text } + + it_behaves_like 'successful parsing' end - it 'sets non-conflicted lines as both' do - expect(lines[0..7]).to all(include(type: nil)) - expect(lines[20..25]).to all(include(type: nil)) - expect(lines[30..55]).to all(include(type: nil)) - expect(lines[60..62]).to all(include(type: nil)) + context 'content has CRLF endings' do + let(:content) { text.gsub("\n", "\r\n") } + + it_behaves_like 'successful parsing' end - it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do - expect(line_indexes).to eq(0.upto(62).to_a) - expect(old_line_numbers).to eq(1.upto(53).to_a) - expect(new_line_numbers).to eq(1.upto(53).to_a) + context 'content has mixed LF and CRLF endings' do + # Simulate mixed line endings by only changing some of the lines to CRLF + let(:content) do + text.each_line.map.with_index do |line, index| + index.odd? ? line.gsub("\n", "\r\n") : line + end.join + end + + it_behaves_like 'successful parsing' end end diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index 91960ebbede..3b1eb0319f8 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Gitlab::Git::ObjectPool do - include RepoHelpers - let(:pool_repository) { create(:pool_repository) } let(:source_repository) { pool_repository.source_project.repository } @@ -80,8 +78,6 @@ RSpec.describe Gitlab::Git::ObjectPool do end describe '#fetch' do - let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } - let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) } let(:commit_count) { source_repository.commit_count } context "when the object's pool repository exists" do @@ -106,7 +102,13 @@ RSpec.describe Gitlab::Git::ObjectPool do end it 'fetches objects from the source repository' do - new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid + new_commit_id = source_repository.create_file( + pool_repository.source_project.owner, + 'a.file', + 'This is a file', + branch_name: source_repository.root_ref, + message: 'Add a file' + ) expect(subject.repository.exists?).to be false diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 47688c4b3e6..e20d5b928c4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -352,12 +352,30 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('left-branch') repository.create_branch('right-branch') - left.times do - new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + left.times do |i| + repository.multi_action( + user, + branch_name: 'left-branch', + message: 'some more content for a', + actions: [{ + action: i == 0 ? :create : :update, + file_path: 'encoding/CHANGELOG', + content: 'some stuff' + }] + ) end - right.times do - new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + right.times do |i| + repository.multi_action( + user, + branch_name: 'right-branch', + message: 'some more content for b', + actions: [{ + action: i == 0 ? :create : :update, + file_path: 'encoding/CHANGELOG', + content: 'some stuff' + }] + ) end end @@ -367,8 +385,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end it 'returns the correct count bounding at max_count' do - branch_a_sha = repository_rugged.branches['left-branch'].target.oid - branch_b_sha = repository_rugged.branches['right-branch'].target.oid + branch_a_sha = repository.find_branch('left-branch').dereferenced_target.sha + branch_b_sha = repository.find_branch('right-branch').dereferenced_target.sha count = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: 1000) @@ -392,12 +410,30 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do repository.create_branch('left-branch') repository.create_branch('right-branch') - left.times do - new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + left.times do |i| + repository.multi_action( + user, + branch_name: 'left-branch', + message: 'some more content for a', + actions: [{ + action: i == 0 ? :create : :update, + file_path: 'encoding/CHANGELOG', + content: 'some stuff' + }] + ) end - right.times do - new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + right.times do |i| + repository.multi_action( + user, + branch_name: 'right-branch', + message: 'some more content for b', + actions: [{ + action: i == 0 ? :create : :update, + file_path: 'encoding/CHANGELOG', + content: 'some stuff' + }] + ) end end @@ -407,8 +443,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end it 'returns the correct count bounding at max_count' do - branch_a_sha = repository_rugged.branches['left-branch'].target.oid - branch_b_sha = repository_rugged.branches['right-branch'].target.oid + branch_a_sha = repository.find_branch('left-branch').dereferenced_target.sha + branch_b_sha = repository.find_branch('right-branch').dereferenced_target.sha results = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: max_count) @@ -469,16 +505,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'deletes the ref' do repository.delete_refs('refs/heads/feature') - expect(repository_rugged.references['refs/heads/feature']).to be_nil + expect(repository.find_branch('feature')).to be_nil end it 'deletes all refs' do refs = %w[refs/heads/wip refs/tags/v1.1.0] repository.delete_refs(*refs) - refs.each do |ref| - expect(repository_rugged.references[ref]).to be_nil - end + expect(repository.list_refs(refs)).to be_empty end it 'does not fail when deleting an empty list of refs' do @@ -491,7 +525,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#branch_names_contains_sha' do - let(:head_id) { repository_rugged.head.target.oid } + let(:head_id) { repository.commit.id } let(:new_branch) { head_id } let(:utf8_branch) { 'branch-é' } @@ -525,7 +559,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'does not error when dereferenced_target is nil' do blob_id = repository.blob_at('master', 'README.md').id - repository_rugged.tags.create("refs/tags/blob-tag", blob_id) + repository.add_tag("refs/tags/blob-tag", user: user, target: blob_id) expect { subject }.not_to raise_error end @@ -559,14 +593,31 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#search_files_by_content' do let(:repository) { mutable_repository } - let(:repository_rugged) { mutable_repository_rugged } let(:ref) { 'search-files-by-content-branch' } let(:content) { 'foobarbazmepmep' } before do repository.create_branch(ref) - new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', ref, 'committing something', content) - new_commit_edit_new_file_on_branch(repository_rugged, 'anotherfile', ref, 'committing something', content) + repository.multi_action( + user, + branch_name: ref, + message: 'committing something', + actions: [{ + action: :create, + file_path: 'encoding/CHANGELOG', + content: content + }] + ) + repository.multi_action( + user, + branch_name: ref, + message: 'committing something', + actions: [{ + action: :create, + file_path: 'anotherfile', + content: content + }] + ) end after do @@ -669,14 +720,42 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do before do # Add new commits so that there's a renamed file in the commit history - @commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid - @rename_commit_id = new_commit_move_file(repository_rugged).oid - @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged, "encoding/CHANGELOG", "Edit encoding/CHANGELOG", "I'm a new changelog with different text").oid + @commit_with_old_name_id = repository.multi_action( + user, + branch_name: repository.root_ref, + message: 'Update CHANGELOG', + actions: [{ + action: :update, + file_path: 'CHANGELOG', + content: 'CHANGELOG' + }] + ).newrev + @rename_commit_id = repository.multi_action( + user, + branch_name: repository.root_ref, + message: 'Move CHANGELOG to encoding/', + actions: [{ + action: :move, + previous_path: 'CHANGELOG', + file_path: 'encoding/CHANGELOG', + content: 'CHANGELOG' + }] + ).newrev + @commit_with_new_name_id = repository.multi_action( + user, + branch_name: repository.root_ref, + message: 'Edit encoding/CHANGELOG', + actions: [{ + action: :update, + file_path: 'encoding/CHANGELOG', + content: "I'm a new changelog with different text" + }] + ).newrev end after do # Erase our commits so other tests get the original repo - repository_rugged.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID) end context "where 'follow' == true" do @@ -1649,27 +1728,28 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#gitattribute' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } - after do - ensure_seeds - end + context 'with gitattributes' do + before do + repository.copy_gitattributes('gitattributes') + end - it 'returns matching language attribute' do - expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby') - end + it 'returns matching language attribute' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby') + end - it 'returns matching language attribute with additional options' do - expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json') - end + it 'returns matching language attribute with additional options' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json') + end - it 'returns nil if nothing matches' do - expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil) + it 'returns nil if nothing matches' do + expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil) + end end - context 'without gitattributes file' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - + context 'without gitattributes' do it 'returns nil' do expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil) end @@ -1760,25 +1840,13 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#languages' do it 'returns exactly the expected results' do languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') - expected_languages = [ - { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, - { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, - { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, - { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } - ] - - expect(languages.size).to eq(expected_languages.size) - - expected_languages.size.times do |i| - a = expected_languages[i] - b = languages[i] - expect(a.keys.sort).to eq(b.keys.sort) - expect(a[:value]).to be_within(0.1).of(b[:value]) - - non_float_keys = a.keys - [:value] - expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys)) - end + expect(languages).to match_array([ + { value: a_value_within(0.1).of(66.7), label: "Ruby", color: "#701516", highlight: "#701516" }, + { value: a_value_within(0.1).of(22.96), label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, + { value: a_value_within(0.1).of(7.9), label: "HTML", color: "#e34c26", highlight: "#e34c26" }, + { value: a_value_within(0.1).of(2.51), label: "CoffeeScript", color: "#244776", highlight: "#244776" } + ]) end it "uses the repository's HEAD when no ref is passed" do @@ -1818,12 +1886,18 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do context 'when the branch exists' do context 'when the commit does not exist locally' do let(:source_branch) { 'new-branch-for-fetch-source-branch' } - let(:source_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } - let(:source_rugged) { Rugged::Repository.new(source_path) } - let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } - before do - source_rugged.branches.create(source_branch, new_oid) + let!(:new_oid) do + source_repository.multi_action( + user, + branch_name: source_branch, + message: 'Add a file', + actions: [{ + action: :create, + file_path: 'a.file', + content: 'This is a file.' + }] + ).newrev end it 'writes the ref' do @@ -1869,7 +1943,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it "removes the branch from the repo" do repository.rm_branch(branch_name, user: user) - expect(repository_rugged.branches[branch_name]).to be_nil + expect(repository.find_branch(branch_name)).to be_nil end end @@ -2290,11 +2364,23 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'when the diff contains a rename' do - let(:end_sha) { new_commit_move_file(repository_rugged).oid } + let(:end_sha) do + repository.multi_action( + user, + branch_name: repository.root_ref, + message: 'Move CHANGELOG to encoding/', + actions: [{ + action: :move, + previous_path: 'CHANGELOG', + file_path: 'encoding/CHANGELOG', + content: 'CHANGELOG' + }] + ).newrev + end after do # Erase our commits so other tests get the original repo - repository_rugged.references.update('refs/heads/master', SeedRepo::LastCommit::ID) + repository.write_ref(repository.root_ref, SeedRepo::LastCommit::ID) end it 'does not include the renamed file in the sparse checkout' do @@ -2342,24 +2428,15 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end def create_remote_branch(remote_name, branch_name, source_branch_name) - source_branch = repository.branches.find { |branch| branch.name == source_branch_name } - repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) - end - - def refs(dir) - IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| - line.split("\t").last - end + source_branch = repository.find_branch(source_branch_name) + repository.write_ref("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end describe '#disconnect_alternates' do let(:project) { create(:project, :repository) } let(:pool_repository) { create(:pool_repository) } let(:repository) { project.repository } - let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } let(:object_pool) { pool_repository.object_pool } - let(:object_pool_path) { File.join(TestEnv.repos_path, object_pool.repository.relative_path) } - let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) } before do object_pool.create # rubocop:disable Rails/SaveBang @@ -2369,25 +2446,24 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do expect { repository.disconnect_alternates }.not_to raise_error end - it 'removes the alternates file' do - object_pool.link(repository) - - alternates_file = File.join(repository_path, "objects", "info", "alternates") - expect(File.exist?(alternates_file)).to be_truthy - - repository.disconnect_alternates - - expect(File.exist?(alternates_file)).to be_falsey - end - it 'can still access objects in the object pool' do object_pool.link(repository) - new_commit = new_commit_edit_old_file(object_pool_rugged) - expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) + new_commit_id = object_pool.repository.multi_action( + project.owner, + branch_name: object_pool.repository.root_ref, + message: 'Add a file', + actions: [{ + action: :create, + file_path: 'a.file', + content: 'This is a file.' + }] + ).newrev + + expect(repository.commit(new_commit_id).id).to eq(new_commit_id) repository.disconnect_alternates - expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid) + expect(repository.commit(new_commit_id).id).to eq(new_commit_id) end end @@ -2483,7 +2559,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'mirrors the source repository' do subject - expect(refs(new_repository_path)).to eq(refs(repository_path)) + expect(new_repository.list_refs(['refs/'])).to eq(repository.list_refs(['refs/'])) end end @@ -2495,7 +2571,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'mirrors the source repository' do subject - expect(refs(new_repository_path)).to eq(refs(repository_path)) + expect(new_repository.list_refs(['refs/'])).to eq(repository.list_refs(['refs/'])) end context 'with keep-around refs' do @@ -2511,8 +2587,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'includes the temporary and keep-around refs' do subject - expect(refs(new_repository_path)).to include(keep_around_ref) - expect(refs(new_repository_path)).to include(tmp_ref) + expect(new_repository.list_refs([keep_around_ref]).map(&:name)).to match_array([keep_around_ref]) + expect(new_repository.list_refs([tmp_ref]).map(&:name)).to match_array([tmp_ref]) end end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 97ba177da71..172d7a3f27b 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" RSpec.describe Gitlab::Git::Tree, :seed_helper do + let_it_be(:user) { create(:user) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } shared_examples :repo do @@ -85,51 +87,29 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do context :flat_path do let(:filename) { 'files/flat/path/correct/content.txt' } - let(:sha) { create_file(filename) } let(:path) { 'files/flat' } # rubocop: disable Rails/FindBy # This is not ActiveRecord where..first let(:subdir_file) { entries.first } # rubocop: enable Rails/FindBy - let(:repository_rugged) { Rugged::Repository.new(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) } - - it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') } - end - - def create_file(path) - oid = repository_rugged.write('test', :blob) - index = repository_rugged.index - index.add(path: filename, oid: oid, mode: 0100644) - - options = commit_options( - repository_rugged, - index, - repository_rugged.head.target, - 'HEAD', - 'Add new file') + let!(:sha) do + repository.multi_action( + user, + branch_name: 'HEAD', + message: "Create #{filename}", + actions: [{ + action: :create, + file_path: filename, + contents: 'test' + }] + ).newrev + end - Rugged::Commit.create(repository_rugged, options) - end + after do + ensure_seeds + end - # Build the options hash that's passed to Rugged::Commit#create - def commit_options(repo, index, target, ref, message) - options = {} - options[:tree] = index.write_tree(repo) - options[:author] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:committer] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:message] ||= message - options[:parents] = repo.empty? ? [] : [target].compact - options[:update_ref] = ref - - options + it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') } 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 3a34d39c722..d5d1bef7bff 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::GitalyClient::CommitService do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let(:storage_name) { project.repository_storage } let(:relative_path) { project.disk_path + '.git' } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 4320c5460da..e04895d975f 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -2,9 +2,6 @@ require 'spec_helper' -require 'google/rpc/status_pb' -require 'google/protobuf/well_known_types' - RSpec.describe Gitlab::GitalyClient::OperationService do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository) } @@ -188,7 +185,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end shared_examples 'a failed branch deletion' do - it 'raises a PreRecieveError' do + it 'raises a PreReceiveError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_delete_branch).with(request, kind_of(Hash)) .and_raise(custom_hook_error) @@ -288,7 +285,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end shared_examples 'a failed merge' do - it 'raises a PreRecieveError' do + it 'raises a PreReceiveError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(custom_hook_error) @@ -816,14 +813,4 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end end end - - def new_detailed_error(error_code, error_message, details) - status_error = Google::Rpc::Status.new( - code: error_code, - message: error_message, - details: [Google::Protobuf::Any.pack(details)] - ) - - GRPC::BadStatus.new(error_code, error_message, { "grpc-status-details-bin" => Google::Rpc::Status.encode(status_error) }) - end end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 2e37c98a591..566bdbacf4a 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -241,30 +241,70 @@ RSpec.describe Gitlab::GitalyClient::RefService do end end - describe '#ref_exists?', :seed_helper do - it 'finds the master branch ref' do - expect(client.ref_exists?('refs/heads/master')).to eq(true) - end + describe '#ref_exists?' do + let(:ref) { 'refs/heads/master' } - it 'returns false for an illegal tag name ref' do - expect(client.ref_exists?('refs/tags/.this-tag-name-is-illegal')).to eq(false) - end + it 'sends a ref_exists message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:ref_exists) + .with(gitaly_request_with_params(ref: ref), kind_of(Hash)) + .and_return(double('ref_exists_response', value: true)) - it 'raises an argument error if the ref name parameter does not start with refs/' do - expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError) + expect(client.ref_exists?(ref)).to be true end end describe '#delete_refs' do let(:prefixes) { %w(refs/heads refs/keep-around) } + subject(:delete_refs) { client.delete_refs(except_with_prefixes: prefixes) } + it 'sends a delete_refs message' do expect_any_instance_of(Gitaly::RefService::Stub) .to receive(:delete_refs) .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) .and_return(double('delete_refs_response', git_error: "")) - client.delete_refs(except_with_prefixes: prefixes) + delete_refs + end + + context 'with a references locked error' do + let(:references_locked_error) do + new_detailed_error( + GRPC::Core::StatusCodes::FAILED_PRECONDITION, + "error message", + Gitaly::DeleteRefsError.new(references_locked: Gitaly::ReferencesLockedError.new)) + end + + it 'raises ReferencesLockedError' do + expect_any_instance_of(Gitaly::RefService::Stub).to receive(:delete_refs) + .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) + .and_raise(references_locked_error) + + expect { delete_refs }.to raise_error(Gitlab::Git::ReferencesLockedError) + end + end + + context 'with a invalid format error' do + let(:invalid_refs) {['\invali.\d/1', '\.invali/d/2']} + let(:invalid_reference_format_error) do + new_detailed_error( + GRPC::Core::StatusCodes::INVALID_ARGUMENT, + "error message", + Gitaly::DeleteRefsError.new(invalid_format: Gitaly::InvalidRefFormatError.new(refs: invalid_refs))) + end + + it 'raises InvalidRefFormatError' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:delete_refs) + .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) + .and_raise(invalid_reference_format_error) + + expect { delete_refs }.to raise_error do |error| + expect(error).to be_a(Gitlab::Git::InvalidRefFormatError) + expect(error.message).to eq("references have an invalid format: #{invalid_refs.join(",")}") + end + end end end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index ba4ea1069d8..a3840ca843f 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -358,11 +358,7 @@ RSpec.describe Gitlab::GitalyClient do end end - context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do - before do - stub_feature_flags(gitaly_enforce_requests_limits: false) - end - + shared_examples 'enforces maximum allowed Gitaly calls' do it 'allows up the maximum number of allowed calls' do expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error end @@ -408,6 +404,18 @@ RSpec.describe Gitlab::GitalyClient do end end + context 'when RequestStore is enabled and the maximum number of calls is enforced by a feature flag', :request_store do + include_examples 'enforces maximum allowed Gitaly calls' + end + + context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do + before do + stub_feature_flags(gitaly_enforce_requests_limits: false) + end + + include_examples 'enforces maximum allowed Gitaly calls' + end + context 'in production and when RequestStore is enabled', :request_store do before do stub_rails_env('production') @@ -537,4 +545,44 @@ RSpec.describe Gitlab::GitalyClient do end end end + + describe '.decode_detailed_error' do + let(:detailed_error) do + new_detailed_error(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + "error message", + Gitaly::InvalidRefFormatError.new) + end + + let(:error_without_details) do + error_code = GRPC::Core::StatusCodes::INVALID_ARGUMENT + error_message = "error message" + + status_error = Google::Rpc::Status.new( + code: error_code, + message: error_message, + details: nil + ) + + GRPC::BadStatus.new( + error_code, + error_message, + { "grpc-status-details-bin" => Google::Rpc::Status.encode(status_error) }) + end + + context 'decodes a structured error' do + using RSpec::Parameterized::TableSyntax + + where(:error, :result) do + detailed_error | Gitaly::InvalidRefFormatError.new + error_without_details | nil + StandardError.new | nil + end + + with_them do + it 'returns correct detailed error' do + expect(described_class.decode_detailed_error(error)).to eq(result) + end + end + end + end end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb new file mode 100644 index 00000000000..b773598853d --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:issue) { create(:issue, project: project) } + let!(:label) { create(:label, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => event_type, + 'commit_id' => nil, + 'label_title' => label.title, + 'issue_db_id' => issue.id, + 'created_at' => '2022-04-26 18:30:53 UTC' + ) + end + + let(:event_attrs) do + { + user_id: user.id, + issue_id: issue.id, + label_id: label.id, + created_at: issue_event.created_at + }.stringify_keys + end + + shared_examples 'new event' do + it 'creates a new label event' do + expect { importer.execute(issue_event) }.to change { issue.resource_label_events.count } + .from(0).to(1) + expect(issue.resource_label_events.last) + .to have_attributes(expected_event_attrs) + end + end + + before do + allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id) + end + + context 'when importing a labeled event' do + let(:event_type) { 'labeled' } + let(:expected_event_attrs) { event_attrs.merge(action: 'add') } + + it_behaves_like 'new event' + end + + context 'when importing an unlabeled event' do + let(:event_type) { 'unlabeled' } + let(:expected_event_attrs) { event_attrs.merge(action: 'remove') } + + it_behaves_like 'new event' + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb new file mode 100644 index 00000000000..116917d3e06 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:issue) { create(:issue, project: project) } + let(:commit_id) { nil } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => 'closed', + 'created_at' => '2022-04-26 18:30:53 UTC', + 'commit_id' => commit_id, + 'issue_db_id' => issue.id + ) + end + + let(:expected_event_attrs) do + { + project_id: project.id, + author_id: user.id, + target_id: issue.id, + target_type: Issue.name, + action: 'closed', + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_state_event_attrs) do + { + user_id: user.id, + issue_id: issue.id, + state: 'closed', + created_at: issue_event.created_at + }.stringify_keys + end + + it 'creates expected event and state event' do + importer.execute(issue_event) + + expect(issue.events.count).to eq 1 + expect(issue.events[0].attributes) + .to include expected_event_attrs + + expect(issue.resource_state_events.count).to eq 1 + expect(issue.resource_state_events[0].attributes) + .to include expected_state_event_attrs + end + + context 'when closed by commit' do + let!(:closing_commit) { create(:commit, project: project) } + let(:commit_id) { closing_commit.id } + + it 'creates expected event and state event' do + importer.execute(issue_event) + + expect(issue.events.count).to eq 1 + state_event = issue.resource_state_events.last + expect(state_event.source_commit).to eq commit_id[0..40] + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb new file mode 100644 index 00000000000..118c482a7d9 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_gitlab_redis_cache do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:sawyer_stub) { Struct.new(:iid, :issuable_type, keyword_init: true) } + + let(:issue) { create(:issue, project: project) } + let(:referenced_in) { build_stubbed(:issue, project: project) } + let(:commit_id) { nil } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => 'cross-referenced', + 'source' => { + 'type' => 'issue', + 'issue' => { + 'number' => referenced_in.iid, + 'pull_request' => pull_request_resource + } + }, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue_db_id' => issue.id + ) + end + + let(:pull_request_resource) { nil } + let(:expected_note_attrs) do + { + system: true, + noteable_type: Issue.name, + noteable_id: issue_event.issue_db_id, + project_id: project.id, + author_id: user.id, + note: expected_note_body, + created_at: issue_event.created_at + }.stringify_keys + end + + context 'when referenced in other issue' do + let(:expected_note_body) { "mentioned in issue ##{issue.iid}" } + + before do + other_issue_resource = sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'Issue') + Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) + .cache_database_id(referenced_in.iid) + end + + it 'creates expected note' do + importer.execute(issue_event) + + expect(issue.notes.count).to eq 1 + expect(issue.notes[0]).to have_attributes expected_note_attrs + expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference' + end + end + + context 'when referenced in pull request' do + let(:referenced_in) { build_stubbed(:merge_request, project: project) } + let(:pull_request_resource) { { 'id' => referenced_in.iid } } + + let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } + + before do + other_issue_resource = + sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'MergeRequest') + Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) + .cache_database_id(referenced_in.iid) + end + + it 'creates expected note' do + importer.execute(issue_event) + + expect(issue.notes.count).to eq 1 + expect(issue.notes[0]).to have_attributes expected_note_attrs + expect(issue.notes[0].system_note_metadata.action).to eq 'cross_reference' + end + end + + context 'when referenced in out of project issue/pull_request' do + it 'creates expected note' do + importer.execute(issue_event) + + expect(issue.notes.count).to eq 0 + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb new file mode 100644 index 00000000000..a8c3fbcb05d --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => 'renamed', + 'commit_id' => nil, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'old_title' => 'old title', + 'new_title' => 'new title', + 'issue_db_id' => issue.id + ) + end + + let(:expected_note_attrs) do + { + noteable_id: issue.id, + noteable_type: Issue.name, + project_id: project.id, + author_id: user.id, + note: "changed title from **{-old-} title** to **{+new+} title**", + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_system_note_metadata_attrs) do + { + action: "title", + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + describe '#execute' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issue.notes.count } + .from(0).to(1) + + expect(issue.notes.last) + .to have_attributes(expected_note_attrs) + end + + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } + .from(0).to(1) + + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) + ) + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb new file mode 100644 index 00000000000..81653b0ecdc --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_failures do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => 'reopened', + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue_db_id' => issue.id + ) + end + + let(:expected_event_attrs) do + { + project_id: project.id, + author_id: user.id, + target_id: issue.id, + target_type: Issue.name, + action: 'reopened', + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_state_event_attrs) do + { + user_id: user.id, + state: 'reopened', + created_at: issue_event.created_at + }.stringify_keys + end + + it 'creates expected event and state event' do + importer.execute(issue_event) + + expect(issue.events.count).to eq 1 + expect(issue.events[0].attributes) + .to include expected_event_attrs + + expect(issue.resource_state_events.count).to eq 1 + expect(issue.resource_state_events[0].attributes) + .to include expected_state_event_attrs + end +end diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb new file mode 100644 index 00000000000..da32a3b3766 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab_redis_cache do + let(:importer) { described_class.new(issue_event, project, client) } + + let(:project) { create(:project) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + 'actor' => { 'id' => actor_id, 'login' => 'alice' }, + 'event' => event_name, + 'commit_id' => '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + 'commit_url' => + 'https://api.github.com/repos/octocat/Hello-World/commits/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + 'created_at' => '2022-04-26 18:30:53 UTC', + 'performed_via_github_app' => nil + ) + end + + let(:actor_id) { user.id } + let(:event_name) { 'closed' } + + shared_examples 'triggers specific event importer' do |importer_class| + it importer_class.name do + specific_importer = double(importer_class.name) # rubocop:disable RSpec/VerifiedDoubles + + expect(importer_class) + .to receive(:new).with(project, user.id) + .and_return(specific_importer) + expect(specific_importer).to receive(:execute).with(issue_event) + + importer.execute + end + end + + describe '#execute' do + before do + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:author_id_for) + .with(issue_event, author_key: :actor) + .and_return(user.id, true) + end + + issue_event.attributes[:issue_db_id] = issue.id + end + + context "when it's closed issue event" do + let(:event_name) { 'closed' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::Closed + end + + context "when it's reopened issue event" do + let(:event_name) { 'reopened' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::Reopened + end + + context "when it's labeled issue event" do + let(:event_name) { 'labeled' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedLabel + end + + context "when it's unlabeled issue event" do + let(:event_name) { 'unlabeled' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedLabel + end + + context "when it's renamed issue event" do + let(:event_name) { 'renamed' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::Renamed + end + + context "when it's cross-referenced issue event" do + let(:event_name) { 'cross-referenced' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::CrossReferenced + end + + context "when it's unknown issue event" do + let(:event_name) { 'fake' } + + it 'logs warning and skips' do + expect(Gitlab::GithubImport::Logger).to receive(:debug) + .with( + message: 'UNSUPPORTED_EVENT_TYPE', + event_type: issue_event.event, + event_github_id: issue_event.id + ) + + importer.execute + end + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 2a06983417d..570d26cdf2d 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -131,6 +131,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi title: 'My Issue', author_id: user.id, project_id: project.id, + namespace_id: project.project_namespace_id, description: 'This is my issue', milestone_id: milestone.id, state_id: 1, @@ -160,6 +161,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi title: 'My Issue', author_id: project.creator_id, project_id: project.id, + namespace_id: project.project_namespace_id, description: "*Created by: alice*\n\nThis is my issue", milestone_id: milestone.id, state_id: 1, diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb new file mode 100644 index 00000000000..087faeffe02 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter do + let(:client) { double } + + let_it_be(:project) { create(:project, :import_started, import_source: 'http://somegithub.com') } + let_it_be(:issue) { create(:issue, project: project) } + + subject { described_class.new(project, client, parallel: parallel) } + + let(:parallel) { true } + + it { is_expected.to include_module(Gitlab::GithubImport::ParallelScheduling) } + + describe '#importer_class' do + it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::IssueEventImporter) } + end + + describe '#representation_class' do + it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::IssueEvent) } + end + + describe '#sidekiq_worker_class' do + it { expect(subject.sidekiq_worker_class).to eq(Gitlab::GithubImport::ImportIssueEventWorker) } + end + + describe '#object_type' do + it { expect(subject.object_type).to eq(:issue_event) } + end + + describe '#collection_method' do + it { expect(subject.collection_method).to eq(:issue_timeline) } + end + + describe '#page_counter_id' do + it { expect(subject.page_counter_id(issue)).to eq("issues/#{issue.iid}/issue_timeline") } + end + + describe '#id_for_already_imported_cache' do + let(:event) { instance_double('Event', id: 1) } + + it { expect(subject.id_for_already_imported_cache(event)).to eq(1) } + end + + describe '#collection_options' do + it do + expect(subject.collection_options) + .to eq({ state: 'all', sort: 'created', direction: 'asc' }) + end + end + + describe '#each_object_to_import', :clean_gitlab_redis_cache do + let(:issue_event) do + struct = Struct.new(:id, :event, :created_at, :issue_db_id, keyword_init: true) + struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') + end + + let(:page) do + instance_double( + Gitlab::GithubImport::Client::Page, + number: 1, objects: [issue_event] + ) + end + + let(:page_counter) { instance_double(Gitlab::GithubImport::PageCounter) } + + before do + allow(client).to receive(:each_page) + .once + .with( + :issue_timeline, + project.import_source, + issue.iid, + { state: 'all', sort: 'created', direction: 'asc', page: 1 } + ).and_yield(page) + end + + it 'imports each issue event page by page' do + counter = 0 + subject.each_object_to_import do |object| + expect(object).to eq issue_event + expect(issue_event.issue_db_id).to eq issue.id + counter += 1 + end + expect(counter).to eq 1 + end + + it 'triggers page number increment' do + expect(Gitlab::GithubImport::PageCounter) + .to receive(:new).with(project, 'issues/1/issue_timeline') + .and_return(page_counter) + expect(page_counter).to receive(:current).and_return(1) + expect(page_counter) + .to receive(:set).with(page.number).and_return(true) + + counter = 0 + subject.each_object_to_import { counter += 1 } + expect(counter).to eq 1 + end + + context 'when page is already processed' do + before do + page_counter = Gitlab::GithubImport::PageCounter.new( + project, subject.page_counter_id(issue) + ) + page_counter.set(page.number) + end + + it "doesn't process this page" do + counter = 0 + subject.each_object_to_import { counter += 1 } + expect(counter).to eq 0 + end + end + + context 'when event is already processed' do + it "doesn't process this event" do + subject.mark_as_imported(issue_event) + + counter = 0 + subject.each_object_to_import { counter += 1 } + expect(counter).to eq 0 + end + end + end +end diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb index 2d159580b5f..ad45469a4c3 100644 --- a/spec/lib/gitlab/github_import/markdown_text_spec.rb +++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb @@ -12,6 +12,54 @@ RSpec.describe Gitlab::GithubImport::MarkdownText do end end + describe '.convert_ref_links' do + let_it_be(:project) { create(:project) } + + let(:paragraph) { FFaker::Lorem.paragraph } + let(:sentence) { FFaker::Lorem.sentence } + let(:issue_id) { rand(100) } + let(:pull_id) { rand(100) } + + let(:text_in) do + <<-TEXT + #{paragraph} + https://github.com/#{project.import_source}/issues/#{issue_id} + #{sentence} + https://github.com/#{project.import_source}/pull/#{pull_id} + TEXT + end + + let(:text_out) do + <<-TEXT + #{paragraph} + http://localhost/#{project.full_path}/-/issues/#{issue_id} + #{sentence} + http://localhost/#{project.full_path}/-/merge_requests/#{pull_id} + TEXT + end + + it { expect(described_class.convert_ref_links(text_in, project)).to eq text_out } + + context 'when Github EE with custom domain name' do + let(:github_domain) { 'https://custom.github.com/' } + let(:text_in) do + <<-TEXT + #{paragraph} + #{github_domain}#{project.import_source}/issues/#{issue_id} + #{sentence} + #{github_domain}#{project.import_source}/pull/#{pull_id} + TEXT + end + + before do + allow(Gitlab::Auth::OAuth::Provider) + .to receive(:config_for).with('github').and_return({ 'url' => github_domain }) + end + + it { expect(described_class.convert_ref_links(text_in, project)).to eq text_out } + end + end + describe '#to_s' do it 'returns the text when the author was found' do author = double(:author, login: 'Alice') diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb new file mode 100644 index 00000000000..23da8276f64 --- /dev/null +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do + shared_examples 'an IssueEvent' do + it 'returns an instance of IssueEvent' do + expect(issue_event).to be_an_instance_of(described_class) + end + + context 'the returned IssueEvent' do + it 'includes the issue event id' do + expect(issue_event.id).to eq(6501124486) + end + + it 'includes the issue event "event"' do + expect(issue_event.event).to eq('closed') + end + + it 'includes the issue event commit_id' do + expect(issue_event.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + end + + it 'includes the issue event source' do + expect(issue_event.source).to eq({ type: 'issue', id: 123456 }) + end + + it 'includes the issue_db_id' do + expect(issue_event.issue_db_id).to eq(100500) + end + + context 'when actor data present' do + it 'includes the actor details' do + expect(issue_event.actor) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + + expect(issue_event.actor.id).to eq(4) + expect(issue_event.actor.login).to eq('alice') + end + end + + context 'when actor data is empty' do + let(:with_actor) { false } + + it 'does not return such info' do + expect(issue_event.actor).to eq nil + end + end + + context 'when label data is present' do + it 'includes the label_title' do + expect(issue_event.label_title).to eq('label title') + end + end + + context 'when label data is empty' do + let(:with_label) { false } + + it 'does not return such info' do + expect(issue_event.label_title).to eq nil + end + end + + context 'when rename field is present' do + it 'includes the old_title and new_title fields' do + expect(issue_event.old_title).to eq('old title') + expect(issue_event.new_title).to eq('new title') + end + end + + context 'when rename field is empty' do + let(:with_rename) { false } + + it 'does not return such info' do + expect(issue_event.old_title).to eq nil + expect(issue_event.new_title).to eq nil + end + end + + it 'includes the created timestamp' do + expect(issue_event.created_at).to eq('2022-04-26 18:30:53 UTC') + end + end + + describe '#github_identifiers' do + it 'returns a hash with needed identifiers' do + expect(issue_event.github_identifiers).to eq({ id: 6501124486 }) + end + end + end + + describe '.from_api_response' do + let(:response) do + event_resource = Struct.new( + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, + :rename, :issue_db_id, :created_at, :performed_via_github_app, :source, + keyword_init: true + ) + user_resource = Struct.new(:id, :login, keyword_init: true) + event_resource.new( + id: 6501124486, + node_id: 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + url: 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + actor: with_actor ? user_resource.new(id: 4, login: 'alice') : nil, + event: 'closed', + commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + commit_url: 'https://api.github.com/repos/octocat/Hello-World/commits'\ + '/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + rename: with_rename ? { from: 'old title', to: 'new title' } : nil, + source: { type: 'issue', id: 123456 }, + issue_db_id: 100500, + label: with_label ? { name: 'label title' } : nil, + created_at: '2022-04-26 18:30:53 UTC', + performed_via_github_app: nil + ) + end + + let(:with_actor) { true } + let(:with_label) { true } + let(:with_rename) { true } + + it_behaves_like 'an IssueEvent' do + let(:issue_event) { described_class.from_api_response(response) } + end + end + + describe '.from_json_hash' do + it_behaves_like 'an IssueEvent' do + let(:hash) do + { + 'id' => 6501124486, + 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', + 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', + 'actor' => (with_actor ? { 'id' => 4, 'login' => 'alice' } : nil), + 'event' => 'closed', + 'commit_id' => '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + 'commit_url' => + 'https://api.github.com/repos/octocat/Hello-World/commits/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + 'label_title' => (with_label ? 'label title' : nil), + 'old_title' => with_rename ? 'old title' : nil, + 'new_title' => with_rename ? 'new title' : nil, + 'source' => { 'type' => 'issue', 'id' => 123456 }, + "issue_db_id" => 100500, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'performed_via_github_app' => nil + } + end + + let(:with_actor) { true } + let(:with_label) { true } + let(:with_rename) { true } + + let(:issue_event) { described_class.from_json_hash(hash) } + end + end +end diff --git a/spec/lib/gitlab/github_import/single_endpoint_notes_importing_spec.rb b/spec/lib/gitlab/github_import/single_endpoint_notes_importing_spec.rb new file mode 100644 index 00000000000..64dbc939348 --- /dev/null +++ b/spec/lib/gitlab/github_import/single_endpoint_notes_importing_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::SingleEndpointNotesImporting do + let(:importer_class) do + Class.new do + def self.name + 'MyImporter' + end + + include(Gitlab::GithubImport::SingleEndpointNotesImporting) + end + end + + let(:importer_instance) { importer_class.new } + + describe '#parent_collection' do + it { expect { importer_instance.parent_collection }.to raise_error(NotImplementedError) } + end + + describe '#parent_imported_cache_key' do + it { expect { importer_instance.parent_imported_cache_key }.to raise_error(NotImplementedError) } + end + + describe '#page_counter_id' do + it { expect { importer_instance.page_counter_id(build(:merge_request)) }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index eb4c404e454..984c690add6 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -21,8 +21,8 @@ RSpec.describe Gitlab::GitlabImport::Importer do 'name' => 'John Doe' } } - ]) - stub_request('issues/3/notes', []) + ].to_json) + stub_request('issues/3/notes', [].to_json) end it 'persists issues' do diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 9c399e78d80..919335bc9fa 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -3,6 +3,34 @@ require 'spec_helper' RSpec.describe Gitlab::Gpg::Commit do + let_it_be(:project) { create(:project, :repository, path: 'sample-project') } + + let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let(:committer_email) { GpgHelpers::User1.emails.first } + let(:user_email) { committer_email } + let(:public_key) { GpgHelpers::User1.public_key } + let(:user) { create(:user, email: user_email) } + let(:commit) { create(:commit, project: project, sha: commit_sha, committer_email: committer_email) } + let(:crypto) { instance_double(GPGME::Crypto) } + let(:mock_signature_data?) { true } + # gpg_keys must be pre-loaded so that they can be found during signature verification. + let!(:gpg_key) { create(:gpg_key, key: public_key, user: user) } + + let(:signature_data) do + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + end + + before do + if mock_signature_data? + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return(signature_data) + end + end + describe '#signature' do shared_examples 'returns the cached signature on second call' do it 'returns the cached signature on second call' do @@ -17,11 +45,8 @@ RSpec.describe Gitlab::Gpg::Commit do end end - let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - context 'unsigned commit' do - let!(:commit) { create :commit, project: project, sha: commit_sha } + let(:signature_data) { nil } it 'returns nil' do expect(described_class.new(commit).signature).to be_nil @@ -29,20 +54,12 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'invalid signature' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - # Corrupt the key - GpgHelpers::User1.signed_commit_signature.tr('=', 'a'), - GpgHelpers::User1.signed_commit_base_data - ] - ) + let(:signature_data) do + [ + # Corrupt the key + GpgHelpers::User1.signed_commit_signature.tr('=', 'a'), + GpgHelpers::User1.signed_commit_base_data + ] end it 'returns nil' do @@ -53,25 +70,6 @@ RSpec.describe Gitlab::Gpg::Commit do context 'known key' do context 'user matches the key uid' do context 'user email matches the email committer' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end - it 'returns a valid signature' do signature = described_class.new(commit).signature @@ -112,32 +110,13 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'valid key signed using recent version of Gnupg' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - let!(:crypto) { instance_double(GPGME::Crypto) } - before do - fake_signature = [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return(fake_signature) - end - - it 'returns a valid signature' do verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true) allow(GPGME::Crypto).to receive(:new).and_return(crypto) allow(crypto).to receive(:verify).and_yield(verified_signature) + end + it 'returns a valid signature' do signature = described_class.new(commit).signature expect(signature).to have_attributes( @@ -153,33 +132,14 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'valid key signed using older version of Gnupg' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - let!(:crypto) { instance_double(GPGME::Crypto) } - before do - fake_signature = [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return(fake_signature) - end - - it 'returns a valid signature' do keyid = GpgHelpers::User1.fingerprint.last(16) verified_signature = double('verified-signature', fingerprint: keyid, valid?: true) allow(GPGME::Crypto).to receive(:new).and_return(crypto) allow(crypto).to receive(:verify).and_yield(verified_signature) + end + it 'returns a valid signature' do signature = described_class.new(commit).signature expect(signature).to have_attributes( @@ -195,32 +155,13 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'commit with multiple signatures' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - let!(:crypto) { instance_double(GPGME::Crypto) } - before do - fake_signature = [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return(fake_signature) - end - - it 'returns an invalid signatures error' do verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true) allow(GPGME::Crypto).to receive(:new).and_return(crypto) allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature) + end + it 'returns an invalid signatures error' do signature = described_class.new(commit).signature expect(signature).to have_attributes( @@ -236,27 +177,18 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'commit signed with a subkey' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first } - - let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User3.public_key, user: user - end + let(:committer_email) { GpgHelpers::User3.emails.first } + let(:public_key) { GpgHelpers::User3.public_key } let(:gpg_key_subkey) do gpg_key.subkeys.find_by(fingerprint: GpgHelpers::User3.subkey_fingerprints.last) end - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User3.signed_commit_signature, - GpgHelpers::User3.signed_commit_base_data - ] - ) + let(:signature_data) do + [ + GpgHelpers::User3.signed_commit_signature, + GpgHelpers::User3.signed_commit_base_data + ] end it 'returns a valid signature' do @@ -275,7 +207,7 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'user email does not match the committer email, but is the same user' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } + let(:committer_email) { GpgHelpers::User2.emails.first } let(:user) do create(:user, email: GpgHelpers::User1.emails.first).tap do |user| @@ -283,21 +215,6 @@ RSpec.describe Gitlab::Gpg::Commit do end end - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end - it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( commit_sha: commit_sha, @@ -314,24 +231,8 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'user email does not match the committer email' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } - - let(:user) { create(:user, email: GpgHelpers::User1.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end + let(:committer_email) { GpgHelpers::User2.emails.first } + let(:user_email) { GpgHelpers::User1.emails.first } it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( @@ -350,24 +251,8 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'user does not match the key uid' do - let!(:commit) { create :commit, project: project, sha: commit_sha } - - let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } - - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end + let(:user_email) { GpgHelpers::User2.emails.first } + let(:public_key) { GpgHelpers::User1.public_key } it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( @@ -386,18 +271,7 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'unknown key' do - let!(:commit) { create :commit, project: project, sha: commit_sha } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end + let(:gpg_key) { nil } it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( @@ -415,15 +289,15 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'multiple commits with signatures' do - let(:first_signature) { create(:gpg_signature) } - - let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) } - let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) } + let(:mock_signature_data?) { false } + let!(:first_signature) { create(:gpg_signature) } + let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) } + let!(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) } let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) } let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) } - let(:commits) do + let!(:commits) do [first_commit, second_commit].map do |commit| gpg_commit = described_class.new(commit) @@ -442,4 +316,21 @@ RSpec.describe Gitlab::Gpg::Commit do end end end + + describe '#update_signature!' do + let!(:gpg_key) { nil } + + let(:signature) { described_class.new(commit).signature } + + it 'updates signature record' do + signature + + create(:gpg_key, key: public_key, user: user) + + stored_signature = CommitSignatures::GpgSignature.find_by_commit_sha(commit_sha) + expect { described_class.new(commit).update_signature!(stored_signature) }.to( + change { signature.reload.verification_status }.from('unknown_key').to('verified') + ) + end + end end diff --git a/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb new file mode 100644 index 00000000000..94e880d979d --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GrapeLogging::Loggers::ResponseLogger do + let(:logger) { described_class.new } + + describe '#parameters' do + let(:response1) { 'response1' } + let(:response) { [response1] } + + subject { logger.parameters(nil, response) } + + it { expect(subject).to eq({ response_bytes: response1.bytesize }) } + + context 'with multiple response parts' do + let(:response2) { 'response2' } + let(:response) { [response1, response2] } + + it { expect(subject).to eq({ response_bytes: response1.bytesize + response2.bytesize }) } + end + + context 'with log_response_length disabled' do + before do + stub_feature_flags(log_response_length: false) + end + + it { expect(subject).to eq({}) } + end + end +end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb index 97613edee5e..8a2b5ae0d38 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb @@ -79,7 +79,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end end end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 61a79d90546..6574b3e3131 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(:updated_at) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end it 'includes the :id even when not specified in the order' do @@ -104,7 +104,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(:updated_at).order(:created_at) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end end @@ -112,7 +112,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end end end diff --git a/spec/lib/gitlab/harbor/client_spec.rb b/spec/lib/gitlab/harbor/client_spec.rb index bc5b593370a..4e80b8b53e3 100644 --- a/spec/lib/gitlab/harbor/client_spec.rb +++ b/spec/lib/gitlab/harbor/client_spec.rb @@ -3,12 +3,277 @@ require 'spec_helper' RSpec.describe Gitlab::Harbor::Client do - let(:harbor_integration) { build(:harbor_integration) } + let_it_be(:harbor_integration) { create(:harbor_integration) } subject(:client) { described_class.new(harbor_integration) } + describe '#initialize' do + context 'if integration is nil' do + let(:harbor_integration) { nil } + + it 'raises ConfigError' do + expect { client }.to raise_error(described_class::ConfigError) + end + end + + context 'integration is provided' do + it 'is initialized successfully' do + expect { client }.not_to raise_error + end + end + end + + describe '#get_repositories' do + context 'with valid params' do + let(:mock_response) do + [ + { + "artifact_count": 1, + "creation_time": "2022-03-13T09:36:43.240Z", + "id": 1, + "name": "jihuprivate/busybox", + "project_id": 4, + "pull_count": 0, + "update_time": "2022-03-13T09:36:43.240Z" + } + ] + end + + let(:mock_repositories) do + { + body: mock_response, + total_count: 2 + } + end + + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 2 }) + end + + it 'get repositories' do + expect(client.get_repositories({}).deep_stringify_keys).to eq(mock_repositories.deep_stringify_keys) + end + end + + context 'when harbor project does not exist' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 404, body: {}.to_json) + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_repositories({}) + end.to raise_error(Gitlab::Harbor::Client::Error, 'request error') + end + end + + context 'with invalid response' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: '[not json}') + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_repositories({}) + end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format') + end + end + end + + describe '#get_artifacts' do + context 'with valid params' do + let(:mock_response) do + [ + { + "digest": "sha256:661e8e44e5d7290fbd42d0495ab4ff6fdf1ad251a9f358969b3264a22107c14d", + "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06", + "id": 1, + "project_id": 1, + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.901Z", + "repository_id": 1, + "size": 126745886, + "tags": [ + { + "artifact_id": 1, + "id": 1, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.920Z", + "repository_id": 1, + "signed": false + } + ], + "type": "IMAGE" + } + ] + end + + let(:mock_artifacts) do + { + body: mock_response, + total_count: 1 + } + end + + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 1 }) + end + + it 'get artifacts' do + expect(client.get_artifacts({ repository_name: 'test' }) + .deep_stringify_keys).to eq(mock_artifacts.deep_stringify_keys) + end + end + + context 'when harbor repository does not exist' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 404, body: {}.to_json) + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_artifacts({ repository_name: 'test' }) + end.to raise_error(Gitlab::Harbor::Client::Error, 'request error') + end + end + + context 'with invalid response' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: '[not json}') + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_artifacts({ repository_name: 'test' }) + end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format') + end + end + end + + describe '#get_tags' do + context 'with valid params' do + let(:mock_response) do + [ + { + "artifact_id": 1, + "id": 1, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.920Z", + "repository_id": 1, + "signed": false + } + ] + end + + let(:mock_tags) do + { + body: mock_response, + total_count: 1 + } + end + + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: mock_response.to_json, headers: { "x-total-count": 1 }) + end + + it 'get tags' do + expect(client.get_tags({ repository_name: 'test', artifact_name: '1' }) + .deep_stringify_keys).to eq(mock_tags.deep_stringify_keys) + end + end + + context 'when harbor artifact does not exist' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 404, body: {}.to_json) + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_tags({ repository_name: 'test', artifact_name: '1' }) + end.to raise_error(Gitlab::Harbor::Client::Error, 'request error') + end + end + + context 'with invalid response' do + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: '[not json}') + end + + it 'raises Gitlab::Harbor::Client::Error' do + expect do + client.get_tags({ repository_name: 'test', artifact_name: '1' }) + end.to raise_error(Gitlab::Harbor::Client::Error, 'invalid response format') + end + end + end + describe '#ping' do - let!(:harbor_ping_request) { stub_harbor_request("https://demo.goharbor.io/api/v2.0/ping") } + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/ping") + .with( + headers: { + 'Content-Type': 'application/json' + }) + .to_return(status: 200, body: 'pong') + end it "calls api/v2.0/ping successfully" do expect(client.ping).to eq(success: true) diff --git a/spec/lib/gitlab/harbor/query_spec.rb b/spec/lib/gitlab/harbor/query_spec.rb new file mode 100644 index 00000000000..dcb9a16b27b --- /dev/null +++ b/spec/lib/gitlab/harbor/query_spec.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Harbor::Query do + let_it_be(:harbor_integration) { create(:harbor_integration) } + + let(:params) { {} } + + subject(:query) { described_class.new(harbor_integration, ActionController::Parameters.new(params)) } + + describe 'Validations' do + context 'page' do + context 'with valid page' do + let(:params) { { page: 1 } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid page' do + let(:params) { { page: -1 } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + + context 'limit' do + context 'with valid limit' do + let(:params) { { limit: 1 } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid limit' do + context 'with limit less than 0' do + let(:params) { { limit: -1 } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + + context 'with limit greater than 25' do + let(:params) { { limit: 26 } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + end + + context 'repository_id' do + context 'with valid repository_id' do + let(:params) { { repository_id: 'test' } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid repository_id' do + let(:params) { { repository_id: 'test@@' } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + + context 'artifact_id' do + context 'with valid artifact_id' do + let(:params) { { artifact_id: 'test' } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid artifact_id' do + let(:params) { { artifact_id: 'test@@' } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + + context 'sort' do + context 'with valid sort' do + let(:params) { { sort: 'creation_time desc' } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid sort' do + let(:params) { { sort: 'blabla desc' } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + + context 'search' do + context 'with valid search' do + let(:params) { { search: 'name=desc' } } + + it 'initialize successfully' do + expect(query.valid?).to eq(true) + end + end + + context 'with invalid search' do + let(:params) { { search: 'blabla' } } + + it 'initialize failed' do + expect(query.valid?).to eq(false) + end + end + end + end + + describe '#repositories' do + let(:response) { { total_count: 0, repositories: [] } } + + def expect_query_option_include(expected_params) + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_repositories) + .with(hash_including(expected_params)) + .and_return(response) + end + + query.repositories + end + + context 'when params is {}' do + it 'fills default params' do + expect_query_option_include(page_size: 10, page: 1) + end + end + + context 'when params contains options' do + let(:params) { { search: 'name=bu', sort: 'creation_time desc', limit: 20, page: 3 } } + + it 'fills params with standard of Harbor' do + expect_query_option_include(q: 'name=~bu', sort: '-creation_time', page_size: 20, page: 3) + end + end + + context 'when params contains invalid sort option' do + let(:params) { { search: 'name=bu', sort: 'blabla desc', limit: 20, page: 3 } } + + it 'ignores invalid sort params' do + expect(query.valid?).to eq(false) + end + end + + context 'when client.get_repositories returns data' do + let(:response_with_data) do + { + total_count: 1, + body: + [ + { + "id": 3, + "name": "testproject/thirdbusybox", + "artifact_count": 1, + "creation_time": "2022-03-15T07:12:14.479Z", + "update_time": "2022-03-15T07:12:14.479Z", + "project_id": 3, + "pull_count": 0 + }.with_indifferent_access + ] + } + end + + it 'returns the right repositories data' do + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_repositories) + .with(hash_including(page_size: 10, page: 1)) + .and_return(response_with_data) + end + + expect(query.repositories.first).to include( + "name": "testproject/thirdbusybox", + "artifact_count": 1 + ) + end + end + end + + describe '#artifacts' do + let(:response) { { total_count: 0, artifacts: [] } } + + def expect_query_option_include(expected_params) + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_artifacts) + .with(hash_including(expected_params)) + .and_return(response) + end + + query.artifacts + end + + context 'when params is {}' do + it 'fills default params' do + expect_query_option_include(page_size: 10, page: 1) + end + end + + context 'when params contains options' do + let(:params) do + { search: 'tags=1', repository_id: 'jihuprivate', sort: 'creation_time desc', limit: 20, page: 3 } + end + + it 'fills params with standard of Harbor' do + expect_query_option_include(q: 'tags=~1', sort: '-creation_time', page_size: 20, page: 3) + end + end + + context 'when params contains invalid sort option' do + let(:params) { { search: 'tags=1', repository_id: 'jihuprivate', sort: 'blabla desc', limit: 20, page: 3 } } + + it 'ignores invalid sort params' do + expect(query.valid?).to eq(false) + end + end + + context 'when client.get_artifacts returns data' do + let(:response_with_data) do + { + total_count: 1, + body: + [ + { + "digest": "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38", + "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06", + "id": 5, + "project_id": 14, + "push_time": "2022-03-22T09:04:56.170Z", + "repository_id": 5, + "size": 774790, + "tags": [ + { + "artifact_id": 5, + "id": 7, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-03-22T09:05:04.844Z", + "repository_id": 5 + }, + { + "artifact_id": 5, + "id": 6, + "immutable": false, + "name": "1", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-03-22T09:04:56.186Z", + "repository_id": 5 + } + ], + "type": "IMAGE" + }.with_indifferent_access + ] + } + end + + it 'returns the right artifacts data' do + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_artifacts) + .with(hash_including(page_size: 10, page: 1)) + .and_return(response_with_data) + end + + artifact = query.artifacts.first + + expect(artifact).to include( + "digest": "sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38", + "push_time": "2022-03-22T09:04:56.170Z" + ) + expect(artifact["tags"].size).to eq(2) + end + end + end + + describe '#tags' do + let(:response) { { total_count: 0, tags: [] } } + + def expect_query_option_include(expected_params) + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_tags) + .with(hash_including(expected_params)) + .and_return(response) + end + + query.tags + end + + context 'when params is {}' do + it 'fills default params' do + expect_query_option_include(page_size: 10, page: 1) + end + end + + context 'when params contains options' do + let(:params) { { repository_id: 'jihuprivate', sort: 'creation_time desc', limit: 20, page: 3 } } + + it 'fills params with standard of Harbor' do + expect_query_option_include(sort: '-creation_time', page_size: 20, page: 3) + end + end + + context 'when params contains invalid sort option' do + let(:params) { { repository_id: 'jihuprivate', artifact_id: 'test', sort: 'blabla desc', limit: 20, page: 3 } } + + it 'ignores invalid sort params' do + expect(query.valid?).to eq(false) + end + end + + context 'when client.get_tags returns data' do + let(:response_with_data) do + { + total_count: 2, + body: + [ + { + "artifact_id": 5, + "id": 7, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-03-22T09:05:04.844Z", + "repository_id": 5 + }, + { + "artifact_id": 5, + "id": 6, + "immutable": false, + "name": "1", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-03-22T09:04:56.186Z", + "repository_id": 5 + }.with_indifferent_access + ] + } + end + + it 'returns the right tags data' do + expect_next_instance_of(Gitlab::Harbor::Client) do |client| + expect(client).to receive(:get_tags) + .with(hash_including(page_size: 10, page: 1)) + .and_return(response_with_data) + end + + tag = query.tags.first + + expect(tag).to include( + "immutable": false, + "push_time": "2022-03-22T09:05:04.844Z" + ) + end + end + end +end diff --git a/spec/lib/gitlab/hash_digest/facade_spec.rb b/spec/lib/gitlab/hash_digest/facade_spec.rb deleted file mode 100644 index b352744513e..00000000000 --- a/spec/lib/gitlab/hash_digest/facade_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::HashDigest::Facade do - describe '.hexdigest' do - let(:plaintext) { 'something that is plaintext' } - - let(:sha256_hash) { OpenSSL::Digest::SHA256.hexdigest(plaintext) } - let(:md5_hash) { Digest::MD5.hexdigest(plaintext) } # rubocop:disable Fips/MD5 - - it 'uses SHA256' do - expect(described_class.hexdigest(plaintext)).to eq(sha256_hash) - end - - context 'when feature flags is not available' do - before do - allow(Feature).to receive(:feature_flags_available?).and_return(false) - end - - it 'uses MD5' do - expect(described_class.hexdigest(plaintext)).to eq(md5_hash) - end - end - - context 'when active_support_hash_digest_sha256 FF is disabled' do - before do - stub_feature_flags(active_support_hash_digest_sha256: false) - end - - it 'uses MD5' do - expect(described_class.hexdigest(plaintext)).to eq(md5_hash) - end - end - end -end 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 771fc0218e2..25b84a67ab2 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -7,13 +7,12 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do let(:builder) { described_class.new(merge_request) } - describe '#build' do - let(:data) { builder.build } + describe '.safe_hook_attributes' do + let(:safe_attribute_keys) { described_class.safe_hook_attributes } it 'includes safe attribute' do - %w[ + expected_safe_attribute_keys = %i[ assignee_id - assignee_ids author_id blocking_discussions_resolved created_at @@ -32,17 +31,21 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do milestone_id source_branch source_project_id - state + state_id target_branch target_project_id time_estimate title updated_at updated_by_id - ].each do |key| - expect(data).to include(key) - end + ].freeze + + expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys) end + end + + describe '#build' do + let(:data) { builder.build } %i[source target].each do |key| describe "#{key} key" do @@ -52,17 +55,30 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do end end + it 'includes safe attributes' do + expect(data).to include(*described_class.safe_hook_attributes) + end + it 'includes additional attrs' do - expect(data).to include(:source) - expect(data).to include(:target) - expect(data).to include(:last_commit) - expect(data).to include(:work_in_progress) - expect(data).to include(:total_time_spent) - expect(data).to include(:time_change) - expect(data).to include(:human_time_estimate) - expect(data).to include(:human_total_time_spent) - expect(data).to include(:human_time_change) - expect(data).to include(:labels) + expected_additional_attributes = %w[ + description + url + last_commit + work_in_progress + total_time_spent + time_change + human_total_time_spent + human_time_change + human_time_estimate + assignee_ids + assignee_id + labels + state + blocking_discussions_resolved + first_contribution + ].freeze + + expect(data).to include(*expected_additional_attributes) end context 'when the MR has an image in the description' do diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb index cde8376febd..a241a4b6490 100644 --- a/spec/lib/gitlab/http_connection_adapter_spec.rb +++ b/spec/lib/gitlab/http_connection_adapter_spec.rb @@ -15,18 +15,6 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do stub_all_dns('https://example.org', ip_address: '93.184.216.34') end - context 'with use_read_total_timeout option' do - let(:options) { { use_read_total_timeout: true } } - - it 'sets up the connection using the Gitlab::NetHttpAdapter' do - expect(connection).to be_a(Gitlab::NetHttpAdapter) - expect(connection.address).to eq('93.184.216.34') - expect(connection.hostname_override).to eq('example.org') - expect(connection.addr_port).to eq('example.org') - expect(connection.port).to eq(443) - end - end - context 'when local requests are allowed' do let(:options) { { allow_local_requests: true } } diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index c2fb987d195..929fd37ee40 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -83,67 +83,25 @@ RSpec.describe Gitlab::HTTP do subject(:request_slow_responder) { described_class.post('http://example.org', **options) } - shared_examples 'tracks the timeout but does not raise an error' do - specify :aggregate_failures do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - an_instance_of(Gitlab::HTTP::ReadTotalTimeout) - ).once - - expect { request_slow_responder }.not_to raise_error - end - - it 'still calls the block' do - expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_successive_args('a', 'b') - end - end - - shared_examples 'does not track or raise timeout error' do - specify :aggregate_failures do - expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - - expect { request_slow_responder }.not_to raise_error - end - end - - it_behaves_like 'tracks the timeout but does not raise an error' - - context 'and use_read_total_timeout option is truthy' do - let(:options) { { use_read_total_timeout: true } } - - it 'raises an error' do - expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/) - end + it 'raises an error' do + expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/) end context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do let(:options) { { timeout: 10.seconds } } - it_behaves_like 'does not track or raise timeout error' + it 'does not raise an error' do + expect { request_slow_responder }.not_to raise_error + end end context 'and stream_body option is truthy' do let(:options) { { stream_body: true } } - it_behaves_like 'does not track or raise timeout error' - - context 'but skip_read_total_timeout option is falsey' do - let(:options) { { stream_body: true, skip_read_total_timeout: false } } - - it_behaves_like 'tracks the timeout but does not raise an error' + it 'does not raise an error' do + expect { request_slow_responder }.not_to raise_error end end - - context 'and skip_read_total_timeout option is truthy' do - let(:options) { { skip_read_total_timeout: true } } - - it_behaves_like 'does not track or raise timeout error' - end - - context 'and skip_read_total_timeout option is falsely' do - let(:options) { { skip_read_total_timeout: false } } - - it_behaves_like 'tracks the timeout but does not raise an error' - end end it 'calls a block' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 9d516c8d7ac..af910b08fae 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -420,6 +420,8 @@ project: - zentao_integration # dingtalk_integration JiHu-specific, see https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417 - dingtalk_integration +# dingtalk_integration JiHu-specific, see https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/640 +- feishu_integration - redmine_integration - youtrack_integration - custom_issue_tracker_integration @@ -557,7 +559,6 @@ project: - packages - package_files - packages_cleanup_policy -- tracing_setting - alerting_setting - project_setting - webide_pipelines @@ -604,6 +605,7 @@ project: - incident_management_oncall_schedules - incident_management_oncall_rotations - incident_management_escalation_policies +- incident_management_issuable_escalation_statuses - debian_distributions - merge_request_metrics - security_orchestration_policy_configuration @@ -695,8 +697,6 @@ epic_issues: feature_flag_issues: - issue - feature_flag -tracing_setting: -- project reviews: - project - merge_request diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index 03f522ae490..3f73a730744 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -171,4 +171,27 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE) end end + + describe '#serialize_relation' do + context 'when record is a merge request' do + let(:json_writer) do + Class.new do + def write_relation_array(_, _, enumerator) + enumerator.each { _1 } + end + end.new + end + + it 'removes cached external diff' do + merge_request = create(:merge_request, source_project: exportable, target_project: exportable) + cache_dir = merge_request.merge_request_diff.send(:external_diff_cache_dir) + + expect(subject).to receive(:remove_cached_external_diff).with(merge_request).twice + + subject.serialize_relation({ merge_requests: { include: [] } }) + + expect(Dir.exist?(cache_dir)).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 87ca899a87d..d7ad34255c1 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -258,7 +258,7 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do end before do - group.add_users([user, user2], GroupMember::DEVELOPER) + group.add_members([user, user2], GroupMember::DEVELOPER) end it 'maps the project member' do @@ -281,7 +281,7 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do end before do - group.add_users([user, user2], GroupMember::DEVELOPER) + group.add_members([user, user2], GroupMember::DEVELOPER) end it 'maps the importer' do @@ -315,7 +315,7 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do shared_examples_for 'it fetches the access level from parent group' do before do - group.add_users([user], group_access_level) + group.add_members([user], group_access_level) end it "and resolves it correctly" do diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index d3397e89f1f..157cd408da9 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -383,21 +383,52 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end end - it 'restores releases with links & milestones' do - release = @project.releases.last - link = release.links.last + context 'restores releases' do + it 'with links & milestones' do + release = @project.releases.last + link = release.links.last + + aggregate_failures do + expect(release.tag).to eq('release-1.2') + expect(release.description).to eq('Some release notes') + expect(release.name).to eq('release-1.2') + expect(release.sha).to eq('903de3a8bd5573f4a049b1457d28bc1592ba6bf9') + expect(release.released_at).to eq('2019-12-27T10:17:14.615Z') + expect(release.milestone_releases.count).to eq(1) + expect(release.milestone_releases.first.milestone.title).to eq('test milestone') + + expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download') + expect(link.name).to eq('release-1.2.dmg') + end + end - aggregate_failures do - expect(release.tag).to eq('release-1.1') - expect(release.description).to eq('Some release notes') - expect(release.name).to eq('release-1.1') - expect(release.sha).to eq('901de3a8bd5573f4a049b1457d28bc1592ba6bf9') - expect(release.released_at).to eq('2019-12-26T10:17:14.615Z') - expect(release.milestone_releases.count).to eq(1) - expect(release.milestone_releases.first.milestone.title).to eq('test milestone') - - expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download') - expect(link.name).to eq('release-1.1.dmg') + context 'with author' do + it 'as ghost user when imported release author is empty' do + release = @project.releases.first + + aggregate_failures do + expect(release.tag).to eq('release-1.0') + expect(release.author_id).to eq(User.select(:id).ghost.id) + end + end + + it 'as existing member when imported release author is matched with existing user' do + release = @project.releases.second + + aggregate_failures do + expect(release.tag).to eq('release-1.1') + expect(release.author_id).to eq(@existing_members.first.id) + end + end + + it 'as import user when imported release author cannot be matched' do + release = @project.releases.last + + aggregate_failures do + expect(release.tag).to eq('release-1.2') + expect(release.author_id).to eq(@user.id) + end + end end end @@ -441,7 +472,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end it 'has a new CI build token' do - expect(Ci::Build.where(token: 'abcd')).to be_empty + expect(Ci::Build.find_by_token('abcd')).to be_nil end end @@ -568,20 +599,10 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do context 'when there is an existing build with build token' do before do - create(:ci_build, token: 'abcd') - end - - it_behaves_like 'restores project successfully', - issues: 1, - labels: 2, - label_with_priorities: 'A project label', - milestones: 1, - first_issue_labels: 1 - end - - context 'when there is an existing build with build token' do - before do - create(:ci_build, token: 'abcd') + create(:ci_build).tap do |job| + job.set_token('abcd') + job.save! + end end it_behaves_like 'restores project successfully', @@ -885,7 +906,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do context 'with group visibility' do before do group = create(:group, visibility_level: group_visibility) - group.add_users([user], GroupMember::MAINTAINER) + group.add_members([user], GroupMember::MAINTAINER) project.update!(group: group) end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d7f07a1eadf..bd60bb53d49 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -564,8 +564,6 @@ Project: - suggestion_commit_message - merge_commit_template - squash_commit_template -ProjectTracingSetting: -- external_url Author: - name ProjectFeature: diff --git a/spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb b/spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb new file mode 100644 index 00000000000..dbb753d5b9f --- /dev/null +++ b/spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Issuable::Clone::AttributesRewriter do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project1) { create(:project, :public, group: group) } + let_it_be(:project2) { create(:project, :public, group: group) } + let_it_be(:original_issue) { create(:issue, project: project1) } + + let(:new_attributes) { described_class.new(user, original_issue, project2).execute } + + context 'with missing target parent' do + it 'raises an ArgumentError' do + expect { described_class.new(user, original_issue, nil) }.to raise_error ArgumentError + end + end + + context 'setting labels' do + it 'sets labels present in the new project and group labels' do + project1_label_1 = create(:label, title: 'label1', project: project1) + project1_label_2 = create(:label, title: 'label2', project: project1) + project2_label_1 = create(:label, title: 'label1', project: project2) + group_label = create(:group_label, title: 'group_label', group: group) + create(:label, title: 'label3', project: project2) + + original_issue.update!(labels: [project1_label_1, project1_label_2, group_label]) + + expect(new_attributes[:label_ids]).to match_array([project2_label_1.id, group_label.id]) + end + + it 'does not set any labels when not used on the original issue' do + expect(new_attributes[:label_ids]).to be_empty + end + end + + context 'setting milestones' do + it 'sets milestone to nil when old issue milestone is not in the new project' do + milestone = create(:milestone, title: 'milestone', project: project1) + + original_issue.update!(milestone: milestone) + + expect(new_attributes[:milestone_id]).to be_nil + end + + it 'copies the milestone when old issue milestone title is in the new project' do + milestone_project1 = create(:milestone, title: 'milestone', project: project1) + milestone_project2 = create(:milestone, title: 'milestone', project: project2) + + original_issue.update!(milestone: milestone_project1) + + expect(new_attributes[:milestone_id]).to eq(milestone_project2.id) + end + + it 'copies the milestone when old issue milestone is a group milestone' do + milestone = create(:milestone, title: 'milestone', group: group) + + original_issue.update!(milestone: milestone) + + expect(new_attributes[:milestone_id]).to eq(milestone.id) + end + + context 'when include_milestone is false' do + let(:new_attributes) { described_class.new(user, original_issue, project2).execute(include_milestone: false) } + + it 'does not return any milestone' do + milestone = create(:milestone, title: 'milestone', group: group) + + original_issue.update!(milestone: milestone) + + expect(new_attributes[:milestone_id]).to be_nil + end + end + end + + context 'when target parent is a group' do + let(:new_attributes) { described_class.new(user, original_issue, group).execute } + + context 'setting labels' do + let(:project_label1) { create(:label, title: 'label1', project: project1) } + let!(:project_label2) { create(:label, title: 'label2', project: project1) } + let(:group_label1) { create(:group_label, title: 'group_label', group: group) } + let!(:group_label2) { create(:group_label, title: 'label2', group: group) } + + it 'keeps group labels and merges project labels where possible' do + original_issue.update!(labels: [project_label1, project_label2, group_label1]) + + expect(new_attributes[:label_ids]).to match_array([group_label1.id, group_label2.id]) + end + end + end +end diff --git a/spec/lib/gitlab/issuable/clone/copy_resource_events_service_spec.rb b/spec/lib/gitlab/issuable/clone/copy_resource_events_service_spec.rb new file mode 100644 index 00000000000..1700939f49e --- /dev/null +++ b/spec/lib/gitlab/issuable/clone/copy_resource_events_service_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Issuable::Clone::CopyResourceEventsService do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project1) { create(:project, :public, group: group) } + let_it_be(:project2) { create(:project, :public, group: group) } + let_it_be(:new_issue) { create(:issue, project: project2) } + let_it_be_with_reload(:original_issue) { create(:issue, project: project1) } + + subject { described_class.new(user, original_issue, new_issue) } + + it 'copies the resource label events' do + resource_label_events = create_list(:resource_label_event, 2, issue: original_issue) + + subject.execute + + expected = resource_label_events.map(&:label_id) + + expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected) + end + + context 'with existing milestone events' do + let!(:milestone1_project1) { create(:milestone, title: 'milestone1', project: project1) } + let!(:milestone2_project1) { create(:milestone, title: 'milestone2', project: project1) } + let!(:milestone3_project1) { create(:milestone, title: 'milestone3', project: project1) } + + let!(:milestone1_project2) { create(:milestone, title: 'milestone1', project: project2) } + let!(:milestone2_project2) { create(:milestone, title: 'milestone2', project: project2) } + + before do + original_issue.update!(milestone: milestone2_project1) + + create_event(milestone1_project1) + create_event(milestone2_project1) + create_event(nil, 'remove') + create_event(milestone3_project1) + end + + it 'copies existing resource milestone events' do + subject.execute + + new_issue_milestone_events = new_issue.reload.resource_milestone_events + expect(new_issue_milestone_events.count).to eq(3) + + expect_milestone_event( + new_issue_milestone_events.first, milestone: milestone1_project2, action: 'add', state: 'opened' + ) + expect_milestone_event( + new_issue_milestone_events.second, milestone: milestone2_project2, action: 'add', state: 'opened' + ) + expect_milestone_event( + new_issue_milestone_events.third, milestone: nil, action: 'remove', state: 'opened' + ) + end + + def create_event(milestone, action = 'add') + create(:resource_milestone_event, issue: original_issue, milestone: milestone, action: action) + end + + def expect_milestone_event(event, expected_attrs) + expect(event.milestone_id).to eq(expected_attrs[:milestone]&.id) + expect(event.action).to eq(expected_attrs[:action]) + expect(event.state).to eq(expected_attrs[:state]) + end + end + + context 'with existing state events' do + let!(:event1) { create(:resource_state_event, issue: original_issue, state: 'opened') } + let!(:event2) { create(:resource_state_event, issue: original_issue, state: 'closed') } + let!(:event3) { create(:resource_state_event, issue: original_issue, state: 'reopened') } + + it 'copies existing state events as expected' do + subject.execute + + state_events = new_issue.reload.resource_state_events + expect(state_events.size).to eq(3) + + expect_state_event(state_events.first, issue: new_issue, state: 'opened') + expect_state_event(state_events.second, issue: new_issue, state: 'closed') + expect_state_event(state_events.third, issue: new_issue, state: 'reopened') + end + + def expect_state_event(event, expected_attrs) + expect(event.issue_id).to eq(expected_attrs[:issue]&.id) + expect(event.state).to eq(expected_attrs[:state]) + end + end +end diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb index 198d2db234c..30ad24472b4 100644 --- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb +++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') } let_it_be(:current_user) { create(:user) } let_it_be(:user) { create(:user) } + let_it_be(:issue_type_id) { WorkItems::Type.default_issue_type.id } let(:iid) { 5 } let(:key) { 'PROJECT-5' } @@ -54,7 +55,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do let(:params) { { iid: iid } } - subject { described_class.new(project, jira_issue, current_user.id, params).execute } + subject { described_class.new(project, jira_issue, current_user.id, issue_type_id, params).execute } let(:expected_description) do <<~MD @@ -74,6 +75,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do expect(subject).to eq( iid: iid, project_id: project.id, + namespace_id: project.project_namespace_id, description: expected_description.strip, title: "[#{key}] #{summary}", state_id: 1, @@ -81,7 +83,8 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do created_at: created_at, author_id: current_user.id, assignee_ids: nil, - label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id) + label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id), + work_item_type_id: issue_type_id ) end diff --git a/spec/lib/gitlab/jira_import/issues_importer_spec.rb b/spec/lib/gitlab/jira_import/issues_importer_spec.rb index 565a9ad17e1..1bc052ee0b6 100644 --- a/spec/lib/gitlab/jira_import/issues_importer_spec.rb +++ b/spec/lib/gitlab/jira_import/issues_importer_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do let_it_be(:project) { create(:project) } let_it_be(:jira_import) { create(:jira_import_state, project: project, user: current_user) } let_it_be(:jira_integration) { create(:jira_integration, project: project) } + let_it_be(:default_issue_type_id) { WorkItems::Type.default_issue_type.id } subject { described_class.new(project) } @@ -47,12 +48,22 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do count.times do |i| if raise_exception_on_even_mocks && i.even? - expect(Gitlab::JiraImport::IssueSerializer).to receive(:new) - .with(project, jira_issues[i], current_user.id, { iid: next_iid + 1 }).and_raise('Some error') + expect(Gitlab::JiraImport::IssueSerializer).to receive(:new).with( + project, + jira_issues[i], + current_user.id, + default_issue_type_id, + { iid: next_iid + 1 } + ).and_raise('Some error') else next_iid += 1 - expect(Gitlab::JiraImport::IssueSerializer).to receive(:new) - .with(project, jira_issues[i], current_user.id, { iid: next_iid }).and_return(serializer) + expect(Gitlab::JiraImport::IssueSerializer).to receive(:new).with( + project, + jira_issues[i], + current_user.id, + default_issue_type_id, + { iid: next_iid } + ).and_return(serializer) end end end diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb index 58b05be6ff9..090b79c5d3c 100644 --- a/spec/lib/gitlab/lograge/custom_options_spec.rb +++ b/spec/lib/gitlab/lograge/custom_options_spec.rb @@ -25,7 +25,8 @@ RSpec.describe Gitlab::Lograge::CustomOptions do remote_ip: '192.168.1.2', ua: 'Nyxt', queue_duration_s: 0.2, - etag_route: '/etag' + etag_route: '/etag', + response_bytes: 1234 } end @@ -55,6 +56,20 @@ RSpec.describe Gitlab::Lograge::CustomOptions do expect(subject[:user_id]).to eq('test') end + it 'adds the response length' do + expect(subject[:response_bytes]).to eq(1234) + end + + context 'with log_response_length disabled' do + before do + stub_feature_flags(log_response_length: false) + end + + it 'does not add the response length' do + expect(subject).not_to include(:response_bytes) + end + end + it 'adds Cloudflare headers' do expect(subject[:cf_ray]).to eq(event.payload[:cf_ray]) expect(subject[:cf_request_id]).to eq(event.payload[:cf_request_id]) diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb index d22bef5bda9..81910773dfa 100644 --- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -11,6 +11,8 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do attribute :author attribute :project + + before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id } end end diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb new file mode 100644 index 00000000000..8b82078bcb9 --- /dev/null +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do + context 'watchdog' do + let(:logger) { instance_double(::Logger) } + let(:handler) { instance_double(described_class::NullHandler) } + + let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) } + let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) } + let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) } + + let(:sleep_time) { 0.1 } + let(:max_heap_fragmentation) { 0.2 } + + subject(:watchdog) do + described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time, + max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation) + end + + before do + allow(handler).to receive(:on_high_heap_fragmentation).and_return(true) + + allow(logger).to receive(:warn) + allow(logger).to receive(:info) + + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation) + end + + after do + watchdog.stop + end + + context 'when starting up' do + let(:fragmentation) { 0 } + let(:max_strikes) { 0 } + + it 'sets the heap fragmentation limit gauge' do + allow(Gitlab::Metrics).to receive(:gauge).and_return(heap_frag_limit_gauge) + + expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation) + end + + context 'when no settings are set in the environment' do + it 'initializes with defaults' do + watchdog = described_class.new(handler: handler, logger: logger) + + expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD) + expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES) + expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS) + end + end + + context 'when settings are passed through the environment' do + before do + stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1) + stub_env('GITLAB_MEMWD_MAX_STRIKES', 2) + stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3) + end + + it 'initializes with these settings' do + watchdog = described_class.new(handler: handler, logger: logger) + + expect(watchdog.max_heap_fragmentation).to eq(1) + expect(watchdog.max_strikes).to eq(2) + expect(watchdog.sleep_time_seconds).to eq(3) + end + end + end + + context 'when process does not exceed heap fragmentation threshold' do + let(:fragmentation) { max_heap_fragmentation - 0.1 } + let(:max_strikes) { 0 } # To rule out that we were granting too many strikes. + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when process exceeds heap fragmentation threshold permanently' do + let(:fragmentation) { max_heap_fragmentation + 0.1 } + + before do + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_total, anything, anything) + .and_return(heap_frag_violations_counter) + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything) + .and_return(heap_frag_violations_handled_counter) + allow(heap_frag_violations_counter).to receive(:increment) + allow(heap_frag_violations_handled_counter).to receive(:increment) + end + + context 'when process has not exceeded allowed number of strikes' do + let(:max_strikes) { 10 } + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'does not log any events' do + expect(logger).not_to receive(:warn) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'increments the violations counter' do + expect(heap_frag_violations_counter).to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'does not increment violations handled counter' do + expect(heap_frag_violations_handled_counter).not_to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when process exceeds the allowed number of strikes' do + let(:max_strikes) { 1 } + + it 'signals the handler and resets strike counter' do + expect(handler).to receive(:on_high_heap_fragmentation).and_return(true) + + watchdog.start + + sleep sleep_time * 3 + + expect(watchdog.strikes).to eq(0) + end + + it 'logs the event' do + expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1') + expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) + expect(logger).to receive(:warn).with({ + message: 'heap fragmentation limit exceeded', + pid: Process.pid, + worker_id: 'worker_1', + memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble', + memwd_sleep_time_s: sleep_time, + memwd_max_heap_frag: max_heap_fragmentation, + memwd_cur_heap_frag: fragmentation, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: max_strikes + 1, + memwd_rss_bytes: 1024 + }) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'increments both the violations and violations handled counters' do + expect(heap_frag_violations_counter).to receive(:increment) + expect(heap_frag_violations_handled_counter).to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + + context 'when enforce_memory_watchdog ops toggle is off' do + before do + stub_feature_flags(enforce_memory_watchdog: false) + end + + it 'always uses the NullHandler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + expect(described_class::NullHandler.instance).to( + receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true) + ) + + watchdog.start + + sleep sleep_time * 3 + end + end + end + + context 'when handler result is true' do + let(:max_strikes) { 1 } + + it 'considers the event handled and stops itself' do + expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when handler result is false' do + let(:max_strikes) { 1 } + + it 'keeps running' do + # Return true the third time to terminate the daemon. + expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true) + + watchdog.start + + sleep sleep_time * 4 + end + end + end + + context 'when process exceeds heap fragmentation threshold temporarily' do + let(:fragmentation) { max_heap_fragmentation } + let(:max_strikes) { 1 } + + before do + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return( + fragmentation - 0.1, + fragmentation + 0.2, + fragmentation - 0.1, + fragmentation + 0.1 + ) + end + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 4 + end + end + + context 'when gitlab_memory_watchdog ops toggle is off' do + let(:fragmentation) { 0 } + let(:max_strikes) { 0 } + + before do + stub_feature_flags(gitlab_memory_watchdog: false) + end + + it 'does not monitor heap fragmentation' do + expect(Gitlab::Metrics::Memory).not_to receive(:gc_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + end + end + + context 'handlers' do + context 'NullHandler' do + subject(:handler) { described_class::NullHandler.instance } + + describe '#on_high_heap_fragmentation' do + it 'does nothing' do + expect(handler.on_high_heap_fragmentation(1.0)).to be(false) + end + end + end + + context 'TermProcessHandler' do + subject(:handler) { described_class::TermProcessHandler.new(42) } + + describe '#on_high_heap_fragmentation' do + it 'sends SIGTERM to the current process' do + expect(Process).to receive(:kill).with(:TERM, 42) + + expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + end + end + end + + context 'PumaHandler' do + # rubocop: disable RSpec/VerifiedDoubles + # In tests, the Puma constant is not loaded so we cannot make this an instance_double. + let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') } + let(:puma_worker_handle) { double('worker') } + # rubocop: enable RSpec/VerifiedDoubles + + subject(:handler) { described_class::PumaHandler.new({}) } + + before do + stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class) + end + + describe '#on_high_heap_fragmentation' do + it 'invokes orderly termination via Puma API' do + expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle) + expect(puma_worker_handle).to receive(:term) + + expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + end + end + end + end +end diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb index 66fba7ab683..dc5c7eb2e55 100644 --- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb +++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb @@ -19,6 +19,7 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do allow(settings).to receive(:enabled).and_return(true) allow(settings).to receive(:port).and_return(0) allow(settings).to receive(:address).and_return('127.0.0.1') + allow(settings).to receive(:[]).with('tls_enabled').and_return(false) end after do @@ -88,6 +89,51 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do exporter end end + + context 'with TLS enabled' do + let(:test_cert) { Rails.root.join('spec/fixtures/x509_certificate.crt').to_s } + let(:test_key) { Rails.root.join('spec/fixtures/x509_certificate_pk.key').to_s } + + before do + allow(settings).to receive(:[]).with('tls_enabled').and_return(true) + allow(settings).to receive(:[]).with('tls_cert_path').and_return(test_cert) + allow(settings).to receive(:[]).with('tls_key_path').and_return(test_key) + end + + it 'injects the necessary OpenSSL config for WEBrick' do + expect(::WEBrick::HTTPServer).to receive(:new).with( + a_hash_including( + SSLEnable: true, + SSLCertificate: an_instance_of(OpenSSL::X509::Certificate), + SSLPrivateKey: an_instance_of(OpenSSL::PKey::RSA), + SSLStartImmediately: true, + SSLExtraChainCert: [] + )) + + exporter.start + end + + context 'with intermediate certificates' do + let(:test_cert) { Rails.root.join('spec/fixtures/clusters/chain_certificates.pem').to_s } + let(:test_key) { Rails.root.join('spec/fixtures/clusters/sample_key.key').to_s } + + it 'injects them in the extra chain' do + expect(::WEBrick::HTTPServer).to receive(:new).with( + a_hash_including( + SSLEnable: true, + SSLCertificate: an_instance_of(OpenSSL::X509::Certificate), + SSLPrivateKey: an_instance_of(OpenSSL::PKey::RSA), + SSLStartImmediately: true, + SSLExtraChainCert: [ + an_instance_of(OpenSSL::X509::Certificate), + an_instance_of(OpenSSL::X509::Certificate) + ] + )) + + exporter.start + end + end + end end describe 'when thread is not alive' do @@ -159,6 +205,7 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do allow(settings).to receive(:enabled).and_return(true) allow(settings).to receive(:port).and_return(0) allow(settings).to receive(:address).and_return('127.0.0.1') + allow(settings).to receive(:[]).with('tls_enabled').and_return(false) stub_const('Gitlab::Metrics::Exporter::MetricsMiddleware', fake_collector) diff --git a/spec/lib/gitlab/metrics/memory_spec.rb b/spec/lib/gitlab/metrics/memory_spec.rb new file mode 100644 index 00000000000..fd8ca3b37c6 --- /dev/null +++ b/spec/lib/gitlab/metrics/memory_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Metrics::Memory do + describe '.gc_heap_fragmentation' do + subject(:call) do + described_class.gc_heap_fragmentation( + heap_live_slots: gc_stat_heap_live_slots, + heap_eden_pages: gc_stat_heap_eden_pages + ) + end + + context 'when the Ruby heap is perfectly utilized' do + # All objects are located in a single heap page. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { 1 } + + it { is_expected.to eq(0) } + end + + context 'when the Ruby heap is greatly fragmented' do + # There is one object per heap page. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { described_class::HEAP_SLOTS_PER_PAGE } + + # The heap can never be "perfectly fragmented" because that would require + # zero objects per page. + it { is_expected.to be > 0.99 } + end + + context 'when the Ruby heap is semi-fragmented' do + # All objects are spread over two pages i.e. each page is 50% utilized. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { 2 } + + it { is_expected.to eq(0.5) } + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index dfae5aa6784..b1566ffa7b4 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -125,5 +125,11 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end + + it 'adds a heap fragmentation metric' do + expect(sampler.metrics[:heap_fragmentation]).to receive(:set).with({}, anything) + + sampler.sample + end end end diff --git a/spec/lib/gitlab/metrics/sli_spec.rb b/spec/lib/gitlab/metrics/sli_spec.rb index 102ea442b3a..d100f66be19 100644 --- a/spec/lib/gitlab/metrics/sli_spec.rb +++ b/spec/lib/gitlab/metrics/sli_spec.rb @@ -172,11 +172,11 @@ RSpec.describe Gitlab::Metrics::Sli do fake_counter end - def fake_total_counter(name) - fake_prometheus_counter("gitlab_sli:#{name}:total") + def fake_total_counter(name, separator = '_') + fake_prometheus_counter(['gitlab_sli', name, 'total'].join(separator)) end - def fake_numerator_counter(name, numerator_name) - fake_prometheus_counter("gitlab_sli:#{name}:#{numerator_name}_total") + def fake_numerator_counter(name, numerator_name, separator = '_') + fake_prometheus_counter(["gitlab_sli", name, "#{numerator_name}_total"].join(separator)) end end diff --git a/spec/lib/gitlab/pages/cache_control_spec.rb b/spec/lib/gitlab/pages/cache_control_spec.rb new file mode 100644 index 00000000000..6ed823427fb --- /dev/null +++ b/spec/lib/gitlab/pages/cache_control_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pages::CacheControl do + it 'fails with invalid type' do + expect { described_class.new(type: :unknown, id: nil) } + .to raise_error(ArgumentError, "type must be :namespace or :project") + end + + describe '.for_namespace' do + let(:subject) { described_class.for_namespace(1) } + + it { expect(subject.cache_key).to eq('pages_domain_for_namespace_1') } + + describe '#clear_cache' do + it 'clears the cache' do + expect(Rails.cache) + .to receive(:delete) + .with('pages_domain_for_namespace_1') + + subject.clear_cache + end + end + end + + describe '.for_project' do + let(:subject) { described_class.for_project(1) } + + it { expect(subject.cache_key).to eq('pages_domain_for_project_1') } + + describe '#clear_cache' do + it 'clears the cache' do + expect(Rails.cache) + .to receive(:delete) + .with('pages_domain_for_project_1') + + subject.clear_cache + end + end + end +end diff --git a/spec/lib/gitlab/pages/deployment_update_spec.rb b/spec/lib/gitlab/pages/deployment_update_spec.rb new file mode 100644 index 00000000000..cf109248f36 --- /dev/null +++ b/spec/lib/gitlab/pages/deployment_update_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pages::DeploymentUpdate do + let_it_be(:project, refind: true) { create(:project, :repository) } + + let_it_be(:old_pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } + + let(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } + let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') } + + let(:file) { fixture_file_upload("spec/fixtures/pages.zip") } + let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.zip") } + let(:empty_metadata_filename) { "spec/fixtures/pages_empty.zip.meta" } + let(:metadata_filename) { "spec/fixtures/pages.zip.meta" } + let(:metadata) { fixture_file_upload(metadata_filename) if File.exist?(metadata_filename) } + + subject(:pages_deployment_update) { described_class.new(project, build) } + + context 'for new artifacts' do + context 'for a valid job' do + let!(:artifacts_archive) { create(:ci_job_artifact, :correct_checksum, file: file, job: build) } + + before do + create(:ci_job_artifact, file_type: :metadata, file_format: :gzip, file: metadata, job: build) + + build.reload + end + + it 'is valid' do + expect(pages_deployment_update).to be_valid + end + + context 'when missing artifacts metadata' do + before do + expect(build).to receive(:artifacts_metadata?).and_return(false) + end + + it 'is invalid' do + expect(pages_deployment_update).not_to be_valid + expect(pages_deployment_update.errors.full_messages).to include('missing artifacts metadata') + end + end + end + + it 'is invalid for invalid archive' do + create(:ci_job_artifact, :archive, file: invalid_file, job: build) + + expect(pages_deployment_update).not_to be_valid + expect(pages_deployment_update.errors.full_messages).to include('missing artifacts metadata') + end + end + + describe 'maximum pages artifacts size' do + let(:metadata) { spy('metadata') } # rubocop: disable RSpec/VerifiedDoubles + + before do + file = fixture_file_upload('spec/fixtures/pages.zip') + metafile = fixture_file_upload('spec/fixtures/pages.zip.meta') + + create(:ci_job_artifact, :archive, :correct_checksum, file: file, job: build) + create(:ci_job_artifact, :metadata, file: metafile, job: build) + + allow(build).to receive(:artifacts_metadata_entry) + .and_return(metadata) + end + + context 'when maximum pages size is set to zero' do + before do + stub_application_setting(max_pages_size: 0) + end + + context "when size is above the limit" do + before do + allow(metadata).to receive(:total_size).and_return(1.megabyte) + allow(metadata).to receive(:entries).and_return([]) + end + + it 'is valid' do + expect(pages_deployment_update).to be_valid + end + end + end + + context 'when size is limited on the instance level' do + before do + stub_application_setting(max_pages_size: 100) + end + + context "when size is below the limit" do + before do + allow(metadata).to receive(:total_size).and_return(1.megabyte) + allow(metadata).to receive(:entries).and_return([]) + end + + it 'is valid' do + expect(pages_deployment_update).to be_valid + end + end + + context "when size is above the limit" do + before do + allow(metadata).to receive(:total_size).and_return(101.megabyte) + allow(metadata).to receive(:entries).and_return([]) + end + + it 'is invalid' do + expect(pages_deployment_update).not_to be_valid + expect(pages_deployment_update.errors.full_messages) + .to include('artifacts for pages are too large: 105906176') + end + end + end + end + + context 'when retrying the job' do + let!(:older_deploy_job) do + create( + :generic_commit_status, + :failed, + pipeline: pipeline, + ref: build.ref, + stage: 'deploy', + name: 'pages:deploy' + ) + end + + before do + create(:ci_job_artifact, :correct_checksum, file: file, job: build) + create(:ci_job_artifact, file_type: :metadata, file_format: :gzip, file: metadata, job: build) + build.reload + end + + it 'marks older pages:deploy jobs retried' do + expect(pages_deployment_update).to be_valid + end + end +end diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb index ac2695977c4..879c874b134 100644 --- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb +++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb @@ -15,6 +15,22 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do end end + describe '.enforced_for_type?' do + subject { described_class.enforced_for_type?(relation) } + + context 'when relation is Group' do + let(:relation) { Group.all } + + it { is_expected.to be true } + end + + context 'when relation is AuditEvent' do + let(:relation) { AuditEvent.all } + + it { is_expected.to be false } + end + end + describe '.available?' do let(:request_context) { double('request_context', params: { order_by: order_by, sort: sort }) } let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request_context) } diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb index abbb3a21cd4..c1fc73603d6 100644 --- a/spec/lib/gitlab/pagination/keyset/order_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb @@ -680,4 +680,28 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do end end end + + describe '#attribute_names' do + let(:expected_attribute_names) { %w(id name) } + let(:order) do + Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: Project.arel_table['id'].desc, + nullable: :not_nullable, + distinct: true + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'name', + order_expression: Project.arel_table['name'].desc, + nullable: :not_nullable, + distinct: true + ) + ]) + end + + subject { order.attribute_names } + + it { is_expected.to match_array(expected_attribute_names) } + end end diff --git a/spec/lib/gitlab/quick_actions/users_extractor_spec.rb b/spec/lib/gitlab/quick_actions/users_extractor_spec.rb new file mode 100644 index 00000000000..d00f52bb056 --- /dev/null +++ b/spec/lib/gitlab/quick_actions/users_extractor_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::QuickActions::UsersExtractor do + subject(:extractor) { described_class.new(current_user, project: project, group: group, target: target, text: text) } + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:target) { create(:issue, project: project) } + + let_it_be(:pancakes) { create(:user, username: 'pancakes') } + let_it_be(:waffles) { create(:user, username: 'waffles') } + let_it_be(:syrup) { create(:user, username: 'syrup') } + + before do + allow(target).to receive(:allows_multiple_assignees?).and_return(false) + end + + context 'when the text is nil' do + let(:text) { nil } + + it 'returns an empty array' do + expect(extractor.execute).to be_empty + end + end + + context 'when the text is blank' do + let(:text) { ' ' } + + it 'returns an empty array' do + expect(extractor.execute).to be_empty + end + end + + context 'when there are users to be found' do + context 'when using usernames' do + let(:text) { 'me, pancakes waffles and syrup' } + + it 'finds the users' do + expect(extractor.execute).to contain_exactly(current_user, pancakes, waffles, syrup) + end + end + + context 'when there are too many users' do + let(:text) { 'me, pancakes waffles and syrup' } + + before do + stub_const("#{described_class}::MAX_QUICK_ACTION_USERS", 2) + end + + it 'complains' do + expect { extractor.execute }.to raise_error(described_class::TooManyError) + end + end + + context 'when using references' do + let(:text) { 'me, @pancakes @waffles and @syrup' } + + it 'finds the users' do + expect(extractor.execute).to contain_exactly(current_user, pancakes, waffles, syrup) + end + end + + context 'when using a mixture of usernames and references' do + let(:text) { 'me, @pancakes waffles and @syrup' } + + it 'finds the users' do + expect(extractor.execute).to contain_exactly(current_user, pancakes, waffles, syrup) + end + end + + context 'when one or more users cannot be found' do + let(:text) { 'me, @bacon @pancakes, chicken waffles and @syrup' } + + it 'reports an error' do + expect { extractor.execute }.to raise_error(described_class::MissingError, include('bacon', 'chicken')) + end + end + + context 'when trying to find group members' do + let(:group) { create(:group, path: 'breakfast-foods') } + let(:text) { group.to_reference } + + it 'reports an error' do + [pancakes, waffles].each { group.add_developer(_1) } + + expect { extractor.execute }.to raise_error(described_class::MissingError, include('breakfast-foods')) + end + end + end +end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index e127c89c303..50ebf43a05e 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -507,7 +507,7 @@ RSpec.describe Gitlab::Redis::MultiStore do secondary_store.flushdb end - describe "command execution in a transaction" do + describe "command execution in a pipelined command" do let(:counter) { Gitlab::Metrics::NullMetric.instance } before do @@ -557,7 +557,15 @@ RSpec.describe Gitlab::Redis::MultiStore do include_examples 'verify that store contains values', :secondary_store end - describe 'return values from a transaction' do + describe 'return values from a pipelined command' do + RSpec::Matchers.define :pipeline_diff_error_with_stacktrace do |message| + match do |object| + expect(object).to be_a(Gitlab::Redis::MultiStore::PipelinedDiffError) + expect(object.backtrace).not_to be_nil + expect(object.message).to eq(message) + end + end + subject do multi_store.send(name) do |redis| redis.get(key1) @@ -585,7 +593,10 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'returns the value from the secondary store, logging an error' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( - an_instance_of(Gitlab::Redis::MultiStore::PipelinedDiffError), + pipeline_diff_error_with_stacktrace( + 'Pipelined command executed on both stores successfully but results differ between them. ' \ + "Result from the primary: [#{value1.inspect}]. Result from the secondary: [#{value2.inspect}]." + ), hash_including(command_name: name, instance_name: instance_name) ).and_call_original expect(counter).to receive(:increment).with(command: name, instance_name: instance_name) @@ -601,7 +612,10 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'returns the value from the secondary store, logging an error' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( - an_instance_of(Gitlab::Redis::MultiStore::PipelinedDiffError), + pipeline_diff_error_with_stacktrace( + 'Pipelined command executed on both stores successfully but results differ between them. ' \ + "Result from the primary: [nil]. Result from the secondary: [#{value2.inspect}]." + ), hash_including(command_name: name, instance_name: instance_name) ) expect(counter).to receive(:increment).with(command: name, instance_name: instance_name) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index d48e8183650..a3afbed18e2 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -968,4 +968,18 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('abc!abc') } it { is_expected.not_to match((['abc'] * 100).join('.') + '!') } end + + describe '.x509_subject_key_identifier_regex' do + subject { described_class.x509_subject_key_identifier_regex } + + it { is_expected.to match('AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB') } + it { is_expected.to match('CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD') } + it { is_expected.to match('79:FB:C1:E5:6B:53:8B:0A') } + it { is_expected.to match('79:fb:c1:e5:6b:53:8b:0a') } + + it { is_expected.not_to match('') } + it { is_expected.not_to match('CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:GG') } + it { is_expected.not_to match('random string') } + it { is_expected.not_to match('12321342545356434523412341245452345623453542345234523453245') } + end end diff --git a/spec/lib/gitlab/security/scan_configuration_spec.rb b/spec/lib/gitlab/security/scan_configuration_spec.rb index 1760796c5a0..774a362617a 100644 --- a/spec/lib/gitlab/security/scan_configuration_spec.rb +++ b/spec/lib/gitlab/security/scan_configuration_spec.rb @@ -15,7 +15,7 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do let(:configured) { true } context 'with a core scanner' do - where(type: %i(sast sast_iac secret_detection)) + where(type: %i(sast sast_iac secret_detection container_scanning)) with_them do it { is_expected.to be_truthy } diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 4a952a2040a..01b7270d761 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -129,7 +129,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do allow(Sidekiq).to receive(:options).and_return(timeout: 9) end - it 'return true when everything is within limit' do + it 'return true when everything is within limit', :aggregate_failures do expect(memory_killer).to receive(:get_rss).and_return(100) expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200) expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300) @@ -144,7 +144,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do expect(subject).to be true end - it 'return false when rss exceeds hard_limit_rss' do + it 'return false when rss exceeds hard_limit_rss', :aggregate_failures do expect(memory_killer).to receive(:get_rss).at_least(:once).and_return(400) expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200) expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300) @@ -159,12 +159,12 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original - expect(memory_killer).to receive(:log_rss_out_of_range).with(400, 300, 200) + expect(memory_killer).to receive(:out_of_range_description).with(400, 300, 200, true) expect(subject).to be false end - it 'return false when rss exceed hard_limit_rss after a while' do + it 'return false when rss exceed hard_limit_rss after a while', :aggregate_failures do expect(memory_killer).to receive(:get_rss).and_return(250, 400, 400) expect(memory_killer).to receive(:get_soft_limit_rss).at_least(:once).and_return(200) expect(memory_killer).to receive(:get_hard_limit_rss).at_least(:once).and_return(300) @@ -180,12 +180,13 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original expect(memory_killer).to receive(:sleep).with(check_interval_seconds) - expect(memory_killer).to receive(:log_rss_out_of_range).with(400, 300, 200) + expect(memory_killer).to receive(:out_of_range_description).with(400, 300, 200, false) + expect(memory_killer).to receive(:out_of_range_description).with(400, 300, 200, true) expect(subject).to be false end - it 'return true when rss below soft_limit_rss after a while within GRACE_BALLOON_SECONDS' do + it 'return true when rss below soft_limit_rss after a while within GRACE_BALLOON_SECONDS', :aggregate_failures do expect(memory_killer).to receive(:get_rss).and_return(250, 100) expect(memory_killer).to receive(:get_soft_limit_rss).and_return(200, 200) expect(memory_killer).to receive(:get_hard_limit_rss).and_return(300, 300) @@ -201,15 +202,15 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do expect(Gitlab::Metrics::System).to receive(:monotonic_time).twice.and_call_original expect(memory_killer).to receive(:sleep).with(check_interval_seconds) - expect(memory_killer).not_to receive(:log_rss_out_of_range) + expect(memory_killer).to receive(:out_of_range_description).with(100, 300, 200, false) expect(subject).to be true end - context 'when exceeding GRACE_BALLOON_SECONDS' do + context 'when exceeds GRACE_BALLOON_SECONDS' do let(:grace_balloon_seconds) { 0 } - it 'return false when rss exceed soft_limit_rss' do + it 'return false when rss exceed soft_limit_rss', :aggregate_failures do allow(memory_killer).to receive(:get_rss).and_return(250) allow(memory_killer).to receive(:get_soft_limit_rss).and_return(200) allow(memory_killer).to receive(:get_hard_limit_rss).and_return(300) @@ -222,8 +223,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do .with(:above_soft_limit) .and_call_original - expect(memory_killer).to receive(:log_rss_out_of_range) - .with(250, 300, 200) + expect(memory_killer).to receive(:out_of_range_description).with(250, 300, 200, true) expect(subject).to be false end @@ -318,7 +318,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do subject { memory_killer.send(:signal_pgroup, signal, explanation) } - it 'send signal to this proces if it is not group leader' do + it 'send signal to this process if it is not group leader' do expect(Process).to receive(:getpgrp).and_return(pid + 1) expect(Sidekiq.logger).to receive(:warn).once @@ -351,12 +351,34 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do let(:current_rss) { 100 } let(:soft_limit_rss) { 200 } let(:hard_limit_rss) { 300 } + let(:jid) { 1 } let(:reason) { 'rss out of range reason description' } + let(:queue) { 'default' } + let(:running_jobs) { [{ jid: jid, worker_class: 'DummyWorker' }] } + let(:worker) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + end + end + + before do + stub_const("DummyWorker", worker) + + allow(memory_killer).to receive(:get_rss).and_return(*current_rss) + allow(memory_killer).to receive(:get_soft_limit_rss).and_return(soft_limit_rss) + allow(memory_killer).to receive(:get_hard_limit_rss).and_return(hard_limit_rss) + + memory_killer.send(:refresh_state, :running) + end - subject { memory_killer.send(:log_rss_out_of_range, current_rss, hard_limit_rss, soft_limit_rss) } + subject { memory_killer.send(:log_rss_out_of_range) } it 'invoke sidekiq logger warn' do - expect(memory_killer).to receive(:out_of_range_description).with(current_rss, hard_limit_rss, soft_limit_rss).and_return(reason) + expect(memory_killer).to receive(:out_of_range_description).with(current_rss, hard_limit_rss, soft_limit_rss, true).and_return(reason) expect(Sidekiq.logger).to receive(:warn) .with( class: described_class.to_s, @@ -365,9 +387,12 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do current_rss: current_rss, hard_limit_rss: hard_limit_rss, soft_limit_rss: soft_limit_rss, - reason: reason) + reason: reason, + running_jobs: running_jobs) - subject + Gitlab::SidekiqDaemon::Monitor.instance.within_job(DummyWorker, jid, queue) do + subject + end end end @@ -375,8 +400,9 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do let(:hard_limit) { 300 } let(:soft_limit) { 200 } let(:grace_balloon_seconds) { 12 } + let(:deadline_exceeded) { true } - subject { memory_killer.send(:out_of_range_description, rss, hard_limit, soft_limit) } + subject { memory_killer.send(:out_of_range_description, rss, hard_limit, soft_limit, deadline_exceeded) } context 'when rss > hard_limit' do let(:rss) { 400 } @@ -389,9 +415,20 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do context 'when rss <= hard_limit' do let(:rss) { 300 } - it 'tells reason' do - stub_const("#{described_class}::GRACE_BALLOON_SECONDS", grace_balloon_seconds) - expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})") + context 'deadline exceeded' do + let(:deadline_exceeded) { true } + + it 'tells reason' do + stub_const("#{described_class}::GRACE_BALLOON_SECONDS", grace_balloon_seconds) + expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})") + end + end + context 'deadline not exceeded' do + let(:deadline_exceeded) { false } + + it 'tells reason' do + expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit})") + end 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 7d31979a393..117b37ffda3 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -169,6 +169,16 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do subject.call(worker, job, :test) { nil } end end + + context 'when job is interrupted' do + let(:job) { { 'interrupted_count' => 1 } } + + it 'sets sidekiq_jobs_interrupted_total metric' do + expect(interrupted_total_metric).to receive(:increment) + + subject.call(worker, job, :test) { nil } + end + end end end diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb index 2b94eaa2db9..2554a15d97e 100644 --- a/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb +++ b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb @@ -5,46 +5,83 @@ require 'spec_helper' RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do include StubENV + let(:snowplow_micro_settings) do + { + enabled: true, + address: address + } + end + + let(:address) { "gdk.test:9091" } + before do - stub_application_setting(snowplow_enabled: true) - stub_env('SNOWPLOW_MICRO_ENABLE', '1') allow(Rails.env).to receive(:development?).and_return(true) end describe '#hostname' do - context 'when SNOWPLOW_MICRO_URI is set' do + context 'when snowplow_micro config is set' do + let(:address) { '127.0.0.1:9091' } + before do - stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091') + stub_config(snowplow_micro: snowplow_micro_settings) end - it 'returns hostname URI part' do - expect(subject.hostname).to eq('gdk.test:9091') + it 'returns proper URI' do + expect(subject.hostname).to eq('127.0.0.1:9091') + expect(subject.uri.scheme).to eq('http') + end + + context 'when gitlab config has https scheme' do + before do + stub_config_setting(https: true) + end + + it 'returns proper URI' do + expect(subject.hostname).to eq('127.0.0.1:9091') + expect(subject.uri.scheme).to eq('https') + end end end - context 'when SNOWPLOW_MICRO_URI is without protocol' do + context 'when snowplow_micro config is not set' do before do - stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091') + allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting) end - it 'returns hostname URI part' do - expect(subject.hostname).to eq('gdk.test:9091') + context 'when SNOWPLOW_MICRO_URI has scheme and port' do + before do + stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091') + end + + it 'returns hostname URI part' do + expect(subject.hostname).to eq('gdk.test:9091') + end end - end - context 'when SNOWPLOW_MICRO_URI is hostname only' do - before do - stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport') + context 'when SNOWPLOW_MICRO_URI is without protocol' do + before do + stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091') + end + + it 'returns hostname URI part' do + expect(subject.hostname).to eq('gdk.test:9091') + end end - it 'returns hostname URI with default HTTP port' do - expect(subject.hostname).to eq('uriwithoutport:80') + context 'when SNOWPLOW_MICRO_URI is hostname only' do + before do + stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport') + end + + it 'returns hostname URI with default HTTP port' do + expect(subject.hostname).to eq('uriwithoutport:80') + end end - end - context 'when SNOWPLOW_MICRO_URI is not set' do - it 'returns localhost hostname' do - expect(subject.hostname).to eq('localhost:9090') + context 'when SNOWPLOW_MICRO_URI is not set' do + it 'returns localhost hostname' do + expect(subject.hostname).to eq('localhost:9090') + end end end end @@ -53,7 +90,7 @@ RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do let_it_be(:group) { create :group } before do - stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091') + stub_config(snowplow_micro: snowplow_micro_settings) end it 'includes protocol with the correct value' do diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb index 06cc2d3800c..1d4725cf405 100644 --- a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb +++ b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Tracking::Destinations::Snowplow do +RSpec.describe Gitlab::Tracking::Destinations::Snowplow, :do_not_stub_snowplow_by_default do let(:emitter) { SnowplowTracker::Emitter.new('localhost', buffer_size: 1) } let(:tracker) { SnowplowTracker::Tracker.new(emitter, SnowplowTracker::Subject.new, 'namespace', 'app_id') } diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb index fbcb9bf3e4c..ef7816aa0db 100644 --- a/spec/lib/gitlab/tracking/incident_management_spec.rb +++ b/spec/lib/gitlab/tracking/incident_management_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Tracking::IncidentManagement do described_class.track_from_params(params) end - context 'known params' do + context 'known params', :do_not_stub_snowplow_by_default do known_params = described_class.tracking_keys known_params.each do |key, values| diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index 508b33949a8..cfb83bc0528 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -93,30 +93,11 @@ RSpec.describe Gitlab::Tracking::StandardContext do end context 'with incorrect argument type' do - context 'when standard_context_type_check FF is disabled' do - before do - stub_feature_flags(standard_context_type_check: false) - end - - subject { described_class.new(project: create(:group)) } - - it 'does not call `track_and_raise_for_dev_exception`' do - expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) - snowplow_context - end - end + subject { described_class.new(project: create(:group)) } - context 'when standard_context_type_check FF is enabled' do - before do - stub_feature_flags(standard_context_type_check: true) - end - - subject { described_class.new(project: create(:group)) } - - it 'does call `track_and_raise_for_dev_exception`' do - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) - snowplow_context - end + 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 diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index cc973be8be9..dd62c832f6f 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -10,11 +10,11 @@ RSpec.describe Gitlab::Tracking do stub_application_setting(snowplow_cookie_domain: '.gitfoo.com') stub_application_setting(snowplow_app_id: '_abc123_') - described_class.instance_variable_set("@snowplow", nil) + described_class.instance_variable_set("@tracker", nil) end after do - described_class.instance_variable_set("@snowplow", nil) + described_class.instance_variable_set("@tracker", nil) end describe '.options' do @@ -34,6 +34,26 @@ RSpec.describe Gitlab::Tracking do end end + shared_examples 'delegates to SnowplowMicro destination with proper options' do + it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro + + it 'returns useful client options' do + expected_fields = { + namespace: 'gl', + hostname: 'localhost:9090', + cookieDomain: '.gitlab.com', + appId: '_abc123_', + protocol: 'http', + port: 9090, + forceSecureTracker: false, + formTracking: true, + linkClickTracking: true + } + + expect(subject.options(nil)).to match(expected_fields) + end + end + context 'when destination is Snowplow' do it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow @@ -53,26 +73,31 @@ RSpec.describe Gitlab::Tracking do context 'when destination is SnowplowMicro' do before do - stub_env('SNOWPLOW_MICRO_ENABLE', '1') allow(Rails.env).to receive(:development?).and_return(true) end - it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro + context "enabled with yml config" do + let(:snowplow_micro_settings) do + { + enabled: true, + address: "localhost:9090" + } + end - it 'returns useful client options' do - expected_fields = { - namespace: 'gl', - hostname: 'localhost:9090', - cookieDomain: '.gitlab.com', - appId: '_abc123_', - protocol: 'http', - port: 9090, - forceSecureTracker: false, - formTracking: true, - linkClickTracking: true - } + before do + stub_config(snowplow_micro: snowplow_micro_settings) + end - expect(subject.options(nil)).to match(expected_fields) + it_behaves_like 'delegates to SnowplowMicro destination with proper options' + end + + context "enabled with env variable" do + before do + allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting) + stub_env('SNOWPLOW_MICRO_ENABLE', '1') + end + + it_behaves_like 'delegates to SnowplowMicro destination with proper options' end end diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index 3021d92244e..f45005fcc9b 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -30,50 +30,31 @@ RSpec.describe Gitlab::TreeSummary do describe '#summarize' do let(:project) { create(:project, :custom_repo, files: { 'a.txt' => '' }) } - subject(:summarized) { summary.summarize } + subject(:entries) { summary.summarize } - it 'returns an array of entries, and an array of commits' do - expect(summarized).to be_a(Array) - expect(summarized.size).to eq(2) + it 'returns an array of entries' do + expect(entries).to be_a(Array) + expect(entries.size).to eq(1) - entries, commits = *summarized aggregate_failures do expect(entries).to contain_exactly( a_hash_including(file_name: 'a.txt', commit: have_attributes(id: commit.id)) ) - expect(commits).to match_array(entries.map { |entry| entry[:commit] }) - end - end - - context 'when offset is over the limit' do - let(:offset) { 100 } - - it 'returns an empty array' do - expect(summarized).to eq([[], []]) + expect(summary.resolved_commits.values).to match_array(entries.map { |entry| entry[:commit] }) end end context 'with caching', :use_clean_rails_memory_store_caching do subject { Rails.cache.fetch(key) } - context 'Repository tree cache' do - let(:key) { ['projects', project.id, 'content', commit.id, path] } - - it 'creates a cache for repository content' do - summarized - - is_expected.to eq([{ file_name: 'a.txt', type: :blob }]) - end - end - context 'Commits list cache' do let(:offset) { 0 } let(:limit) { 25 } - let(:key) { ['projects', project.id, 'last_commits', commit.id, path, offset, limit] } + let(:key) { ['projects', project.id, 'last_commits', commit.id, path, offset, limit + 1] } it 'creates a cache for commits list' do - summarized + entries is_expected.to eq('a.txt' => commit.to_hash) end @@ -93,7 +74,7 @@ RSpec.describe Gitlab::TreeSummary do let(:expected_message) { message[0...1021] + '...' } it 'truncates commit message to 1 kilobyte' do - summarized + entries is_expected.to include('long.txt' => a_hash_including(message: expected_message)) end @@ -102,7 +83,7 @@ RSpec.describe Gitlab::TreeSummary do end end - describe '#summarize (entries)' do + describe '#fetch_logs' do let(:limit) { 4 } custom_files = { @@ -116,33 +97,32 @@ RSpec.describe Gitlab::TreeSummary do let!(:project) { create(:project, :custom_repo, files: custom_files) } let(:commit) { repo.head_commit } - subject(:entries) { summary.summarize.first } + subject(:entries) { summary.fetch_logs.first } it 'summarizes the entries within the window' do is_expected.to contain_exactly( - a_hash_including(type: :tree, file_name: 'directory'), - a_hash_including(type: :blob, file_name: 'a.txt'), - a_hash_including(type: :blob, file_name: ':file'), - a_hash_including(type: :tree, file_name: ':dir') + a_hash_including('file_name' => 'directory'), + a_hash_including('file_name' => 'a.txt'), + a_hash_including('file_name' => ':file'), + a_hash_including('file_name' => ':dir') # b.txt is excluded by the limit ) end it 'references the commit and commit path in entries' do # There are 2 trees and the summary is not ordered - entry = entries.find { |entry| entry[:commit].id == commit.id } + entry = entries.find { |entry| entry['commit']['id'] == commit.id } expected_commit_path = Gitlab::Routing.url_helpers.project_commit_path(project, commit) - expect(entry[:commit]).to be_a(::Commit) - expect(entry[:commit_path]).to eq(expected_commit_path) - expect(entry[:commit_title_html]).to eq(commit.message) + expect(entry['commit_path']).to eq(expected_commit_path) + expect(entry['commit_title_html']).to eq(commit.message) end context 'in a good subdirectory' do let(:path) { 'directory' } it 'summarizes the entries in the subdirectory' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'c.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'c.txt')) end end @@ -150,7 +130,7 @@ RSpec.describe Gitlab::TreeSummary do let(:path) { ':dir' } it 'summarizes the entries in the subdirectory' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'test.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'test.txt')) end end @@ -164,7 +144,25 @@ RSpec.describe Gitlab::TreeSummary do let(:offset) { 4 } it 'returns entries from the offset' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'b.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'b.txt')) + end + end + + context 'next offset' do + subject { summary.fetch_logs.last } + + context 'when there are more entries to fetch' do + it 'returns next offset' do + is_expected.to eq(4) + end + end + + context 'when there are no more entries to fetch' do + let(:limit) { 5 } + + it 'returns next offset' do + is_expected.to be_nil + end end end end @@ -178,10 +176,11 @@ RSpec.describe Gitlab::TreeSummary do let(:project) { create(:project, :repository) } let(:commit) { repo.commit(test_commit_sha) } let(:limit) { nil } - let(:entries) { summary.summarize.first } + let(:entries) { summary.summarize } subject(:commits) do - summary.summarize.last + summary.summarize + summary.resolved_commits.values end it 'returns an Array of ::Commit objects' do @@ -227,7 +226,7 @@ RSpec.describe Gitlab::TreeSummary do let_it_be(:project) { create(:project, :empty_repo) } let_it_be(:issue) { create(:issue, project: project) } - let(:entries) { summary.summarize.first } + let(:entries) { summary.summarize } let(:entry) { entries.find { |entry| entry[:file_name] == 'issue.txt' } } before_all do @@ -264,67 +263,6 @@ RSpec.describe Gitlab::TreeSummary do end end - describe '#more?' do - let(:path) { 'tmp/more' } - - where(:num_entries, :offset, :limit, :expected_result) do - 0 | 0 | 0 | false - 0 | 0 | 1 | false - - 1 | 0 | 0 | true - 1 | 0 | 1 | false - 1 | 1 | 0 | false - 1 | 1 | 1 | false - - 2 | 0 | 0 | true - 2 | 0 | 1 | true - 2 | 0 | 2 | false - 2 | 0 | 3 | false - 2 | 1 | 0 | true - 2 | 1 | 1 | false - 2 | 2 | 0 | false - 2 | 2 | 1 | false - end - - with_them do - before do - create_file('dummy', path: 'other') if num_entries == 0 - 1.upto(num_entries) { |n| create_file(n, path: path) } - end - - subject { summary.more? } - - it { is_expected.to eq(expected_result) } - end - end - - describe '#next_offset' do - let(:path) { 'tmp/next_offset' } - - where(:num_entries, :offset, :limit, :expected_result) do - 0 | 0 | 0 | 0 - 0 | 0 | 1 | 1 - 0 | 1 | 0 | 1 - 0 | 1 | 1 | 1 - - 1 | 0 | 0 | 0 - 1 | 0 | 1 | 1 - 1 | 1 | 0 | 1 - 1 | 1 | 1 | 2 - end - - with_them do - before do - create_file('dummy', path: 'other') if num_entries == 0 - 1.upto(num_entries) { |n| create_file(n, path: path) } - end - - subject { summary.next_offset } - - it { is_expected.to eq(expected_result) } - end - end - def create_file(unique, path:) repo.create_file( project.creator, diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index 070586319a5..a1bddcb3a47 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do milestone: '14.1', default_generation: 'generation_1', key_path: 'uuid', - product_group: 'group::product analytics', + product_group: 'product_analytics', time_frame: 'none', data_source: 'database', distribution: %w(ee ce), @@ -270,7 +270,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do milestone: '14.1', default_generation: 'generation_1', key_path: 'counter.category.event', - product_group: 'group::product analytics', + product_group: 'product_analytics', time_frame: 'none', data_source: 'database', distribution: %w(ee ce), diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index 10ae94e746b..8e0fce37e46 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Usage::Metric do description: "Count of Issues created", product_section: "dev", product_stage: "plan", - product_group: "group::plan", + product_group: "plan", product_category: "issue_tracking", value_type: "number", status: "active", diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric_spec.rb deleted file mode 100644 index 8a0ce61de74..00000000000 --- a/spec/lib/gitlab/usage/metrics/instrumentations/unique_active_users_metric_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Usage::Metrics::Instrumentations::UniqueActiveUsersMetric do - let_it_be(:user1) { create(:user, last_activity_on: 1.day.ago) } - let_it_be(:user2) { create(:user, last_activity_on: 5.days.ago) } - let_it_be(:user3) { create(:user, last_activity_on: 50.days.ago) } - let_it_be(:user4) { create(:user) } - let_it_be(:user5) { create(:user, user_type: 1, last_activity_on: 5.days.ago ) } # support bot - let_it_be(:user6) { create(:user, state: 'blocked') } - - context '28d' do - let(:start) { 30.days.ago.to_date.to_s } - let(:finish) { 2.days.ago.to_date.to_s } - let(:expected_value) { 1 } - let(:expected_query) do - "SELECT COUNT(\"users\".\"id\") FROM \"users\" WHERE (\"users\".\"state\" IN ('active')) AND " \ - "(\"users\".\"user_type\" IS NULL OR \"users\".\"user_type\" IN (6, 4)) AND \"users\".\"last_activity_on\" " \ - "BETWEEN '#{start}' AND '#{finish}'" - end - - it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' } - end - - context 'all' do - let(:expected_value) { 4 } - - it_behaves_like 'a correct instrumented metric value', { time_frame: 'all' } - end -end diff --git a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb index 76548483cfa..9d2711c49c6 100644 --- a/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb +++ b/spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb @@ -46,4 +46,54 @@ RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do expect(described_class.new(['counts.ci_builds'], :with_value).build).to eq({}) end end + + context 'with broken metric definition file' do + let(:key_path) { 'counts.broken_metric_definition_test' } + let(:definitions) { [Gitlab::Usage::MetricDefinition.new(key_path, key_path: key_path)] } + + subject(:build_metric) { described_class.new([key_path], :with_value).build } + + before do + allow(Gitlab::Usage::MetricDefinition).to receive(:with_instrumentation_class).and_return(definitions) + allow_next_instance_of(Gitlab::Usage::Metric) do |instance| + allow(instance).to receive(:with_value).and_raise(error) + end + end + + context 'when instrumentation class name is incorrect' do + let(:error) { NameError.new("uninitialized constant Gitlab::Usage::Metrics::Instrumentations::IDontExists") } + + it 'tracks error and return fallback', :aggregate_failures do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(build_metric).to eql(counts: { broken_metric_definition_test: -1 }) + end + end + + context 'when instrumentation class raises TypeError' do + let(:error) { TypeError.new("nil can't be coerced into BigDecimal") } + + it 'tracks error and return fallback', :aggregate_failures do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(build_metric).to eql(counts: { broken_metric_definition_test: -1 }) + end + end + + context 'when instrumentation class raises ArgumentError' do + let(:error) { ArgumentError.new("wrong number of arguments (given 2, expected 0)") } + + it 'tracks error and return fallback', :aggregate_failures do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(build_metric).to eql(counts: { broken_metric_definition_test: -1 }) + end + end + + context 'when instrumentation class raises StandardError' do + let(:error) { StandardError.new("something went very wrong") } + + it 'tracks error and return fallback', :aggregate_failures do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(build_metric).to eql(counts: { broken_metric_definition_test: -1 }) + end + 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 dbc34681660..bfbabd858f0 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 @@ -6,6 +6,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red let(:user1) { build(:user, id: 1) } let(:user2) { build(:user, id: 2) } let(:user3) { build(:user, id: 3) } + let(:project) { build(:project) } let(:time) { Time.zone.now } shared_examples 'tracks and counts action' do @@ -15,10 +16,9 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red specify do aggregate_failures do - expect(track_action(author: user1)).to be_truthy - expect(track_action(author: user1)).to be_truthy - expect(track_action(author: user2)).to be_truthy - expect(track_action(author: user3, time: time - 3.days)).to be_truthy + expect(track_action(author: user1, project: project)).to be_truthy + expect(track_action(author: user2, project: project)).to be_truthy + expect(track_action(author: user3, time: time - 3.days, project: project)).to be_truthy expect(count_unique(date_from: time, date_to: Date.today)).to eq(2) expect(count_unique(date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3) @@ -26,7 +26,7 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end it 'does not track edit actions if author is not present' do - expect(track_action(author: nil)).to be_nil + expect(track_action(author: nil, project: project)).to be_nil end end @@ -67,16 +67,16 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end it 'can return the count of actions per user deduplicated' do - described_class.track_web_ide_edit_action(author: user1) - described_class.track_live_preview_edit_action(author: user1) - described_class.track_snippet_editor_edit_action(author: user1) - described_class.track_sfe_edit_action(author: user1) - described_class.track_web_ide_edit_action(author: user2, time: time - 2.days) - described_class.track_web_ide_edit_action(author: user3, time: time - 3.days) - described_class.track_live_preview_edit_action(author: user2, time: time - 2.days) - described_class.track_live_preview_edit_action(author: user3, time: time - 3.days) - described_class.track_snippet_editor_edit_action(author: user3, time: time - 3.days) - described_class.track_sfe_edit_action(author: user3, time: time - 3.days) + described_class.track_web_ide_edit_action(author: user1, project: project) + described_class.track_live_preview_edit_action(author: user1, project: project) + described_class.track_snippet_editor_edit_action(author: user1, project: project) + described_class.track_sfe_edit_action(author: user1, project: project) + described_class.track_web_ide_edit_action(author: user2, time: time - 2.days, project: project) + described_class.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project) + described_class.track_live_preview_edit_action(author: user2, time: time - 2.days, project: project) + described_class.track_live_preview_edit_action(author: user3, time: time - 3.days, project: project) + described_class.track_snippet_editor_edit_action(author: user3, time: time - 3.days, project: project) + described_class.track_sfe_edit_action(author: user3, time: time - 3.days, project: project) expect(described_class.count_edit_using_editor(date_from: time, date_to: Date.today)).to eq(1) expect(described_class.count_edit_using_editor(date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3) 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 77cf94daa3f..54d49b432f4 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 @@ -19,6 +19,82 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s # Monday 6th of June reference_time = Time.utc(2020, 6, 1) travel_to(reference_time) { example.run } + described_class.clear_memoization(:known_events) + end + + context 'migration to instrumentation classes data collection' do + let_it_be(:instrumented_events) do + ::Gitlab::Usage::MetricDefinition.all.map do |definition| + next unless definition.attributes[:instrumentation_class] == 'RedisHLLMetric' && definition.available? + + definition.attributes.dig(:options, :events)&.sort + end.compact.to_set + end + + def not_instrumented_events(category) + described_class + .events_for_category(category) + .sort + .reject do |event| + instrumented_events.include?([event]) + end + end + + def not_instrumented_aggregate(category) + events = described_class.events_for_category(category).sort + + return unless described_class::CATEGORIES_FOR_TOTALS.include?(category) + return unless described_class.send(:eligible_for_totals?, events) + return if instrumented_events.include?(events) + + events + end + + describe 'Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS' do + it 'includes only fully migrated categories' do + wrong_skipped_events = described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS.map do |category| + next if not_instrumented_events(category).empty? && not_instrumented_aggregate(category).nil? + + [category, [not_instrumented_events(category), not_instrumented_aggregate(category)].compact] + end.compact.to_h + + expect(wrong_skipped_events).to be_empty + end + + context 'with not instrumented category' do + let(:instrumented_events) { [] } + + it 'can detect not migrated category' do + wrong_skipped_events = described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS.map do |category| + next if not_instrumented_events(category).empty? && not_instrumented_aggregate(category).nil? + + [category, [not_instrumented_events(category), not_instrumented_aggregate(category)].compact] + end.compact.to_h + + expect(wrong_skipped_events).not_to be_empty + end + end + end + + describe '.unique_events_data' do + context 'with use_redis_hll_instrumentation_classes feature enabled' do + it 'does not include instrumented categories' do + stub_feature_flags(use_redis_hll_instrumentation_classes: true) + + expect(described_class.unique_events_data.keys) + .not_to include(*described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS) + end + end + + context 'with use_redis_hll_instrumentation_classes feature disabled' do + it 'includes instrumented categories' do + stub_feature_flags(use_redis_hll_instrumentation_classes: false) + + expect(described_class.unique_events_data.keys) + .to include(*described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS) + end + end + end end describe '.categories' do @@ -53,11 +129,40 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'growth', 'work_items', 'ci_users', - 'error_tracking' + 'error_tracking', + 'manage' ) end end + describe '.known_events' do + let(:ce_temp_dir) { Dir.mktmpdir } + let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) } + let(:ce_event) do + { + "name" => "ce_event", + "redis_slot" => "analytics", + "category" => "analytics", + "expiry" => 84, + "aggregation" => "weekly" + } + end + + before do + 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 + + it 'returns ce events' do + expect(described_class.known_events).to include(ce_event) + end + + after do + ce_temp_file.unlink + FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir) + end + end + describe 'known_events' do let(:feature) { 'test_hll_redis_counter_ff_check' } diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index cd3388701fe..3f44cfdcf27 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -82,11 +82,43 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end describe '.track_approve_mr_action' do - subject { described_class.track_approve_mr_action(user: user) } + include ProjectForksHelper + + let(:merge_request) { create(:merge_request, target_project: target_project, source_project: source_project) } + let(:source_project) { fork_project(target_project) } + let(:target_project) { create(:project) } + + subject { described_class.track_approve_mr_action(user: user, merge_request: merge_request) } it_behaves_like 'a tracked merge request unique event' do let(:action) { described_class::MR_APPROVE_ACTION } end + + it 'records correct payload with Snowplow event', :snowplow do + stub_feature_flags(route_hll_to_snowplow_phase2: true) + + subject + + expect_snowplow_event( + category: 'merge_requests', + action: 'i_code_review_user_approve_mr', + namespace: target_project.namespace, + user: user, + project: target_project + ) + end + + context 'when FF is disabled' do + before do + stub_feature_flags(route_hll_to_snowplow_phase2: false) + end + + it 'doesnt emit snowplow events', :snowplow do + subject + + expect_no_snowplow_event + end + end end describe '.track_unapprove_mr_action' do diff --git a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb index 4561d898479..0264236f087 100644 --- a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb @@ -5,46 +5,6 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter, :clean_gitlab_redis_shared_state do let(:user) { build(:user, id: 1) } - shared_examples 'counter that does not track the event' do - it 'does not track the event' do - expect { 3.times { track_event } }.to not_change { - Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( - event_names: event_name, - start_date: 2.weeks.ago, - end_date: 2.weeks.from_now - ) - } - end - end - - shared_examples 'work item unique counter' do - context 'when track_work_items_activity FF is enabled' do - it 'tracks a unique event only once' do - expect { 3.times { track_event } }.to change { - Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( - event_names: event_name, - start_date: 2.weeks.ago, - end_date: 2.weeks.from_now - ) - }.by(1) - end - - context 'when author is nil' do - let(:user) { nil } - - it_behaves_like 'counter that does not track the event' - end - end - - context 'when track_work_items_activity FF is disabled' do - before do - stub_feature_flags(track_work_items_activity: false) - end - - it_behaves_like 'counter that does not track the event' - end - end - describe '.track_work_item_created_action' do subject(:track_event) { described_class.track_work_item_created_action(author: user) } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 790f5b638b9..6eb00053b17 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -249,7 +249,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ) end - it 'includes imports usage data' do + it 'includes imports usage data', :clean_gitlab_redis_cache do for_defined_days_back do user = create(:user) @@ -347,7 +347,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do cluster = create(:cluster, user: user) project = create(:project, creator: user) create(:clusters_integrations_prometheus, cluster: cluster) - create(:project_tracing_setting) create(:project_error_tracking_setting) create(:incident) create(:incident, alert_management_alert: create(:alert_management_alert)) @@ -358,7 +357,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do clusters: 2, clusters_integrations_prometheus: 2, operations_dashboard_default_dashboard: 2, - projects_with_tracing_enabled: 2, projects_with_error_tracking_enabled: 2, projects_with_incidents: 4, projects_with_alert_incidents: 2, @@ -370,7 +368,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do clusters: 1, clusters_integrations_prometheus: 1, operations_dashboard_default_dashboard: 1, - projects_with_tracing_enabled: 1, projects_with_error_tracking_enabled: 1, projects_with_incidents: 2, projects_with_alert_incidents: 1 @@ -535,7 +532,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:groups_inheriting_slack_active]).to eq(1) expect(count_data[:projects_with_repositories_enabled]).to eq(3) expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) - expect(count_data[:projects_with_tracing_enabled]).to eq(1) expect(count_data[:projects_with_enabled_alert_integrations]).to eq(1) expect(count_data[:projects_with_terraform_reports]).to eq(2) expect(count_data[:projects_with_terraform_states]).to eq(2) @@ -564,7 +560,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:clusters_platforms_eks]).to eq(1) expect(count_data[:clusters_platforms_gke]).to eq(1) expect(count_data[:clusters_platforms_user]).to eq(1) - expect(count_data[:clusters_integrations_elastic_stack]).to eq(1) expect(count_data[:clusters_integrations_prometheus]).to eq(1) expect(count_data[:grafana_integrated_projects]).to eq(2) expect(count_data[:clusters_management_project]).to eq(1) @@ -1157,35 +1152,36 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:user2) { build(:user, id: 2) } let(:user3) { build(:user, id: 3) } let(:user4) { build(:user, id: 4) } + let(:project) { build(:project) } before do counter = Gitlab::UsageDataCounters::TrackUniqueEvents - project = Event::TARGET_TYPES[:project] + project_type = Event::TARGET_TYPES[:project] wiki = Event::TARGET_TYPES[:wiki] design = Event::TARGET_TYPES[:design] - counter.track_event(event_action: :pushed, event_target: project, author_id: 1) - counter.track_event(event_action: :pushed, event_target: project, author_id: 1) - counter.track_event(event_action: :pushed, event_target: project, author_id: 2) - counter.track_event(event_action: :pushed, event_target: project, author_id: 3) - counter.track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days) + counter.track_event(event_action: :pushed, event_target: project_type, author_id: 1) + counter.track_event(event_action: :pushed, event_target: project_type, author_id: 1) + counter.track_event(event_action: :pushed, event_target: project_type, author_id: 2) + counter.track_event(event_action: :pushed, event_target: project_type, author_id: 3) + counter.track_event(event_action: :pushed, event_target: project_type, author_id: 4, time: time - 3.days) counter.track_event(event_action: :created, event_target: wiki, author_id: 3) counter.track_event(event_action: :created, event_target: design, author_id: 3) counter.track_event(event_action: :created, event_target: design, author_id: 4) counter = Gitlab::UsageDataCounters::EditorUniqueCounter - counter.track_web_ide_edit_action(author: user1) - counter.track_web_ide_edit_action(author: user1) - counter.track_sfe_edit_action(author: user1) - counter.track_snippet_editor_edit_action(author: user1) - counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days) + counter.track_web_ide_edit_action(author: user1, project: project) + counter.track_web_ide_edit_action(author: user1, project: project) + counter.track_sfe_edit_action(author: user1, project: project) + counter.track_snippet_editor_edit_action(author: user1, project: project) + counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days, project: project) - counter.track_web_ide_edit_action(author: user2) - counter.track_sfe_edit_action(author: user2) + counter.track_web_ide_edit_action(author: user2, project: project) + counter.track_sfe_edit_action(author: user2, project: project) - counter.track_web_ide_edit_action(author: user3, time: time - 3.days) - counter.track_snippet_editor_edit_action(author: user3) + counter.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project) + counter.track_snippet_editor_edit_action(author: user3, project: project) end it 'returns the distinct count of user actions within the specified time period' do @@ -1212,6 +1208,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:ignored_metrics) { ["i_package_composer_deploy_token_weekly"] } it 'has all known_events' do + stub_feature_flags(use_redis_hll_instrumentation_classes: false) expect(subject).to have_key(:redis_hll_counters) expect(subject[:redis_hll_counters].keys).to match_array(categories) @@ -1312,8 +1309,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do "in_product_marketing_email_team_1_sent" => -1, "in_product_marketing_email_team_1_cta_clicked" => -1, "in_product_marketing_email_team_2_sent" => -1, - "in_product_marketing_email_team_2_cta_clicked" => -1, - "in_product_marketing_email_experience_0_sent" => -1 + "in_product_marketing_email_team_2_cta_clicked" => -1 } expect(subject).to eq(expected_data) @@ -1358,8 +1354,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do "in_product_marketing_email_team_1_sent" => 0, "in_product_marketing_email_team_1_cta_clicked" => 0, "in_product_marketing_email_team_2_sent" => 0, - "in_product_marketing_email_team_2_cta_clicked" => 0, - "in_product_marketing_email_experience_0_sent" => 0 + "in_product_marketing_email_team_2_cta_clicked" => 0 } expect(subject).to eq(expected_data) @@ -1368,29 +1363,11 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end describe ".with_duration" do - context 'with feature flag measure_service_ping_metric_collection turned off' do - before do - stub_feature_flags(measure_service_ping_metric_collection: false) - end - - it 'does NOT record duration and return block response' do - expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator).not_to receive(:new) - - expect(described_class.with_duration { 1 + 1 }).to be 2 - end - end + it 'records duration' do + expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator) + .to receive(:new).with(2, kind_of(Float)) - context 'with feature flag measure_service_ping_metric_collection turned off' do - before do - stub_feature_flags(measure_service_ping_metric_collection: true) - end - - it 'records duration' do - expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator) - .to receive(:new).with(2, kind_of(Float)) - - described_class.with_duration { 1 + 1 } - end + described_class.with_duration { 1 + 1 } end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index b1de3e21b77..1ae45d41f2d 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -219,19 +219,19 @@ RSpec.describe Gitlab::UserAccess do describe '#can_create_tag?' do describe 'push to none protected tag' do it 'returns true if user is a maintainer' do - project.add_user(user, :maintainer) + project.add_member(user, :maintainer) expect(access.can_create_tag?('random_tag')).to be_truthy end it 'returns true if user is a developer' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_create_tag?('random_tag')).to be_truthy end it 'returns false if user is a reporter' do - project.add_user(user, :reporter) + project.add_member(user, :reporter) expect(access.can_create_tag?('random_tag')).to be_falsey end @@ -242,19 +242,19 @@ RSpec.describe Gitlab::UserAccess do let(:not_existing_tag) { create :protected_tag, project: project } it 'returns true if user is a maintainer' do - project.add_user(user, :maintainer) + project.add_member(user, :maintainer) expect(access.can_create_tag?(tag.name)).to be_truthy end it 'returns false if user is a developer' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_create_tag?(tag.name)).to be_falsey end it 'returns false if user is a reporter' do - project.add_user(user, :reporter) + project.add_member(user, :reporter) expect(access.can_create_tag?(tag.name)).to be_falsey end @@ -266,19 +266,19 @@ RSpec.describe Gitlab::UserAccess do end it 'returns true if user is a maintainer' do - project.add_user(user, :maintainer) + project.add_member(user, :maintainer) expect(access.can_create_tag?(@tag.name)).to be_truthy end it 'returns true if user is a developer' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_create_tag?(@tag.name)).to be_truthy end it 'returns false if user is a reporter' do - project.add_user(user, :reporter) + project.add_member(user, :reporter) expect(access.can_create_tag?(@tag.name)).to be_falsey end @@ -288,19 +288,19 @@ RSpec.describe Gitlab::UserAccess do describe '#can_delete_branch?' do describe 'delete unprotected branch' do it 'returns true if user is a maintainer' do - project.add_user(user, :maintainer) + project.add_member(user, :maintainer) expect(access.can_delete_branch?('random_branch')).to be_truthy end it 'returns true if user is a developer' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_delete_branch?('random_branch')).to be_truthy end it 'returns false if user is a reporter' do - project.add_user(user, :reporter) + project.add_member(user, :reporter) expect(access.can_delete_branch?('random_branch')).to be_falsey end @@ -310,19 +310,19 @@ RSpec.describe Gitlab::UserAccess do let(:branch) { create(:protected_branch, project: project, name: "test") } it 'returns true if user is a maintainer' do - project.add_user(user, :maintainer) + project.add_member(user, :maintainer) expect(access.can_delete_branch?(branch.name)).to be_truthy end it 'returns false if user is a developer' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_delete_branch?(branch.name)).to be_falsey end it 'returns false if user is a reporter' do - project.add_user(user, :reporter) + project.add_member(user, :reporter) expect(access.can_delete_branch?(branch.name)).to be_falsey end @@ -334,7 +334,7 @@ RSpec.describe Gitlab::UserAccess do context 'when user cannot push_code to a project repository (eg. as a guest)' do it 'is false' do - project.add_user(user, :guest) + project.add_member(user, :guest) expect(access.can_push_for_ref?(ref)).to be_falsey end @@ -342,7 +342,7 @@ RSpec.describe Gitlab::UserAccess do context 'when user can push_code to a project repository (eg. as a developer)' do it 'is true' do - project.add_user(user, :developer) + project.add_member(user, :developer) expect(access.can_push_for_ref?(ref)).to be_truthy end diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index f81e3aa070a..6ed094f11c8 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -1,73 +1,170 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' -RSpec.describe 'Gitlab::VersionInfo' do +RSpec.describe Gitlab::VersionInfo do before do - @unknown = Gitlab::VersionInfo.new - @v0_0_1 = Gitlab::VersionInfo.new(0, 0, 1) - @v0_1_0 = Gitlab::VersionInfo.new(0, 1, 0) - @v1_0_0 = Gitlab::VersionInfo.new(1, 0, 0) - @v1_0_1 = Gitlab::VersionInfo.new(1, 0, 1) - @v1_1_0 = Gitlab::VersionInfo.new(1, 1, 0) - @v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0) + @unknown = described_class.new + @v0_0_1 = described_class.new(0, 0, 1) + @v0_1_0 = described_class.new(0, 1, 0) + @v1_0_0 = described_class.new(1, 0, 0) + @v1_0_1 = described_class.new(1, 0, 1) + @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1') + @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1') + @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2') + @v1_1_0 = described_class.new(1, 1, 0) + @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1') + @v2_0_0 = described_class.new(2, 0, 0) + @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true) end - context '>' do + describe '>' do it { expect(@v2_0_0).to be > @v1_1_0 } it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1_b1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 } + it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc2 } it { expect(@v1_0_1).to be > @v1_0_0 } it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 } + it { expect(@v1_1_0).to be > @v1_1_0_beta1 } it { expect(@v0_1_0).to be > @v0_0_1 } end - context '>=' do - it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } + describe '>=' do + it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) } it { expect(@v2_0_0).to be >= @v1_1_0 } + it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 } end - context '<' do + describe '<' do it { expect(@v0_0_1).to be < @v0_1_0 } it { expect(@v0_1_0).to be < @v1_0_0 } it { expect(@v1_0_0).to be < @v1_0_1 } it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_0_0).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc2).to be < @v1_0_1 } it { expect(@v1_1_0).to be < @v2_0_0 } + it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 } + it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 } end - context '<=' do - it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } + describe '<=' do + it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) } it { expect(@v0_0_1).to be <= @v0_1_0 } + it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 } + it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 } + it { expect(@v1_1_0_beta1).to be <= @v1_1_0 } end - context '==' do - it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } - it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } - it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } + describe '==' do + it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) } + it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) } end - context '!=' do + describe '!=' do it { expect(@v0_0_1).not_to eq(@v0_1_0) } + it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) } end - context 'unknown' do + describe '.unknown' do it { expect(@unknown).not_to be @v0_0_1 } - it { expect(@unknown).not_to be Gitlab::VersionInfo.new } + it { expect(@unknown).not_to be described_class.new } it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } end - context 'parse' do - it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-ee")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-rc1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("git 1.0.0b1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } + describe '.parse' do + it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0b1")).not_to be_valid } + + context 'with parse_suffix: true' do + let(:versions) do + <<-VERSIONS.lines + 0.0.1 + 0.1.0 + 1.0.0 + 1.0.1-b1 + 1.0.1-rc1 + 1.0.1-rc2 + 1.0.1 + 1.1.0-beta1 + 1.1.0 + 2.0.0 + v13.10.0-pre + v13.10.0-rc1 + v13.10.0-rc2 + v13.10.0 + v13.10.1~beta.1574.gf6ea9389 + v13.10.1~beta.1575.gf6ea9389 + v13.10.1-rc1 + v13.10.1-rc2 + v13.10.1 + VERSIONS + end + + let(:parsed_versions) do + versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) } + end + + it 'versions are returned in a correct order' do + expect(parsed_versions.shuffle.sort).to eq(parsed_versions) + end + end end - context 'to_s' do + describe '.to_s' do it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") } it { expect(@unknown.to_s).to eq("Unknown") } end + + describe '.hash' do + it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) } + end + + describe '.eql?' do + it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } + it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey } + it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy } + it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] } + end + + describe '.same_minor_version?' do + it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey } + it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy } + it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey } + it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey } + end + + describe '.without_patch' do + it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) } + it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) } + end end diff --git a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb index c0629c8d795..3152dc2ad2f 100644 --- a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb +++ b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' RSpec.describe Gitlab::WikiPages::FrontMatterParser do - subject(:parser) { described_class.new(raw_content) } + subject(:parser) { described_class.new(raw_content, gate) } let(:content) { 'This is the content' } let(:end_divider) { '---' } + let(:gate) { stub_feature_flag_gate('Gate') } let(:with_front_matter) do <<~MD @@ -61,6 +62,32 @@ RSpec.describe Gitlab::WikiPages::FrontMatterParser do it { is_expected.to have_attributes(reason: :no_match) } end + context 'the feature flag is disabled' do + let(:raw_content) { with_front_matter } + + before do + stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => false) + end + + it { is_expected.to have_attributes(front_matter: be_empty, content: raw_content) } + end + + context 'the feature flag is enabled for the gated object' do + let(:raw_content) { with_front_matter } + + before do + stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => gate) + end + + it do + is_expected.to have_attributes( + front_matter: have_correct_front_matter, + content: content + "\n", + reason: be_nil + ) + end + end + context 'the end divider is ...' do let(:end_divider) { '...' } let(:raw_content) { with_front_matter } diff --git a/spec/lib/gitlab/x509/certificate_spec.rb b/spec/lib/gitlab/x509/certificate_spec.rb index 2dc30cc871d..d919b99de2a 100644 --- a/spec/lib/gitlab/x509/certificate_spec.rb +++ b/spec/lib/gitlab/x509/certificate_spec.rb @@ -116,9 +116,69 @@ RSpec.describe Gitlab::X509::Certificate do end end + describe '.default_cert_dir' do + before do + described_class.reset_default_cert_paths + end + + after(:context) do + described_class.reset_default_cert_paths + end + + context 'when SSL_CERT_DIR env variable is not set' do + before do + stub_env('SSL_CERT_DIR', nil) + end + + it 'returns default directory from OpenSSL' do + expect(described_class.default_cert_dir).to eq(OpenSSL::X509::DEFAULT_CERT_DIR) + end + end + + context 'when SSL_CERT_DIR env variable is set' do + before do + stub_env('SSL_CERT_DIR', '/tmp/foo/certs') + end + + it 'returns specified directory' do + expect(described_class.default_cert_dir).to eq('/tmp/foo/certs') + end + end + end + + describe '.default_cert_file' do + before do + described_class.reset_default_cert_paths + end + + after(:context) do + described_class.reset_default_cert_paths + end + + context 'when SSL_CERT_FILE env variable is not set' do + before do + stub_env('SSL_CERT_FILE', nil) + end + + it 'returns default file from OpenSSL' do + expect(described_class.default_cert_file).to eq(OpenSSL::X509::DEFAULT_CERT_FILE) + end + end + + context 'when SSL_CERT_FILE env variable is set' do + before do + stub_env('SSL_CERT_FILE', '/tmp/foo/cert.pem') + end + + it 'returns specified file' do + expect(described_class.default_cert_file).to eq('/tmp/foo/cert.pem') + end + end + end + describe '.ca_certs_paths' do it 'returns all files specified by OpenSSL defaults' do - cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"] + cert_paths = Dir["#{described_class.default_cert_dir}/*"] expect(described_class.ca_certs_paths).to match_array(cert_paths + [sample_cert]) end diff --git a/spec/lib/gitlab/x509/commit_spec.rb b/spec/lib/gitlab/x509/commit_spec.rb index a81955b995e..c7d56e49fab 100644 --- a/spec/lib/gitlab/x509/commit_spec.rb +++ b/spec/lib/gitlab/x509/commit_spec.rb @@ -2,14 +2,21 @@ require 'spec_helper' RSpec.describe Gitlab::X509::Commit do - describe '#signature' do - let(:signature) { described_class.new(commit).signature } + let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } + let(:user) { create(:user, email: X509Helpers::User1.certificate_email) } + let(:project) { create(:project, :repository, path: X509Helpers::User1.path, creator: user) } + let(:commit) { project.commit_by(oid: commit_sha ) } + let(:signature) { Gitlab::X509::Commit.new(commit).signature } + let(:store) { OpenSSL::X509::Store.new } + let(:certificate) { OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert) } - context 'returns the cached signature' do - let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } - let(:project) { create(:project, :public, :repository) } - let(:commit) { create(:commit, project: project, sha: commit_sha) } + before do + store.add_cert(certificate) if certificate + allow(OpenSSL::X509::Store).to receive(:new).and_return(store) + end + describe '#signature' do + context 'returns the cached signature' do it 'on second call' do allow_any_instance_of(described_class).to receive(:new).and_call_original expect_any_instance_of(described_class).to receive(:create_cached_signature!).and_call_original @@ -23,13 +30,29 @@ RSpec.describe Gitlab::X509::Commit do end context 'unsigned commit' do - let!(:project) { create :project, :repository, path: X509Helpers::User1.path } - let!(:commit_sha) { X509Helpers::User1.commit } - let!(:commit) { create :commit, project: project, sha: commit_sha } + let(:project) { create :project, :repository, path: X509Helpers::User1.path } + let(:commit_sha) { X509Helpers::User1.commit } + let(:commit) { create :commit, project: project, sha: commit_sha } it 'returns nil' do expect(signature).to be_nil end end end + + describe '#update_signature!' do + let(:certificate) { nil } + + it 'updates verification status' do + signature + + cert = OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert) + store.add_cert(cert) + + stored_signature = CommitSignatures::X509CommitSignature.find_by_commit_sha(commit_sha) + expect { described_class.new(commit).update_signature!(stored_signature) }.to( + change { signature.reload.verification_status }.from('unverified').to('verified') + ) + end + end end diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index 0e34d5393d6..5626e49bfe1 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -107,7 +107,7 @@ RSpec.describe Gitlab::X509::Signature do f.print certificate.to_pem end - stub_const("OpenSSL::X509::DEFAULT_CERT_FILE", file_path) + allow(Gitlab::X509::Certificate).to receive(:default_cert_file).and_return(file_path) allow(OpenSSL::X509::Store).to receive(:new).and_return(store) end |