diff options
Diffstat (limited to 'spec/lib')
210 files changed, 6788 insertions, 2504 deletions
diff --git a/spec/lib/api/entities/snippet_spec.rb b/spec/lib/api/entities/snippet_spec.rb index 068851f7f6c..090f09c9b61 100644 --- a/spec/lib/api/entities/snippet_spec.rb +++ b/spec/lib/api/entities/snippet_spec.rb @@ -21,16 +21,6 @@ RSpec.describe ::API::Entities::Snippet do it { expect(subject[:visibility]).to eq snippet.visibility } it { expect(subject).to include(:author) } - context 'with snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - end - - it 'does not return files' do - expect(subject).not_to include(:files) - end - end - describe 'file_name' do it 'returns attribute from repository' do expect(subject[:file_name]).to eq snippet.blobs.first.path @@ -77,14 +67,6 @@ RSpec.describe ::API::Entities::Snippet do let(:blob) { snippet.blobs.first } let(:ref) { blob.repository.root_ref } - context 'when repository does not exist' do - it 'does not include the files attribute' do - allow(snippet).to receive(:repository_exists?).and_return(false) - - expect(subject).not_to include(:files) - end - end - shared_examples 'snippet files' do let(:file) { subject[:files].first } @@ -99,6 +81,14 @@ RSpec.describe ::API::Entities::Snippet do it 'has the raw url' do expect(file[:raw_url]).to match(raw_url) end + + context 'when repository does not exist' do + it 'returns empty array' do + allow(snippet.repository).to receive(:empty?).and_return(true) + + expect(subject[:files]).to be_empty + end + end end context 'with PersonalSnippet' do diff --git a/spec/lib/api/github/entities_spec.rb b/spec/lib/api/github/entities_spec.rb new file mode 100644 index 00000000000..00ea60c5d65 --- /dev/null +++ b/spec/lib/api/github/entities_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Github::Entities do + describe API::Github::Entities::User do + let(:user) { create(:user, username: username) } + let(:username) { 'name_of_user' } + let(:gitlab_protocol_and_host) { "#{Gitlab.config.gitlab.protocol}://#{Gitlab.config.gitlab.host}" } + let(:expected_user_url) { "#{gitlab_protocol_and_host}/#{username}" } + let(:entity) { described_class.new(user) } + + subject { entity.as_json } + + specify :aggregate_failure do + expect(subject[:id]).to eq user.id + expect(subject[:login]).to eq 'name_of_user' + expect(subject[:url]).to eq expected_user_url + expect(subject[:html_url]).to eq expected_user_url + expect(subject[:avatar_url]).to include('https://www.gravatar.com/avatar') + end + + context 'with avatar' do + let(:user) { create(:user, :with_avatar, username: username) } + + specify do + expect(subject[:avatar_url]).to include("#{gitlab_protocol_and_host}/uploads/-/system/user/avatar/") + end + end + end +end diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb index ccf96bcbad6..6d06fc3618d 100644 --- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb +++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb @@ -24,6 +24,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do shared_examples 'executing redirect' do it 'redirects to package registry' do + expect(helper).to receive(:track_event).with('npm_request_forward').once expect(helper).to receive(:registry_url).once expect(helper).to receive(:redirect).once expect(helper).to receive(:fallback).never @@ -63,6 +64,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do let(:package_type) { pkg_type } it 'raises an error' do + allow(helper).to receive(:track_event) expect { subject }.to raise_error(ArgumentError, "Can't build registry_url for package_type #{package_type}") end end diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 51a45dff6a4..8e738af0fa3 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -191,41 +191,32 @@ RSpec.describe API::Helpers do describe '#increment_unique_values' do let(:value) { '9f302fea-f828-4ca9-aef4-e10bd723c0b3' } - let(:event_name) { 'my_event' } + let(:event_name) { 'g_compliance_dashboard' } let(:unknown_event) { 'unknown' } let(:feature) { "usage_data_#{event_name}" } + before do + skip_feature_flags_yaml_validation + end + context 'with feature enabled' do before do stub_feature_flags(feature => true) end it 'tracks redis hll event' do - stub_application_setting(usage_ping_enabled: true) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(value, event_name) subject.increment_unique_values(event_name, value) end - it 'does not track event usage ping is not enabled' do - stub_application_setting(usage_ping_enabled: false) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - - subject.increment_unique_values(event_name, value) - end - it 'logs an exception for unknown event' do - stub_application_setting(usage_ping_enabled: true) - expect(Gitlab::AppLogger).to receive(:warn).with("Redis tracking event failed for event: #{unknown_event}, message: Unknown event #{unknown_event}") subject.increment_unique_values(unknown_event, value) end it 'does not track event for nil values' do - stub_application_setting(usage_ping_enabled: true) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) subject.increment_unique_values(unknown_event, nil) diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index c2dbaac7f15..45cc73974d6 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Backup::Files do let(:timestamp) { Time.utc(2017, 3, 22) } around do |example| - Timecop.freeze(timestamp) { example.run } + travel_to(timestamp) { example.run } end describe 'folders with permission' do diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb new file mode 100644 index 00000000000..9c139e9f954 --- /dev/null +++ b/spec/lib/backup/repositories_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Backup::Repositories do + let(:progress) { StringIO.new } + + subject { described_class.new(progress) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:progress).and_return(progress) + end + end + + describe '#dump' do + let_it_be(:projects) { create_list(:project, 5, :repository) } + + RSpec.shared_examples 'creates repository bundles' do + specify :aggregate_failures do + # Add data to the wiki, design repositories, and snippets, so they will be included in the dump. + create(:wiki_page, container: project) + create(:design, :with_file, issue: create(:issue, project: project)) + project_snippet = create(:project_snippet, :repository, project: project) + personal_snippet = create(:personal_snippet, :repository, author: project.owner) + + subject.dump(max_concurrency: 1, max_storage_concurrency: 1) + + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki' + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design' + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle')) + end + end + + context 'hashed storage' do + let_it_be(:project) { create(:project, :repository) } + + it_behaves_like 'creates repository bundles' + end + + context 'legacy storage' do + let_it_be(:project) { create(:project, :repository, :legacy_storage) } + + it_behaves_like 'creates repository bundles' + end + + context 'no concurrency' do + it 'creates the expected number of threads' do + expect(Thread).not_to receive(:new) + + projects.each do |project| + expect(subject).to receive(:dump_project).with(project).and_call_original + end + + subject.dump(max_concurrency: 1, max_storage_concurrency: 1) + end + + describe 'command failure' do + it 'dump_project raises an error' do + allow(subject).to receive(:dump_project).and_raise(IOError) + + expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(IOError) + end + + it 'project query raises an error' do + allow(Project).to receive_message_chain(:includes, :find_each).and_raise(ActiveRecord::StatementTimeout) + + expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(ActiveRecord::StatementTimeout) + end + end + + it 'avoids N+1 database queries' do + control_count = ActiveRecord::QueryRecorder.new do + subject.dump(max_concurrency: 1, max_storage_concurrency: 1) + end.count + + create_list(:project, 2, :repository) + + expect do + subject.dump(max_concurrency: 1, max_storage_concurrency: 1) + end.not_to exceed_query_limit(control_count) + end + end + + [4, 10].each do |max_storage_concurrency| + context "max_storage_concurrency #{max_storage_concurrency}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241701' do + let(:storage_keys) { %w[default test_second_storage] } + + before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys) + end + + it 'creates the expected number of threads' do + expect(Thread).to receive(:new) + .exactly(storage_keys.length * (max_storage_concurrency + 1)).times + .and_call_original + + projects.each do |project| + expect(subject).to receive(:dump_project).with(project).and_call_original + end + + subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) + end + + it 'creates the expected number of threads with extra max concurrency' do + expect(Thread).to receive(:new) + .exactly(storage_keys.length * (max_storage_concurrency + 1)).times + .and_call_original + + projects.each do |project| + expect(subject).to receive(:dump_project).with(project).and_call_original + end + + subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency) + end + + describe 'command failure' do + it 'dump_project raises an error' do + allow(subject).to receive(:dump_project) + .and_raise(IOError) + + expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(IOError) + end + + it 'project query raises an error' do + allow(Project).to receive_message_chain(:for_repository_storage, :includes, :find_each).and_raise(ActiveRecord::StatementTimeout) + + expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(ActiveRecord::StatementTimeout) + end + + context 'misconfigured storages' do + let(:storage_keys) { %w[test_second_storage] } + + it 'raises an error' do + expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(Backup::Error, 'repositories.storages in gitlab.yml is misconfigured') + end + end + end + + it 'avoids N+1 database queries' do + control_count = ActiveRecord::QueryRecorder.new do + subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) + end.count + + create_list(:project, 2, :repository) + + expect do + subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) + end.not_to exceed_query_limit(control_count) + end + end + end + end + + describe '#restore' do + let_it_be(:project) { create(:project) } + let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) } + let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) } + + let(:next_path_to_bundle) do + [ + Rails.root.join('spec/fixtures/lib/backup/project_repo.bundle'), + Rails.root.join('spec/fixtures/lib/backup/wiki_repo.bundle'), + Rails.root.join('spec/fixtures/lib/backup/design_repo.bundle'), + Rails.root.join('spec/fixtures/lib/backup/personal_snippet_repo.bundle'), + Rails.root.join('spec/fixtures/lib/backup/project_snippet_repo.bundle') + ].to_enum + end + + it 'restores repositories from bundles', :aggregate_failures do + allow_next_instance_of(described_class::BackupRestore) do |backup_restore| + allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next) + end + + subject.restore + + collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + + expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d']) + expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e']) + expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1']) + end + + describe 'command failure' do + before do + expect(Project).to receive(:find_each).and_yield(project) + + allow_next_instance_of(DesignManagement::Repository) do |repository| + allow(repository).to receive(:create_repository) { raise 'Fail in tests' } + end + allow_next_instance_of(Repository) do |repository| + allow(repository).to receive(:create_repository) { raise 'Fail in tests' } + end + end + + context 'hashed storage' do + it 'shows the appropriate error' do + subject.restore + + expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})") + end + end + + context 'legacy storage' do + let_it_be(:project) { create(:project, :legacy_storage) } + + it 'shows the appropriate error' do + subject.restore + + expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})") + end + end + end + + context 'restoring object pools' do + it 'schedules restoring of the pool', :sidekiq_might_not_need_inline do + pool_repository = create(:pool_repository, :failed) + pool_repository.delete_object_pool + + subject.restore + + pool_repository.reload + expect(pool_repository).not_to be_failed + expect(pool_repository.object_pool.exists?).to be(true) + end + end + + it 'cleans existing repositories' do + success_response = ServiceResponse.success(message: "Valid Snippet Repo") + allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(success_response) + + expect_next_instance_of(DesignManagement::Repository) do |repository| + expect(repository).to receive(:remove) + end + + # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo + expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args| + repository = method.call(*original_args) + + expect(repository).to receive(:remove) + + repository + end + + subject.restore + end + + context 'restoring snippets' do + before do + create(:snippet_repository, snippet: personal_snippet) + create(:snippet_repository, snippet: project_snippet) + + allow_next_instance_of(described_class::BackupRestore) do |backup_restore| + allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next) + end + end + + context 'when the repository is valid' do + it 'restores the snippet repositories' do + subject.restore + + expect(personal_snippet.snippet_repository.persisted?).to be true + expect(personal_snippet.repository).to exist + + expect(project_snippet.snippet_repository.persisted?).to be true + expect(project_snippet.repository).to exist + end + end + + context 'when repository is invalid' do + before do + error_response = ServiceResponse.error(message: "Repository has more than one branch") + allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response) + end + + it 'shows the appropriate error' do + subject.restore + + expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch") + expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch") + end + + it 'removes the snippets from the DB' do + expect { subject.restore }.to change(PersonalSnippet, :count).by(-1) + .and change(ProjectSnippet, :count).by(-1) + .and change(SnippetRepository, :count).by(-2) + end + + it 'removes the repository from disk' do + gitlab_shell = Gitlab::Shell.new + shard_name = personal_snippet.repository.shard + path = personal_snippet.disk_path + '.git' + + subject.restore + + expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false + end + end + end + end +end diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb deleted file mode 100644 index 718f38f9452..00000000000 --- a/spec/lib/backup/repository_spec.rb +++ /dev/null @@ -1,232 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Backup::Repository do - let_it_be(:project) { create(:project, :wiki_repo) } - - let(:progress) { StringIO.new } - - subject { described_class.new(progress) } - - before do - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - allow(FileUtils).to receive(:mv).and_return(true) - - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:progress).and_return(progress) - end - end - - describe '#dump' do - before do - allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys) - end - - let_it_be(:projects) { create_list(:project, 5, :wiki_repo) + [project] } - - let(:storage_keys) { %w[default test_second_storage] } - - context 'no concurrency' do - it 'creates the expected number of threads' do - expect(Thread).not_to receive(:new) - - projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original - end - - subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - end - - describe 'command failure' do - it 'dump_project raises an error' do - allow(subject).to receive(:dump_project).and_raise(IOError) - - expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(IOError) - end - - it 'project query raises an error' do - allow(Project).to receive_message_chain(:includes, :find_each).and_raise(ActiveRecord::StatementTimeout) - - expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(ActiveRecord::StatementTimeout) - end - end - - it 'avoids N+1 database queries' do - control_count = ActiveRecord::QueryRecorder.new do - subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - end.count - - create_list(:project, 2, :wiki_repo) - - expect do - subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - end.not_to exceed_query_limit(control_count) - end - end - - [4, 10].each do |max_storage_concurrency| - context "max_storage_concurrency #{max_storage_concurrency}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241701' do - it 'creates the expected number of threads' do - expect(Thread).to receive(:new) - .exactly(storage_keys.length * (max_storage_concurrency + 1)).times - .and_call_original - - projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original - end - - subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) - end - - it 'creates the expected number of threads with extra max concurrency' do - expect(Thread).to receive(:new) - .exactly(storage_keys.length * (max_storage_concurrency + 1)).times - .and_call_original - - projects.each do |project| - expect(subject).to receive(:dump_project).with(project).and_call_original - end - - subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency) - end - - describe 'command failure' do - it 'dump_project raises an error' do - allow(subject).to receive(:dump_project) - .and_raise(IOError) - - expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(IOError) - end - - it 'project query raises an error' do - allow(Project).to receive_message_chain(:for_repository_storage, :includes, :find_each).and_raise(ActiveRecord::StatementTimeout) - - expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(ActiveRecord::StatementTimeout) - end - - context 'misconfigured storages' do - let(:storage_keys) { %w[test_second_storage] } - - it 'raises an error' do - expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(Backup::Error, 'repositories.storages in gitlab.yml is misconfigured') - end - end - end - - it 'avoids N+1 database queries' do - control_count = ActiveRecord::QueryRecorder.new do - subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) - end.count - - create_list(:project, 2, :wiki_repo) - - expect do - subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) - end.not_to exceed_query_limit(control_count) - end - end - end - end - - describe '#restore' do - let(:timestamp) { Time.utc(2017, 3, 22) } - let(:temp_dirs) do - Gitlab.config.repositories.storages.map do |name, storage| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s) - end - end - end - - around do |example| - Timecop.freeze(timestamp) { example.run } - end - - after do - temp_dirs.each { |path| FileUtils.rm_rf(path) } - end - - describe 'command failure' do - before do - # Allow us to set expectations on the project directly - expect(Project).to receive(:find_each).and_yield(project) - expect(project.repository).to receive(:create_repository) { raise 'Fail in tests' } - end - - context 'hashed storage' do - it 'shows the appropriate error' do - subject.restore - - expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository") - end - end - - context 'legacy storage' do - let!(:project) { create(:project, :legacy_storage) } - - it 'shows the appropriate error' do - subject.restore - - expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository") - end - end - end - - context 'restoring object pools' do - it 'schedules restoring of the pool', :sidekiq_might_not_need_inline do - pool_repository = create(:pool_repository, :failed) - pool_repository.delete_object_pool - - subject.restore - - pool_repository.reload - expect(pool_repository).not_to be_failed - expect(pool_repository.object_pool.exists?).to be(true) - end - end - - it 'cleans existing repositories' do - wiki_repository_spy = spy(:wiki) - - allow_next_instance_of(ProjectWiki) do |project_wiki| - allow(project_wiki).to receive(:repository).and_return(wiki_repository_spy) - end - - expect_next_instance_of(Repository) do |repo| - expect(repo).to receive(:remove) - end - - subject.restore - - expect(wiki_repository_spy).to have_received(:remove) - end - end - - describe '#empty_repo?' do - context 'for a wiki' do - let(:wiki) { create(:project_wiki) } - - it 'invalidates the emptiness cache' do - expect(wiki.repository).to receive(:expire_emptiness_caches).once - - subject.send(:empty_repo?, wiki) - end - - context 'wiki repo has content' do - let!(:wiki_page) { create(:wiki_page, wiki: wiki) } - - it 'returns true, regardless of bad cache value' do - expect(subject.send(:empty_repo?, wiki)).to be(false) - end - end - - context 'wiki repo does not have content' do - it 'returns true, regardless of bad cache value' do - expect(subject.send(:empty_repo?, wiki)).to be_truthy - end - end - end - end -end diff --git a/spec/lib/banzai/filter/design_reference_filter_spec.rb b/spec/lib/banzai/filter/design_reference_filter_spec.rb index 1b558754932..847c398964a 100644 --- a/spec/lib/banzai/filter/design_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/design_reference_filter_spec.rb @@ -74,26 +74,6 @@ RSpec.describe Banzai::Filter::DesignReferenceFilter do it_behaves_like 'a no-op filter' end - - context 'design reference filter is not enabled' do - before do - stub_feature_flags(described_class::FEATURE_FLAG => false) - end - - it_behaves_like 'a no-op filter' - - it 'issues no queries' do - expect { process(input_text) }.not_to exceed_query_limit(0) - end - end - - context 'the filter is enabled for the context project' do - before do - stub_feature_flags(described_class::FEATURE_FLAG => project) - end - - it_behaves_like 'a good link reference' - end end end diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index e7b6c910b8a..35ef2abfa63 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do include FilterSpecHelper + let_it_be_with_refind(:project) { create(:project) } + shared_examples_for "external issue tracker" do it_behaves_like 'a reference containing an element node' @@ -116,7 +118,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do end context "redmine project" do - let(:project) { create(:redmine_project) } + let_it_be(:service) { create(:redmine_service, project: project) } before do project.update!(issues_enabled: false) @@ -138,7 +140,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do end context "youtrack project" do - let(:project) { create(:youtrack_project) } + let_it_be(:service) { create(:youtrack_service, project: project) } before do project.update!(issues_enabled: false) @@ -181,7 +183,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do end context "jira project" do - let(:project) { create(:jira_project) } + let_it_be(:service) { create(:jira_service, project: project) } let(:reference) { issue.to_reference } context "with right markdown" do @@ -210,7 +212,7 @@ RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do end context "ewm project" do - let_it_be(:project) { create(:ewm_project) } + let_it_be(:service) { create(:ewm_service, project: project) } before do project.update!(issues_enabled: false) diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb index 8bdb24ab08c..d29af311ee5 100644 --- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do it_behaves_like 'a metrics embed filter' around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end context 'when grafana is not configured' do diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 447802d18a7..4b8b575c1f0 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -296,6 +296,12 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do .to eq reference end + it 'link with trailing slash' do + doc = reference_filter("Fixed (#{issue_url + "/"}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(issue.to_reference(project))}</a>\.\)}) + end + it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 62b1711ee57..276fa7952be 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -5,9 +5,11 @@ require 'spec_helper' RSpec.describe Banzai::Filter::MilestoneReferenceFilter do include FilterSpecHelper - let(:parent_group) { create(:group, :public) } - let(:group) { create(:group, :public, parent: parent_group) } - let(:project) { create(:project, :public, group: group) } + let_it_be(:parent_group) { create(:group, :public) } + let_it_be(:group) { create(:group, :public, parent: parent_group) } + let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:another_project) { create(:project, :public, namespace: namespace) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -188,11 +190,9 @@ RSpec.describe Banzai::Filter::MilestoneReferenceFilter do end shared_examples 'cross-project / cross-namespace complete reference' do - let(:namespace) { create(:namespace) } - let(:another_project) { create(:project, :public, namespace: namespace) } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { "#{another_project.full_path}%#{milestone.iid}" } - let!(:result) { reference_filter("See #{reference}") } + let_it_be(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.full_path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls @@ -226,12 +226,10 @@ RSpec.describe Banzai::Filter::MilestoneReferenceFilter do end shared_examples 'cross-project / same-namespace complete reference' do - let(:namespace) { create(:namespace) } - let(:project) { create(:project, :public, namespace: namespace) } - let(:another_project) { create(:project, :public, namespace: namespace) } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { "#{another_project.full_path}%#{milestone.iid}" } - let!(:result) { reference_filter("See #{reference}") } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.full_path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls @@ -265,12 +263,10 @@ RSpec.describe Banzai::Filter::MilestoneReferenceFilter do end shared_examples 'cross project shorthand reference' do - let(:namespace) { create(:namespace) } - let(:project) { create(:project, :public, namespace: namespace) } - let(:another_project) { create(:project, :public, namespace: namespace) } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { "#{another_project.path}%#{milestone.iid}" } - let!(:result) { reference_filter("See #{reference}") } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls @@ -439,13 +435,13 @@ RSpec.describe Banzai::Filter::MilestoneReferenceFilter do context 'when milestone is open' do context 'project milestones' do - let(:milestone) { create(:milestone, project: project) } + let_it_be_with_reload(:milestone) { create(:milestone, project: project) } include_context 'project milestones' end context 'group milestones' do - let(:milestone) { create(:milestone, group: group) } + let_it_be_with_reload(:milestone) { create(:milestone, group: group) } include_context 'group milestones' end @@ -453,13 +449,13 @@ RSpec.describe Banzai::Filter::MilestoneReferenceFilter do context 'when milestone is closed' do context 'project milestones' do - let(:milestone) { create(:milestone, :closed, project: project) } + let_it_be_with_reload(:milestone) { create(:milestone, :closed, project: project) } include_context 'project milestones' end context 'group milestones' do - let(:milestone) { create(:milestone, :closed, group: group) } + let_it_be_with_reload(:milestone) { create(:milestone, :closed, group: group) } include_context 'group milestones' end diff --git a/spec/lib/banzai/reference_redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb index de774267b81..668e427cfa2 100644 --- a/spec/lib/banzai/reference_redactor_spec.rb +++ b/spec/lib/banzai/reference_redactor_spec.rb @@ -182,5 +182,12 @@ RSpec.describe Banzai::ReferenceRedactor do expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node])) end + + it 'handles invalid references gracefully' do + doc = Nokogiri::HTML.fragment('<a data-reference-type="some_invalid_type"></a>') + node = doc.children[0] + + expect(redactor.nodes_visible_to_user([node])).to be_empty + end end end diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb index 49224cf4279..fa0207d829a 100644 --- a/spec/lib/feature/definition_spec.rb +++ b/spec/lib/feature/definition_spec.rb @@ -105,6 +105,7 @@ RSpec.describe Feature::Definition do describe '.load_all!' do let(:store1) { Dir.mktmpdir('path1') } let(:store2) { Dir.mktmpdir('path2') } + let(:definitions) { {} } before do allow(described_class).to receive(:paths).and_return( @@ -115,28 +116,30 @@ RSpec.describe Feature::Definition do ) end + subject { described_class.send(:load_all!) } + it "when there's no feature flags a list of definitions is empty" do - expect(described_class.load_all!).to be_empty + is_expected.to be_empty end it "when there's a single feature flag it properly loads them" do write_feature_flag(store1, path, yaml_content) - expect(described_class.load_all!).to be_one + is_expected.to be_one end it "when the same feature flag is stored multiple times raises exception" do write_feature_flag(store1, path, yaml_content) write_feature_flag(store2, path, yaml_content) - expect { described_class.load_all! } + expect { subject } .to raise_error(/Feature flag 'feature_flag' is already defined/) end it "when one of the YAMLs is invalid it does raise exception" do write_feature_flag(store1, path, '{}') - expect { described_class.load_all! } + expect { subject } .to raise_error(/Feature flag is missing name/) end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index acd7d97ac85..5dff9dbd995 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Feature, stub_feature_flags: false do before do # reset Flipper AR-engine Feature.reset + skip_feature_flags_yaml_validation end describe '.get' do @@ -253,6 +254,9 @@ RSpec.describe Feature, stub_feature_flags: false do end before do + stub_env('LAZILY_CREATE_FEATURE_FLAG', '0') + + allow(Feature::Definition).to receive(:valid_usage!).and_call_original allow(Feature::Definition).to receive(:definitions) do { definition.key => definition } end diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb index 6f6b3055df5..c47c03d6780 100644 --- a/spec/lib/forever_spec.rb +++ b/spec/lib/forever_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Forever do subject { described_class.date } it 'returns Postgresql future date' do - Timecop.travel(Date.new(2999, 12, 31)) do + travel_to(Date.new(2999, 12, 31)) do is_expected.to be > Date.today end end diff --git a/spec/lib/gitlab/alert_management/alert_params_spec.rb b/spec/lib/gitlab/alert_management/alert_params_spec.rb deleted file mode 100644 index c3171be5e29..00000000000 --- a/spec/lib/gitlab/alert_management/alert_params_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::AlertManagement::AlertParams do - let_it_be(:project) { create(:project, :repository, :private) } - - describe '.from_generic_alert' do - let(:started_at) { Time.current.change(usec: 0).rfc3339 } - let(:default_payload) do - { - 'title' => 'Alert title', - 'description' => 'Description', - 'monitoring_tool' => 'Monitoring tool name', - 'service' => 'Service', - 'hosts' => ['gitlab.com'], - 'start_time' => started_at, - 'some' => { 'extra' => { 'payload' => 'here' } } - } - end - - let(:payload) { default_payload } - - subject { described_class.from_generic_alert(project: project, payload: payload) } - - it 'returns Alert compatible parameters' do - is_expected.to eq( - project_id: project.id, - title: 'Alert title', - description: 'Description', - monitoring_tool: 'Monitoring tool name', - service: 'Service', - severity: 'critical', - hosts: ['gitlab.com'], - payload: payload, - started_at: started_at, - ended_at: nil, - fingerprint: nil, - environment: nil - ) - end - - context 'when severity given' do - let(:payload) { default_payload.merge(severity: 'low') } - - it 'returns Alert compatible parameters' do - expect(subject[:severity]).to eq('low') - end - end - - context 'when there are no hosts in the payload' do - let(:payload) { {} } - - it 'hosts param is an empty array' do - expect(subject[:hosts]).to be_empty - end - end - end - - describe '.from_prometheus_alert' do - let(:payload) do - { - 'status' => 'firing', - 'labels' => { - 'alertname' => 'GitalyFileServerDown', - 'channel' => 'gitaly', - 'pager' => 'pagerduty', - 'severity' => 's1' - }, - 'annotations' => { - 'description' => 'Alert description', - 'runbook' => 'troubleshooting/gitaly-down.md', - 'title' => 'Alert title' - }, - 'startsAt' => '2020-04-27T10:10:22.265949279Z', - 'endsAt' => '0001-01-01T00:00:00Z', - 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1', - 'fingerprint' => 'b6ac4d42057c43c1' - } - end - - let(:parsed_alert) { Gitlab::Alerting::Alert.new(project: project, payload: payload) } - - subject { described_class.from_prometheus_alert(project: project, parsed_alert: parsed_alert) } - - it 'returns Alert-compatible params' do - is_expected.to eq( - project_id: project.id, - title: 'Alert title', - description: 'Alert description', - monitoring_tool: 'Prometheus', - payload: payload, - started_at: parsed_alert.starts_at, - ended_at: parsed_alert.ends_at, - fingerprint: parsed_alert.gitlab_fingerprint, - environment: parsed_alert.environment, - prometheus_alert: parsed_alert.gitlab_alert - ) - end - end -end diff --git a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb index a2b8f0aa8d4..fceda763717 100644 --- a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb +++ b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::AlertManagement::AlertStatusCounts do expect(counts.open).to eq(0) expect(counts.all).to eq(0) - AlertManagement::Alert::STATUSES.each_key do |status| + ::AlertManagement::Alert.status_names.each do |status| expect(counts.send(status)).to eq(0) end end @@ -39,7 +39,7 @@ RSpec.describe Gitlab::AlertManagement::AlertStatusCounts do end context 'when filtering params are included' do - let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } } + let(:params) { { status: :resolved } } it 'returns the correct counts for each status' do expect(counts.open).to eq(0) diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb index e0f63bad05d..0c26e94e596 100644 --- a/spec/lib/gitlab/alert_management/payload/base_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -120,14 +120,107 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do end describe '#alert_params' do - before do - allow(parsed_payload).to receive(:title).and_return('title') - allow(parsed_payload).to receive(:description).and_return('description') + subject { parsed_payload.alert_params } + + context 'with every key' do + let_it_be(:raw_payload) { { 'key' => 'value' } } + let_it_be(:stubs) do + { + description: 'description', + ends_at: Time.current, + environment: create(:environment, project: project), + gitlab_fingerprint: 'gitlab_fingerprint', + hosts: 'hosts', + monitoring_tool: 'monitoring_tool', + gitlab_alert: create(:prometheus_alert, project: project), + service: 'service', + severity: 'critical', + starts_at: Time.current, + title: 'title' + } + end + + let(:expected_result) do + { + description: stubs[:description], + ended_at: stubs[:ends_at], + environment: stubs[:environment], + fingerprint: stubs[:gitlab_fingerprint], + hosts: [stubs[:hosts]], + monitoring_tool: stubs[:monitoring_tool], + payload: raw_payload, + project_id: project.id, + prometheus_alert: stubs[:gitlab_alert], + service: stubs[:service], + severity: stubs[:severity], + started_at: stubs[:starts_at], + title: stubs[:title] + } + end + + before do + allow(parsed_payload).to receive_messages(stubs) + end + + it { is_expected.to eq(expected_result) } + + it 'can generate a valid new alert' do + expect(::AlertManagement::Alert.new(subject.except(:ended_at))).to be_valid + end end - subject { parsed_payload.alert_params } + context 'with too-long strings' do + let_it_be(:stubs) do + { + description: 'a' * (::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH + 1), + hosts: 'b' * (::AlertManagement::Alert::HOSTS_MAX_LENGTH + 1), + monitoring_tool: 'c' * (::AlertManagement::Alert::TOOL_MAX_LENGTH + 1), + service: 'd' * (::AlertManagement::Alert::SERVICE_MAX_LENGTH + 1), + title: 'e' * (::AlertManagement::Alert::TITLE_MAX_LENGTH + 1) + } + end - it { is_expected.to eq({ description: 'description', project_id: project.id, title: 'title' }) } + before do + allow(parsed_payload).to receive_messages(stubs) + end + + it do + is_expected.to eq({ + description: stubs[:description].truncate(AlertManagement::Alert::DESCRIPTION_MAX_LENGTH), + hosts: ['b' * ::AlertManagement::Alert::HOSTS_MAX_LENGTH], + monitoring_tool: stubs[:monitoring_tool].truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH), + service: stubs[:service].truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH), + project_id: project.id, + title: stubs[:title].truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH) + }) + end + end + + context 'with too-long hosts array' do + let(:hosts) { %w(abc def ghij) } + let(:shortened_hosts) { %w(abc def ghi) } + + before do + stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9) + allow(parsed_payload).to receive(:hosts).and_return(hosts) + end + + it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) } + + context 'with host cut off between elements' do + let(:hosts) { %w(abcde fghij) } + let(:shortened_hosts) { %w(abcde fghi) } + + it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) } + end + + context 'with nested hosts' do + let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray + let(:shortened_hosts) { %w(abc de f g hi) } + + it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) } + end + end end describe '#gitlab_fingerprint' do diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb index 538a822503e..b7660462b0d 100644 --- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do subject { parsed_payload.starts_at } around do |example| - Timecop.freeze(current_time) { example.run } + travel_to(current_time) { example.run } end context 'without start_time' do @@ -86,4 +86,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do it_behaves_like 'parsable alert payload field', 'gitlab_environment_name' end + + describe '#description' do + subject { parsed_payload.description } + + it_behaves_like 'parsable alert payload field', 'description' + end + + describe '#ends_at' do + let(:current_time) { Time.current.change(usec: 0).utc } + + subject { parsed_payload.ends_at } + + around do |example| + travel_to(current_time) { example.run } + end + + context 'without end_time' do + it { is_expected.to be_nil } + end + + context "with end_time" do + let(:value) { 10.minutes.ago.change(usec: 0).utc } + + before do + raw_payload['end_time'] = value.to_s + end + + it { is_expected.to eq(value) } + end + end end diff --git a/spec/lib/gitlab/alerting/alert_spec.rb b/spec/lib/gitlab/alerting/alert_spec.rb deleted file mode 100644 index b53b71e3f3e..00000000000 --- a/spec/lib/gitlab/alerting/alert_spec.rb +++ /dev/null @@ -1,299 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Alerting::Alert do - let_it_be(:project) { create(:project) } - - let(:alert) { build(:alerting_alert, project: project, payload: payload) } - let(:payload) { {} } - - shared_context 'gitlab alert' do - let!(:gitlab_alert) { create(:prometheus_alert, project: project) } - let(:gitlab_alert_id) { gitlab_alert.id } - - before do - payload['labels'] = { - 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s, - 'gitlab_prometheus_alert_id' => gitlab_alert_id - } - end - end - - shared_context 'full query' do - before do - payload['generatorURL'] = 'http://localhost:9090/graph?g0.expr=vector%281%29' - end - end - - shared_examples 'invalid alert' do - it 'is invalid' do - expect(alert).not_to be_valid - end - end - - shared_examples 'parse payload' do |*pairs| - context 'without payload' do - it { is_expected.to be_nil } - end - - pairs.each do |pair| - context "with #{pair}" do - let(:value) { 'some value' } - - before do - section, name = pair.split('/') - payload[section] = { name => value } - end - - it { is_expected.to eq(value) } - end - end - end - - describe '#gitlab_alert' do - subject { alert.gitlab_alert } - - context 'without payload' do - it { is_expected.to be_nil } - end - - context 'with gitlab alert' do - include_context 'gitlab alert' - - it { is_expected.to eq(gitlab_alert) } - end - - context 'with unknown gitlab alert' do - include_context 'gitlab alert' do - let(:gitlab_alert_id) { 'unknown' } - end - - it { is_expected.to be_nil } - end - - context 'when two alerts with the same metric exist' do - include_context 'gitlab alert' - - let!(:second_gitlab_alert) do - create(:prometheus_alert, - project: project, - prometheus_metric_id: gitlab_alert.prometheus_metric_id - ) - end - - context 'alert id given in params' do - before do - payload['labels'] = { - 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s, - 'gitlab_prometheus_alert_id' => second_gitlab_alert.id - } - end - - it { is_expected.to eq(second_gitlab_alert) } - end - - context 'metric id given in params' do - # This tests the case when two alerts are found, as metric id - # is not unique. - - # Note the metric id was incorrectly named as 'gitlab_alert_id' - # in PrometheusAlert#to_param. - before do - payload['labels'] = { 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id } - end - - it { is_expected.to be_nil } - end - end - end - - describe '#title' do - subject { alert.title } - - it_behaves_like 'parse payload', - 'annotations/title', - 'annotations/summary', - 'labels/alertname' - - context 'with gitlab alert' do - include_context 'gitlab alert' - - context 'with annotations/title' do - let(:value) { 'annotation title' } - - before do - payload['annotations'] = { 'title' => value } - end - - it { is_expected.to eq(gitlab_alert.title) } - end - end - end - - describe '#description' do - subject { alert.description } - - it_behaves_like 'parse payload', 'annotations/description' - end - - describe '#annotations' do - subject { alert.annotations } - - context 'without payload' do - it { is_expected.to eq([]) } - end - - context 'with payload' do - before do - payload['annotations'] = { 'foo' => 'value1', 'bar' => 'value2' } - end - - it 'parses annotations' do - expect(subject.size).to eq(2) - expect(subject.map(&:label)).to eq(%w[foo bar]) - expect(subject.map(&:value)).to eq(%w[value1 value2]) - end - end - end - - describe '#environment' do - subject { alert.environment } - - context 'without gitlab_alert' do - it { is_expected.to be_nil } - end - - context 'with gitlab alert' do - include_context 'gitlab alert' - - it { is_expected.to eq(gitlab_alert.environment) } - end - end - - describe '#starts_at' do - subject { alert.starts_at } - - context 'with empty startsAt' do - before do - payload['startsAt'] = nil - end - - it { is_expected.to be_nil } - end - - context 'with invalid startsAt' do - before do - payload['startsAt'] = 'invalid' - end - - it { is_expected.to be_nil } - end - - context 'with payload' do - let(:time) { Time.current.change(usec: 0) } - - before do - payload['startsAt'] = time.rfc3339 - end - - it { is_expected.to eq(time) } - end - end - - describe '#full_query' do - using RSpec::Parameterized::TableSyntax - - subject { alert.full_query } - - where(:generator_url, :expected_query) do - nil | nil - 'http://localhost' | nil - 'invalid url' | nil - 'http://localhost:9090/graph?g1.expr=vector%281%29' | nil - 'http://localhost:9090/graph?g0.expr=vector%281%29' | 'vector(1)' - end - - with_them do - before do - payload['generatorURL'] = generator_url - end - - it { is_expected.to eq(expected_query) } - end - - context 'with gitlab alert' do - include_context 'gitlab alert' - include_context 'full query' - - it { is_expected.to eq(gitlab_alert.full_query) } - end - end - - describe '#y_label' do - subject { alert.y_label } - - it_behaves_like 'parse payload', 'annotations/gitlab_y_label' - - context 'when y_label is not included in the payload' do - it_behaves_like 'parse payload', 'annotations/title' - end - end - - describe '#alert_markdown' do - subject { alert.alert_markdown } - - it_behaves_like 'parse payload', 'annotations/gitlab_incident_markdown' - end - - describe '#gitlab_fingerprint' do - subject { alert.gitlab_fingerprint } - - context 'when the alert is a GitLab managed alert' do - include_context 'gitlab alert' - - it 'returns a fingerprint' do - plain_fingerprint = [alert.metric_id, alert.starts_at_raw].join('/') - - is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint)) - end - end - - context 'when the alert is from self managed Prometheus' do - include_context 'full query' - - it 'returns a fingerprint' do - plain_fingerprint = [alert.starts_at_raw, alert.title, alert.full_query].join('/') - - is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint)) - end - end - end - - describe '#valid?' do - before do - payload.update( - 'annotations' => { 'title' => 'some title' }, - 'startsAt' => Time.current.rfc3339 - ) - end - - subject { alert } - - it { is_expected.to be_valid } - - context 'without project' do - let(:project) { nil } - - it { is_expected.not_to be_valid } - end - - context 'without starts_at' do - before do - payload['startsAt'] = nil - end - - it { is_expected.not_to be_valid } - end - end -end diff --git a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb deleted file mode 100644 index ff5ab1116fa..00000000000 --- a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb +++ /dev/null @@ -1,204 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Alerting::NotificationPayloadParser do - let_it_be(:project) { build(:project) } - - describe '.call' do - let(:starts_at) { Time.current.change(usec: 0) } - let(:ends_at) { Time.current.change(usec: 0) } - let(:payload) do - { - 'title' => 'alert title', - 'start_time' => starts_at.rfc3339, - 'end_time' => ends_at.rfc3339, - 'description' => 'Description', - 'monitoring_tool' => 'Monitoring tool name', - 'service' => 'Service', - 'hosts' => ['gitlab.com'], - 'severity' => 'low' - } - end - - subject { described_class.call(payload, project) } - - it 'returns Prometheus-like payload' do - is_expected.to eq( - { - 'annotations' => { - 'title' => 'alert title', - 'description' => 'Description', - 'monitoring_tool' => 'Monitoring tool name', - 'service' => 'Service', - 'hosts' => ['gitlab.com'], - 'severity' => 'low' - }, - 'startsAt' => starts_at.rfc3339, - 'endsAt' => ends_at.rfc3339 - } - ) - end - - context 'when title is blank' do - before do - payload[:title] = '' - end - - it 'sets a predefined title' do - expect(subject.dig('annotations', 'title')).to eq('New: Incident') - end - end - - context 'when hosts attribute is a string' do - before do - payload[:hosts] = 'gitlab.com' - end - - it 'returns hosts as an array of one element' do - expect(subject.dig('annotations', 'hosts')).to eq(['gitlab.com']) - end - end - - context 'when the time is in unsupported format' do - before do - payload[:start_time] = 'invalid/date/format' - end - - it 'sets startsAt to a current time in RFC3339 format' do - expect(subject['startsAt']).to eq(starts_at.rfc3339) - end - end - - context 'when payload is blank' do - let(:payload) { {} } - - it 'returns default parameters' do - is_expected.to match( - 'annotations' => { - 'title' => described_class::DEFAULT_TITLE, - 'severity' => described_class::DEFAULT_SEVERITY - }, - 'startsAt' => starts_at.rfc3339 - ) - end - - context 'when severity is blank' do - before do - payload[:severity] = '' - end - - it 'sets severity to the default ' do - expect(subject.dig('annotations', 'severity')).to eq(described_class::DEFAULT_SEVERITY) - end - end - end - - context 'with fingerprint' do - before do - payload[:fingerprint] = data - end - - shared_examples 'fingerprint generation' do - it 'generates the fingerprint correctly' do - expect(result).to eq(Gitlab::AlertManagement::Fingerprint.generate(data)) - end - end - - context 'with blank fingerprint' do - it_behaves_like 'fingerprint generation' do - let(:data) { ' ' } - let(:result) { subject.dig('annotations', 'fingerprint') } - end - end - - context 'with fingerprint given' do - it_behaves_like 'fingerprint generation' do - let(:data) { 'fingerprint' } - let(:result) { subject.dig('annotations', 'fingerprint') } - end - end - - context 'with array fingerprint given' do - it_behaves_like 'fingerprint generation' do - let(:data) { [1, 'fingerprint', 'given'] } - let(:result) { subject.dig('annotations', 'fingerprint') } - end - end - end - - context 'with environment' do - let(:environment) { create(:environment, project: project) } - - before do - payload[:gitlab_environment_name] = environment.name - end - - it 'sets the environment ' do - expect(subject.dig('annotations', 'environment')).to eq(environment) - end - end - - context 'when payload attributes have blank lines' do - let(:payload) do - { - 'title' => '', - 'start_time' => '', - 'end_time' => '', - 'description' => '', - 'monitoring_tool' => '', - 'service' => '', - 'hosts' => [''] - } - end - - it 'returns default parameters' do - is_expected.to eq( - 'annotations' => { - 'title' => 'New: Incident', - 'severity' => described_class::DEFAULT_SEVERITY - }, - 'startsAt' => starts_at.rfc3339 - ) - end - end - - context 'when payload has secondary params' do - let(:payload) do - { - 'description' => 'Description', - 'additional' => { - 'params' => { - '1' => 'Some value 1', - '2' => 'Some value 2', - 'blank' => '' - } - } - } - end - - it 'adds secondary params to annotations' do - is_expected.to eq( - 'annotations' => { - 'title' => 'New: Incident', - 'severity' => described_class::DEFAULT_SEVERITY, - 'description' => 'Description', - 'additional.params.1' => 'Some value 1', - 'additional.params.2' => 'Some value 2' - }, - 'startsAt' => starts_at.rfc3339 - ) - end - end - - context 'when secondary params hash is too big' do - before do - allow(Gitlab::Utils::SafeInlineHash).to receive(:merge_keys!).and_raise(ArgumentError) - end - - it 'catches and re-raises an error' do - expect { subject }.to raise_error Gitlab::Alerting::NotificationPayloadParser::BadPayloadError, 'The payload is too big' - end - end - end -end diff --git a/spec/lib/gitlab/analytics/unique_visits_spec.rb b/spec/lib/gitlab/analytics/unique_visits_spec.rb index 1432c9ac58f..6ac58e13f4c 100644 --- a/spec/lib/gitlab/analytics/unique_visits_spec.rb +++ b/spec/lib/gitlab/analytics/unique_visits_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state # Without freezing the time, the test may behave inconsistently # depending on which day of the week test is run. reference_time = Time.utc(2020, 6, 1) - Timecop.freeze(reference_time) { example.run } + travel_to(reference_time) { example.run } end describe '#track_visit' do diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 1ac8ebe1369..2ebde145bfd 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -419,10 +419,30 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect(find_user_from_web_access_token(:ics)).to eq(user) end - it 'returns the user for API requests' do - set_header('SCRIPT_NAME', '/api/endpoint') + context 'for API requests' do + it 'returns the user' do + set_header('SCRIPT_NAME', '/api/endpoint') + + expect(find_user_from_web_access_token(:api)).to eq(user) + end + + it 'returns nil if URL does not start with /api/' do + set_header('SCRIPT_NAME', '/relative_root/api/endpoint') + + expect(find_user_from_web_access_token(:api)).to be_nil + end - expect(find_user_from_web_access_token(:api)).to eq(user) + context 'when relative_url_root is set' do + before do + stub_config_setting(relative_url_root: '/relative_root') + end + + it 'returns the user' do + set_header('SCRIPT_NAME', '/relative_root/api/endpoint') + + expect(find_user_from_web_access_token(:api)).to eq(user) + end + end end end diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index 60b403780c0..ffd7813190a 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -121,7 +121,7 @@ RSpec.describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_ subject.enable_admin_mode!(password: user.password) expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present' - Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do + travel_to(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do # in the future this will be a new request, simulate by clearing the RequestStore Gitlab::SafeRequestStore.clear! diff --git a/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb new file mode 100644 index 00000000000..0c88421d456 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::Devise do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + subject(:validate) { described_class.new(user).validate(otp_code) } + + it 'calls Devise' do + expect(user).to receive(:validate_and_consume_otp!).with(otp_code) + + validate + end +end diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb new file mode 100644 index 00000000000..18fd6d08057 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + let(:host) { 'forti_authenticator.example.com' } + let(:port) { '444' } + let(:api_username) { 'janedoe' } + let(:api_token) { 's3cr3t' } + + let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" } + + subject(:validate) { described_class.new(user).validate(otp_code) } + + before do + stub_feature_flags(forti_authenticator: true) + + stub_forti_authenticator_config( + host: host, + port: port, + username: api_username, + token: api_token + ) + + request_body = { username: user.username, + token_code: otp_code } + + stub_request(:post, forti_authenticator_auth_url) + .with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' }) + .to_return(status: response_status, body: '', headers: {}) + end + + context 'successful validation' do + let(:response_status) { 200 } + + it 'returns success' do + expect(validate[:status]).to eq(:success) + end + end + + context 'unsuccessful validation' do + let(:response_status) { 401 } + + it 'returns error' do + expect(validate[:status]).to eq(:error) + end + end + + def stub_forti_authenticator_config(forti_authenticator_settings) + allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings)) + end +end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index a08055ab852..b239de841b6 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::Auth::UniqueIpsLimiter, :clean_gitlab_redis_shared_state expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1) expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2) - Timecop.travel(Time.now.utc + described_class.config.unique_ips_limit_time_window) do + travel_to(Time.now.utc + described_class.config.unique_ips_limit_time_window) do expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1) expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2) end diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb index 5cbd22827c9..d3c6cde5590 100644 --- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb +++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb @@ -49,5 +49,13 @@ RSpec.describe Gitlab::Auth::UserAccessDeniedReason do it { is_expected.to match /Your primary email address is not confirmed/ } end + + context 'when the user is blocked pending approval' do + before do + user.block_pending_approval! + end + + it { is_expected.to eq('Your account is pending approval from your administrator and hence blocked.') } + end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 74360637897..1768ab41a71 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -726,6 +726,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do expect( gl_auth.find_with_user_password(username, password) ).not_to eql user end + it 'does not find user in blocked_pending_approval state' do + user.block_pending_approval + + expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + end + context 'with increment_failed_attempts' do wrong_password = 'incorrect_password' diff --git a/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb new file mode 100644 index 00000000000..81b8b5dde08 --- /dev/null +++ b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::AddModifiedToApprovalMergeRequestRule, schema: 20200817195628 do + let(:determine_if_rules_are_modified) { described_class.new } + + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') } + let(:projects) { table(:projects) } + let(:normal_project) { projects.create!(namespace_id: namespace.id) } + let(:overridden_project) { projects.create!(namespace_id: namespace.id) } + let(:rules) { table(:approval_merge_request_rules) } + let(:project_rules) { table(:approval_project_rules) } + let(:sources) { table(:approval_merge_request_rule_sources) } + let(:merge_requests) { table(:merge_requests) } + let(:groups) { table(:namespaces) } + let(:mr_groups) { table(:approval_merge_request_rules_groups) } + let(:project_groups) { table(:approval_project_rules_groups) } + + before do + project_rule = project_rules.create!(project_id: normal_project.id, approvals_required: 3, name: 'test rule') + overridden_project_rule = project_rules.create!(project_id: overridden_project.id, approvals_required: 5, name: 'other test rule') + overridden_project_rule_two = project_rules.create!(project_id: overridden_project.id, approvals_required: 7, name: 'super cool rule') + + merge_request = merge_requests.create!(target_branch: 'feature', source_branch: 'default', source_project_id: normal_project.id, target_project_id: normal_project.id) + overridden_merge_request = merge_requests.create!(target_branch: 'feature-2', source_branch: 'default', source_project_id: overridden_project.id, target_project_id: overridden_project.id) + + merge_rule = rules.create!(merge_request_id: merge_request.id, approvals_required: 3, name: 'test rule') + overridden_merge_rule = rules.create!(merge_request_id: overridden_merge_request.id, approvals_required: 6, name: 'other test rule') + overridden_merge_rule_two = rules.create!(merge_request_id: overridden_merge_request.id, approvals_required: 7, name: 'super cool rule') + + sources.create!(approval_project_rule_id: project_rule.id, approval_merge_request_rule_id: merge_rule.id) + sources.create!(approval_project_rule_id: overridden_project_rule.id, approval_merge_request_rule_id: overridden_merge_rule.id) + sources.create!(approval_project_rule_id: overridden_project_rule_two.id, approval_merge_request_rule_id: overridden_merge_rule_two.id) + + group1 = groups.create!(name: "group1", path: "test_group1", type: 'Group') + group2 = groups.create!(name: "group2", path: "test_group2", type: 'Group') + group3 = groups.create!(name: "group3", path: "test_group3", type: 'Group') + + project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group1.id) + project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group2.id) + project_groups.create!(approval_project_rule_id: overridden_project_rule_two.id, group_id: group3.id) + + mr_groups.create!(approval_merge_request_rule_id: overridden_merge_rule.id, group_id: group1.id) + mr_groups.create!(approval_merge_request_rule_id: overridden_merge_rule_two.id, group_id: group2.id) + end + + describe '#perform' do + it 'changes the correct rules' do + original_count = rules.all.count + + determine_if_rules_are_modified.perform(rules.minimum(:id), rules.maximum(:id)) + + results = rules.where(modified_from_project_rule: true) + + expect(results.count).to eq 2 + expect(results.collect(&:name)).to eq(['other test rule', 'super cool rule']) + expect(rules.count).to eq original_count + end + end +end diff --git a/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb b/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb index a3840e3a22e..85a9c88ebff 100644 --- a/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb +++ b/spec/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Gitlab::BackgroundMigration::MergeRequestAssigneesMigrationProgre described_class.new.perform - expect(Feature.enabled?(:multiple_merge_request_assignees)).to eq(true) + expect(Feature.enabled?(:multiple_merge_request_assignees, type: :licensed)).to eq(true) end end diff --git a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb new file mode 100644 index 00000000000..33498ffa748 --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20200925125321 do + let(:users) { table(:users) } + + let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) } + + let(:u2f_registrations) { table(:u2f_registrations) } + let(:webauthn_registrations) { table(:webauthn_registrations) } + + let!(:u2f_registration_not_migrated) { create_u2f_registration(1, 'reg1') } + let!(:u2f_registration_not_migrated_no_name) { create_u2f_registration(2, nil, 2) } + let!(:u2f_registration_migrated) { create_u2f_registration(3, 'reg3') } + + subject { described_class.new.perform(1, 3) } + + before do + converted_credential = convert_credential_for(u2f_registration_migrated) + webauthn_registrations.create!(converted_credential) + end + + it 'migrates all records' do + expect { subject }.to change { webauthn_registrations.count }.from(1).to(3) + + all_webauthn_registrations = webauthn_registrations.all.map(&:attributes) + + [u2f_registration_not_migrated, u2f_registration_not_migrated_no_name].each do |u2f_registration| + expected_credential = convert_credential_for(u2f_registration).except(:created_at).stringify_keys + expect(all_webauthn_registrations).to include(a_hash_including(expected_credential)) + end + end + + def create_u2f_registration(id, name, counter = 5) + device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) + u2f_registrations.create!({ id: id, + certificate: Base64.strict_encode64(device.cert_raw), + key_handle: U2F.urlsafe_encode64(device.key_handle_raw), + public_key: Base64.strict_encode64(device.origin_public_key_raw), + counter: counter, + name: name, + user_id: user.id }) + end + + def convert_credential_for(u2f_registration) + converted_credential = WebAuthn::U2fMigrator.new( + app_id: Gitlab.config.gitlab.url, + certificate: u2f_registration.certificate, + key_handle: u2f_registration.key_handle, + public_key: u2f_registration.public_key, + counter: u2f_registration.counter + ).credential + + { + credential_xid: Base64.strict_encode64(converted_credential.id), + public_key: Base64.strict_encode64(converted_credential.public_key), + counter: u2f_registration.counter, + name: u2f_registration.name || '', + user_id: u2f_registration.user_id, + u2f_registration_id: u2f_registration.id, + created_at: u2f_registration.created_at + } + end +end diff --git a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb index db3cbe7ccdc..3cec5cb4c35 100644 --- a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb @@ -82,21 +82,4 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateUsersBioToUserDetails, :migra expect(user_detail).to be_nil end - - context 'when `migrate_bio_to_user_details` feature flag is off' do - before do - stub_feature_flags(migrate_bio_to_user_details: false) - end - - it 'does nothing' do - already_existing_user_details = user_details.where(user_id: [ - user_has_different_details.id, - user_already_has_details.id - ]) - - subject - - expect(user_details.all).to match_array(already_existing_user_details) - end - end end diff --git a/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb new file mode 100644 index 00000000000..fa4f2d1fd88 --- /dev/null +++ b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20201015073808 do + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') } + let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') } + let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') } + let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') } + let(:issue_links) { table(:issue_links) } + let!(:blocks_link) { issue_links.create!(source_id: issue1.id, target_id: issue2.id, link_type: 1) } + let!(:bidirectional_link) { issue_links.create!(source_id: issue2.id, target_id: issue1.id, link_type: 2) } + let!(:blocked_link) { issue_links.create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) } + + subject { described_class.new.perform(issue_links.minimum(:id), issue_links.maximum(:id)) } + + it 'deletes issue links where opposite relation already exists' do + expect { subject }.to change { issue_links.count }.by(-1) + end + + it 'ignores issue links other than blocked_by' do + subject + + expect(blocks_link.reload.link_type).to eq(1) + end + + it 'updates blocked_by issue links' do + subject + + link = blocked_link.reload + expect(link.link_type).to eq(1) + expect(link.source_id).to eq(issue3.id) + expect(link.target_id).to eq(issue1.id) + end +end diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb index 392b44d1a1f..2dae4a65eeb 100644 --- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb +++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb @@ -74,14 +74,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent let(:user_mentions) { merge_request_user_mentions } let(:resource) { merge_request } - it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, MergeRequest + it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, 'MergeRequest' context 'when FF disabled' do before do stub_feature_flags(migrate_user_mentions: false) end - it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, MergeRequest + it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, 'MergeRequest' end end @@ -103,14 +103,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent let(:user_mentions) { commit_user_mentions } let(:resource) { commit } - it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, Commit + it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, 'Commit' context 'when FF disabled' do before do stub_feature_flags(migrate_user_mentions: false) end - it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, Commit + it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, 'Commit' end end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index d4483bf1754..b723c31c4aa 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -312,7 +312,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer do # attributes later. existing_label.reload - Timecop.freeze(Time.now + 1.minute) do + travel_to(Time.now + 1.minute) do importer.execute label_after_import = project.labels.find(existing_label.id) diff --git a/spec/lib/gitlab/bulk_import/client_spec.rb b/spec/lib/gitlab/bulk_import/client_spec.rb new file mode 100644 index 00000000000..a6f8dd6d194 --- /dev/null +++ b/spec/lib/gitlab/bulk_import/client_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BulkImport::Client do + include ImportSpecHelper + + let(:uri) { 'http://gitlab.example' } + let(:token) { 'token' } + let(:resource) { 'resource' } + + subject { described_class.new(uri: uri, token: token) } + + describe '#get' do + let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } + + shared_examples 'performs network request' do + it 'performs network request' do + expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double) + + subject.get(resource) + end + end + + describe 'parsed response' do + it 'returns parsed response' do + response_double = double(code: 200, success?: true, parsed_response: [{ id: 1 }, { id: 2 }]) + + allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + + expect(subject.get(resource)).to eq(response_double.parsed_response) + end + end + + describe 'request query' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + query: { + page: described_class::DEFAULT_PAGE, + per_page: described_class::DEFAULT_PER_PAGE + } + ) + ] + end + end + end + + describe 'request headers' do + include_examples 'performs network request' do + let(:expected_args) do + [ + anything, + hash_including( + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{token}" + } + ) + ] + end + end + end + + describe 'request uri' do + include_examples 'performs network request' do + let(:expected_args) do + ['http://gitlab.example:80/api/v4/resource', anything] + end + end + end + + context 'error handling' do + context 'when error occurred' do + it 'raises ConnectionError' do + allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + + context 'when response is not success' do + it 'raises ConnectionError' do + response_double = double(code: 503, success?: false) + + allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + + expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + end + end + end + end +end diff --git a/spec/lib/gitlab/checks/matching_merge_request_spec.rb b/spec/lib/gitlab/checks/matching_merge_request_spec.rb new file mode 100644 index 00000000000..ca7ee784ee3 --- /dev/null +++ b/spec/lib/gitlab/checks/matching_merge_request_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Checks::MatchingMergeRequest do + describe '#match?' do + let_it_be(:newrev) { '012345678' } + let_it_be(:target_branch) { 'feature' } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:locked_merge_request) do + create(:merge_request, + :locked, + source_project: project, + target_project: project, + target_branch: target_branch, + in_progress_merge_commit_sha: newrev) + end + + subject { described_class.new(newrev, target_branch, project) } + + it 'matches a merge request' do + expect(subject.match?).to be true + end + + it 'does not match any merge request' do + matcher = described_class.new(newrev, 'test', project) + + expect(matcher.match?).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb index 8b1cd812a70..d681447a0e8 100644 --- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb @@ -58,6 +58,15 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do end end + describe '#set_section_options' do + it 'sets the current section\'s options' do + options = { collapsed: true } + subject.set_section_options(options) + + expect(subject.to_h[:section_options]).to eq(options) + end + end + describe '#set_as_section_header' do it 'change the section_header to true' do expect { subject.set_as_section_header } diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb index cb6949fddc2..c9c0d1a744e 100644 --- a/spec/lib/gitlab/ci/ansi2json_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json_spec.rb @@ -229,7 +229,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do expect(convert_json(trace)).to eq([ { offset: 0, - content: [{ text: "section_end:1:2<div>hello</div>" }], + content: [{ text: 'section_end:1:2<div>hello</div>' }], section: 'prepare-script', section_header: true }, @@ -329,6 +329,32 @@ RSpec.describe Gitlab::Ci::Ansi2json do ]) end end + + context 'with section options' do + let(:option_section_start) { "section_start:#{section_start_time.to_i}:#{section_name}[collapsed=true,unused_option=123]\r\033[0K"} + + it 'provides section options when set' do + trace = "#{option_section_start}hello#{section_end}" + expect(convert_json(trace)).to eq([ + { + offset: 0, + content: [{ text: 'hello' }], + section: 'prepare-script', + section_header: true, + section_options: { + 'collapsed' => 'true', + 'unused_option' => '123' + } + }, + { + offset: 83, + content: [], + section: 'prepare-script', + section_duration: '01:03' + } + ]) + end + end end describe 'incremental updates' do @@ -339,7 +365,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do context 'with split word' do let(:pre_text) { "\e[1mHello " } - let(:text) { "World" } + let(:text) { 'World' } let(:lines) do [ @@ -355,7 +381,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do context 'with split word on second line' do let(:pre_text) { "Good\nmorning " } - let(:text) { "World" } + let(:text) { 'World' } let(:lines) do [ @@ -514,7 +540,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do end describe 'trucates' do - let(:text) { "Hello World" } + let(:text) { 'Hello World' } let(:stream) { StringIO.new(text) } let(:subject) { described_class.convert(stream) } @@ -522,11 +548,11 @@ RSpec.describe Gitlab::Ci::Ansi2json do stream.seek(3, IO::SEEK_SET) end - it "returns truncated output" do + it 'returns truncated output' do expect(subject.truncated).to be_truthy end - it "does not append output" do + it 'does not append output' do expect(subject.append).to be_falsey end end diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb index 83a37655ea9..e982f0eb015 100644 --- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb +++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb @@ -18,17 +18,6 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') end - context 'when FF ci_new_artifact_file_reader is disabled' do - before do - stub_feature_flags(ci_new_artifact_file_reader: false) - end - - it 'returns the content at the path' do - is_expected.to be_present - expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') - end - end - context 'when path does not exist' do let(:path) { 'file/does/not/exist.txt' } let(:expected_error) do diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index f33176c3da3..8b2e0410474 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do end end end + + describe '#manual_action?' do + context 'when job is a manual action' do + let(:config) { { script: 'deploy', when: 'manual' } } + + it { is_expected.to be_manual_action } + end + + context 'when job is not a manual action' do + let(:config) { { script: 'deploy' } } + + it { is_expected.not_to be_manual_action } + end + end + + describe '#ignored?' do + context 'when job is a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual' } + end + + it { is_expected.to be_ignored } + end + + context 'when job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: true } + end + + it { is_expected.to be_ignored } + end + + context 'when job is not allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: false } + end + + it { is_expected.not_to be_ignored } + end + end + + context 'when job is not a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) { { script: 'deploy' } } + + it { is_expected.not_to be_ignored } + end + + context 'when job is allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: true } } + + it { is_expected.to be_ignored } + end + + context 'when job is not allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: false } } + + it { is_expected.not_to be_ignored } + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 3501812b76e..80427eaa6ee 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -13,18 +13,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do context 'when entry config value is correct' do let(:policy) { nil } let(:key) { 'some key' } + let(:when_config) { nil } let(:config) do - { key: key, + { + key: key, untracked: true, - paths: ['some/path/'], - policy: policy } + paths: ['some/path/'] + }.tap do |config| + config[:policy] = policy if policy + config[:when] = when_config if when_config + end end describe '#value' do shared_examples 'hash key value' do it 'returns hash value' do - expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push') + expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success') end end @@ -49,6 +54,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do expect(entry.value).to match(a_hash_including(key: nil)) end end + + context 'with `policy`' do + using RSpec::Parameterized::TableSyntax + + where(:policy, :result) do + 'pull-push' | 'pull-push' + 'push' | 'push' + 'pull' | 'pull' + 'unknown' | 'unknown' # invalid + end + + with_them do + it { expect(entry.value).to include(policy: result) } + end + end + + context 'without `policy`' do + it 'assigns policy to default' do + expect(entry.value).to include(policy: 'pull-push') + end + end + + context 'with `when`' do + using RSpec::Parameterized::TableSyntax + + where(:when_config, :result) do + 'on_success' | 'on_success' + 'on_failure' | 'on_failure' + 'always' | 'always' + 'unknown' | 'unknown' # invalid + end + + with_them do + it { expect(entry.value).to include(when: result) } + end + end + + context 'without `when`' do + it 'assigns when to default' do + expect(entry.value).to include(when: 'on_success') + end + end end describe '#valid?' do @@ -61,28 +108,41 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end end - context 'policy is pull-push' do - let(:policy) { 'pull-push' } + context 'with `policy`' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'pull-push') } - end - - context 'policy is push' do - let(:policy) { 'push' } + where(:policy, :valid) do + 'pull-push' | true + 'push' | true + 'pull' | true + 'unknown' | false + end - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'push') } + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end + end end - context 'policy is pull' do - let(:policy) { 'pull' } + context 'with `when`' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'pull') } + where(:when_config, :valid) do + 'on_success' | true + 'on_failure' | true + 'always' | true + 'unknown' | false + end + + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end + end end - context 'when key is missing' do + context 'with key missing' do let(:config) do { untracked: true, paths: ['some/path/'] } @@ -110,13 +170,21 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'when policy is unknown' do - let(:config) { { policy: "unknown" } } + let(:config) { { policy: 'unknown' } } it 'reports error' do is_expected.to include('cache policy should be pull-push, push, or pull') end end + context 'when `when` is unknown' do + let(:config) { { when: 'unknown' } } + + it 'reports error' do + is_expected.to include('cache when should be on_success, on_failure or always') + end + end + context 'when descendants are invalid' do context 'with invalid keys' do let(:config) { { key: 1 } } diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb index 3e816f70c03..59f0b0e7a48 100644 --- a/spec/lib/gitlab/ci/config/entry/include_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb @@ -61,6 +61,31 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do end end end + + context 'when using "project"' do + context 'and specifying "ref" and "file"' do + let(:config) { { project: 'my-group/my-pipeline-library', ref: 'master', file: 'test.yml' } } + + it { is_expected.to be_valid } + end + + context 'without "ref"' do + let(:config) { { project: 'my-group/my-pipeline-library', file: 'test.yml' } } + + it { is_expected.to be_valid } + end + + context 'without "file"' do + let(:config) { { project: 'my-group/my-pipeline-library' } } + + it { is_expected.not_to be_valid } + + it 'has specific error' do + expect(include_entry.errors) + .to include('include config must specify the file where to fetch the config from') + end + end + end end context 'when value is something else' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index ab760b107f8..e0e8bc93770 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -537,7 +537,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'overrides default config' do expect(entry[:image].value).to eq(name: 'some_image') - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') end end @@ -552,7 +552,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'uses config from default entry' do expect(entry[:image].value).to eq 'specified' - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') end end diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb index 39697884e3b..3388ae0af2f 100644 --- a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' require_dependency 'active_model' RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do @@ -46,33 +46,140 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do end end - context 'when entry config has only one variable' do - let(:config) do - [ - { - 'VAR_1' => %w[test] - } - ] + context 'with one_dimensional_matrix feature flag enabled' do + before do + stub_feature_flags(one_dimensional_matrix: true) + matrix.compose! end - describe '#valid?' do - it { is_expected.not_to be_valid } - end + context 'when entry config has only one variable with multiple values' do + let(:config) do + [ + { + 'VAR_1' => %w[build test] + } + ] + end - describe '#errors' do - it 'returns error about too many jobs' do - expect(matrix.errors) - .to include('variables config requires at least 2 items') + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#errors' do + it 'returns no errors' do + expect(matrix.errors) + .to be_empty + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }]) + end end + + context 'when entry config has only one variable with one value' do + let(:config) do + [ + { + 'VAR_1' => %w[test] + } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#errors' do + it 'returns no errors' do + expect(matrix.errors) + .to be_empty + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }]) + end + end + end + end + end + + context 'with one_dimensional_matrix feature flag disabled' do + before do + stub_feature_flags(one_dimensional_matrix: false) + matrix.compose! end - describe '#value' do - before do - matrix.compose! + context 'when entry config has only one variable with multiple values' do + let(:config) do + [ + { + 'VAR_1' => %w[build test] + } + ] end - it 'returns the value without raising an error' do - expect(matrix.value).to eq([{ 'VAR_1' => ['test'] }]) + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about too many jobs' do + expect(matrix.errors) + .to include('variables config requires at least 2 items') + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }]) + end + end + + context 'when entry config has only one variable with one value' do + let(:config) do + [ + { + 'VAR_1' => %w[test] + } + ] + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns no errors' do + expect(matrix.errors) + .to include('variables config requires at least 2 items') + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }]) + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb index 230b001d620..407efb438b5 100644 --- a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'fast_spec_helper' +# After Feature one_dimensional_matrix is removed, this can be changed back to fast_spec_helper +require 'spec_helper' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do @@ -45,43 +46,71 @@ RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do end end - context 'when entry value is not correct' do - shared_examples 'invalid variables' do |message| - describe '#errors' do - it 'saves errors' do - expect(entry.errors).to include(message) - end + context 'with one_dimensional_matrix feature flag enabled' do + context 'with only one variable' do + before do + stub_feature_flags(one_dimensional_matrix: true) end + let(:config) { { VAR: 'test' } } describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty end end end + end - context 'with array' do - let(:config) { [:VAR, 'test'] } + context 'with one_dimensional_matrix feature flag disabled' do + context 'when entry value is not correct' do + before do + stub_feature_flags(one_dimensional_matrix: false) + end + shared_examples 'invalid variables' do |message| + describe '#errors' do + it 'saves errors' do + expect(entry.errors).to include(message) + end + end - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end - context 'with empty array' do - let(:config) { { VAR: 'test', VAR2: [] } } + context 'with array' do + let(:config) { [:VAR, 'test'] } - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end - context 'with nested array' do - let(:config) { { VAR: 'test', VAR2: [1, [2]] } } + context 'with empty array' do + let(:config) { { VAR: 'test', VAR2: [] } } - it_behaves_like 'invalid variables', /should be a hash of key value pairs/ - end + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end - context 'with only one variable' do - let(:config) { { VAR: 'test' } } + context 'with nested array' do + let(:config) { { VAR: 'test', VAR2: [1, [2]] } } + + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end - it_behaves_like 'invalid variables', /variables config requires at least 2 items/ + context 'with one_dimensional_matrix feature flag disabled' do + context 'with only one variable' do + let(:config) { { VAR: 'test' } } + + it_behaves_like 'invalid variables', /variables config requires at least 2 items/ + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 252bda6461d..79716df6b60 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -127,7 +127,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -141,7 +141,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -156,7 +156,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, image: { name: "ruby:2.7" }, services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], - cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" }, + cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, only: { refs: %w(branches tags) }, variables: { 'VAR' => 'job' }, after_script: [], @@ -203,7 +203,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'job' }, ignore: false, after_script: ['make clean'], @@ -261,7 +261,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do describe '#cache_value' do it 'returns correct cache definition' do - expect(root.cache_value).to eq(key: 'a', policy: 'pull-push') + expect(root.cache_value).to eq(key: 'a', policy: 'pull-push', when: 'on_success') end end end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index d6391092f63..ac33f858f43 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -3,56 +3,109 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Variables do - let(:entry) { described_class.new(config) } + subject { described_class.new(config) } - describe 'validations' do - context 'when entry config value is correct' do - let(:config) do - { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + shared_examples 'valid config' do + describe '#value' do + it 'returns hash with key value strings' do + expect(subject.value).to eq result end + end - describe '#value' do - it 'returns hash with key value strings' do - expect(entry.value).to eq config - end - - context 'with numeric keys and values in the config' do - let(:config) { { 10 => 20 } } + describe '#errors' do + it 'does not append errors' do + expect(subject.errors).to be_empty + end + end - it 'converts numeric key and numeric value into strings' do - expect(entry.value).to eq('10' => '20') - end - end + describe '#valid?' do + it 'is valid' do + expect(subject).to be_valid end + end + end - describe '#errors' do - it 'does not append errors' do - expect(entry.errors).to be_empty - end + shared_examples 'invalid config' do + describe '#valid?' do + it 'is not valid' do + expect(subject).not_to be_valid end + end - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end + describe '#errors' do + it 'saves errors' do + expect(subject.errors) + .to include /should be a hash of key value pairs/ end end + end - context 'when entry value is not correct' do - let(:config) { [:VAR, 'test'] } + context 'when entry config value has key-value pairs' do + let(:config) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include /should be a hash of key value pairs/ - end - end + let(:result) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end + it_behaves_like 'valid config' + end + + context 'with numeric keys and values in the config' do + let(:config) { { 10 => 20 } } + let(:result) do + { '10' => '20' } + end + + it_behaves_like 'valid config' + end + + context 'when entry config value has key-value pair and hash' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, + 'VARIABLE_2' => 'value 2' } + end + + let(:result) do + { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } + end + + it_behaves_like 'valid config' + end + + context 'when entry value is an array' do + let(:config) { [:VAR, 'test'] } + + it_behaves_like 'invalid config' + end + + context 'when entry value has hash with other key-pairs' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' }, + 'VARIABLE_2' => 'value 2' } end + + it_behaves_like 'invalid config' + end + + context 'when entry config value has hash with nil description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', description: nil } } + end + + it_behaves_like 'invalid config' + end + + context 'when entry config value has hash without description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1' } } + end + + let(:result) do + { 'VARIABLE_1' => 'value 1' } + end + + it_behaves_like 'valid config' end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index f724825a9cc..dd27b4045c9 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -82,7 +82,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when PST (Pacific Standard Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when PDT (Pacific Daylight Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -117,7 +117,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when CET (Central European Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -125,7 +125,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when CEST (Central European Summer Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -152,7 +152,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when EST (Eastern Standard Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -160,7 +160,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when EDT (Eastern Daylight Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -174,7 +174,7 @@ RSpec.describe Gitlab::Ci::CronParser do # (e.g. America/Chicago) at the start of the test. Stubbing # TZ doesn't appear to be enough. it 'generates day without TZInfo::AmbiguousTime error' do - Timecop.freeze(Time.utc(2020, 1, 1)) do + travel_to(Time.utc(2020, 1, 1)) do expect(subject.year).to eq(year) expect(subject.month).to eq(12) expect(subject.day).to eq(1) diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 077c0fd3162..c67f8464123 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Lint do - let_it_be(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:lint) { described_class.new(project: project, current_user: user) } @@ -61,6 +61,43 @@ RSpec.describe Gitlab::Ci::Lint do end end + shared_examples 'sets merged yaml' do + let(:content) do + <<~YAML + :include: + :local: another-gitlab-ci.yml + :test_job: + :stage: test + :script: echo + YAML + end + + let(:included_content) do + <<~YAML + :another_job: + :script: echo + YAML + end + + before do + project.repository.create_file( + project.creator, + 'another-gitlab-ci.yml', + included_content, + message: 'Automatically created another-gitlab-ci.yml', + branch_name: 'master' + ) + end + + it 'sets merged_config' do + root_config = YAML.safe_load(content, [Symbol]) + included_config = YAML.safe_load(included_content, [Symbol]) + expected_config = included_config.merge(root_config).except(:include) + + expect(subject.merged_yaml).to eq(expected_config.to_yaml) + end + end + shared_examples 'content with errors and warnings' do context 'when content has errors' do let(:content) do @@ -173,6 +210,8 @@ RSpec.describe Gitlab::Ci::Lint do end end + it_behaves_like 'sets merged yaml' + include_context 'advanced validations' do it 'does not catch advanced logical errors' do expect(subject).to be_valid @@ -203,6 +242,8 @@ RSpec.describe Gitlab::Ci::Lint do end end + it_behaves_like 'sets merged yaml' + include_context 'advanced validations' do it 'runs advanced logical validations' do expect(subject).not_to be_valid diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb index 1f497dea2bf..7da602251a5 100644 --- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb +++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb @@ -4,11 +4,12 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Test::Junit do describe '#parse!' do - subject { described_class.new.parse!(junit, test_suite, args) } + subject { described_class.new.parse!(junit, test_suite, job: job) } let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') } let(:test_cases) { flattened_test_cases(test_suite) } - let(:args) { { job: { id: 1, project: "project" } } } + let(:job) { double(max_test_cases_per_report: max_test_cases) } + let(:max_test_cases) { 0 } context 'when data is JUnit style XML' do context 'when there are no <testcases> in <testsuite>' do @@ -43,7 +44,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do let(:junit) do <<-EOF.strip_heredoc <testsuites> - <testsuite> + <testsuite name='Math'> <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> </testsuite> </testsuites> @@ -53,6 +54,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do it 'parses XML and adds a test case to a suite' do expect { subject }.not_to raise_error + expect(test_cases[0].suite_name).to eq('Math') expect(test_cases[0].classname).to eq('Calculator') expect(test_cases[0].name).to eq('sumTest1') expect(test_cases[0].execution_time).to eq(0.01) @@ -62,7 +64,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do context 'when there is <testcase>' do let(:junit) do <<-EOF.strip_heredoc - <testsuite> + <testsuite name='Math'> <testcase classname='Calculator' name='sumTest1' time='0.01'> #{testcase_content} </testcase> @@ -79,6 +81,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do shared_examples_for '<testcase> XML parser' do |status, output| it 'parses XML and adds a test case to the suite' do aggregate_failures do + expect(test_case.suite_name).to eq('Math') expect(test_case.classname).to eq('Calculator') expect(test_case.name).to eq('sumTest1') expect(test_case.execution_time).to eq(0.01) @@ -152,13 +155,15 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do expect { subject }.not_to raise_error expect(test_cases.count).to eq(1) + expect(test_cases.first.suite_name).to eq("XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest") + expect(test_cases.first.name).to eq("testIndexAction") end end context 'when there are two test cases' do let(:junit) do <<-EOF.strip_heredoc - <testsuite> + <testsuite name='Math'> <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> </testsuite> @@ -168,9 +173,11 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do it 'parses XML and adds test cases to a suite' do expect { subject }.not_to raise_error + expect(test_cases[0].suite_name).to eq('Math') expect(test_cases[0].classname).to eq('Calculator') expect(test_cases[0].name).to eq('sumTest1') expect(test_cases[0].execution_time).to eq(0.01) + expect(test_cases[1].suite_name).to eq('Math') expect(test_cases[1].classname).to eq('Calculator') expect(test_cases[1].name).to eq('sumTest2') expect(test_cases[1].execution_time).to eq(0.02) @@ -181,7 +188,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do let(:junit) do <<-EOF.strip_heredoc <testsuites> - <testsuite> + <testsuite name='Math'> <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> </testsuite> @@ -196,18 +203,81 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do it 'parses XML and adds test cases to a suite' do expect { subject }.not_to raise_error - expect(test_cases[0].classname).to eq('Calculator') - expect(test_cases[0].name).to eq('sumTest1') - expect(test_cases[0].execution_time).to eq(0.01) - expect(test_cases[1].classname).to eq('Calculator') - expect(test_cases[1].name).to eq('sumTest2') - expect(test_cases[1].execution_time).to eq(0.02) - expect(test_cases[2].classname).to eq('Statemachine') - expect(test_cases[2].name).to eq('happy path') - expect(test_cases[2].execution_time).to eq(100) - expect(test_cases[3].classname).to eq('Statemachine') - expect(test_cases[3].name).to eq('unhappy path') - expect(test_cases[3].execution_time).to eq(200) + expect(test_cases).to contain_exactly( + have_attributes( + suite_name: 'Math', + classname: 'Calculator', + name: 'sumTest1', + execution_time: 0.01 + ), + have_attributes( + suite_name: 'Math', + classname: 'Calculator', + name: 'sumTest2', + execution_time: 0.02 + ), + have_attributes( + suite_name: test_suite.name, # Defaults to test suite instance's name + classname: 'Statemachine', + name: 'happy path', + execution_time: 100 + ), + have_attributes( + suite_name: test_suite.name, # Defaults to test suite instance's name + classname: 'Statemachine', + name: 'unhappy path', + execution_time: 200 + ) + ) + end + end + + context 'when number of test cases exceeds the max_test_cases limit' do + let(:max_test_cases) { 1 } + + shared_examples_for 'rejecting too many test cases' do + it 'attaches an error to the TestSuite object' do + expect { subject }.not_to raise_error + expect(test_suite.suite_error).to eq("JUnit data parsing failed: number of test cases exceeded the limit of #{max_test_cases}") + end + end + + context 'and test cases are unique' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuites> + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> + </testsuite> + <testsuite> + <testcase classname='Statemachine' name='happy path' time='100'></testcase> + <testcase classname='Statemachine' name='unhappy path' time='200'></testcase> + </testsuite> + </testsuites> + EOF + end + + it_behaves_like 'rejecting too many test cases' + end + + context 'and test cases are duplicates' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuites> + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> + </testsuite> + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase> + </testsuite> + </testsuites> + EOF + end + + it_behaves_like 'rejecting too many test cases' end end end @@ -296,9 +366,7 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do expect(test_cases[0].has_attachment?).to be_truthy expect(test_cases[0].attachment).to eq("some/path.png") - expect(test_cases[0].job).to be_present - expect(test_cases[0].job[:id]).to eq(1) - expect(test_cases[0].job[:project]).to eq("project") + expect(test_cases[0].job).to eq(job) end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index 74c014b6408..570706bfaac 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -224,7 +224,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do key: 'a-key', paths: ['vendor/ruby'], untracked: true, - policy: 'push' + policy: 'push', + when: 'on_success' } end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 34df0e86a18..0b961336f3f 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do - let(:project) { create(:project, :repository) } - let(:head_sha) { project.repository.head_commit.id } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: head_sha) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:head_sha) { project.repository.head_commit.id } + let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) } let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } } let(:previous_stages) { [] } @@ -503,7 +503,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do using RSpec::Parameterized let(:pipeline) do - build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source) + build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project) end context 'matches' do @@ -766,7 +766,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do context 'with a matching changes: rule' do let(:pipeline) do - create(:ci_pipeline, project: project).tap do |pipeline| + build(:ci_pipeline, project: project).tap do |pipeline| stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml]) end end diff --git a/spec/lib/gitlab/ci/reports/test_case_spec.rb b/spec/lib/gitlab/ci/reports/test_case_spec.rb index 7fb208213c1..a142846fc18 100644 --- a/spec/lib/gitlab/ci/reports/test_case_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_case_spec.rb @@ -6,39 +6,26 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do describe '#initialize' do let(:test_case) { described_class.new(params) } - context 'when both classname and name are given' do - context 'when test case is passed' do - let(:job) { build(:ci_build) } - let(:params) { attributes_for(:test_case).merge!(job: job) } - - it 'initializes an instance' do - expect { test_case }.not_to raise_error - - expect(test_case.name).to eq('test-1') - expect(test_case.classname).to eq('trace') - expect(test_case.file).to eq('spec/trace_spec.rb') - expect(test_case.execution_time).to eq(1.23) - expect(test_case.status).to eq(described_class::STATUS_SUCCESS) - expect(test_case.system_output).to be_nil - expect(test_case.job).to be_present - end - end + context 'when required params are given' do + let(:job) { build(:ci_build) } + let(:params) { attributes_for(:test_case).merge!(job: job) } - context 'when test case is failed' do - let(:job) { build(:ci_build) } - let(:params) { attributes_for(:test_case, :failed).merge!(job: job) } - - it 'initializes an instance' do - expect { test_case }.not_to raise_error - - expect(test_case.name).to eq('test-1') - expect(test_case.classname).to eq('trace') - expect(test_case.file).to eq('spec/trace_spec.rb') - expect(test_case.execution_time).to eq(1.23) - expect(test_case.status).to eq(described_class::STATUS_FAILED) - expect(test_case.system_output) - .to eq('Failure/Error: is_expected.to eq(300) expected: 300 got: -100') - end + it 'initializes an instance', :aggregate_failures do + expect { test_case }.not_to raise_error + + expect(test_case).to have_attributes( + suite_name: params[:suite_name], + name: params[:name], + classname: params[:classname], + file: params[:file], + execution_time: params[:execution_time], + status: params[:status], + system_output: params[:system_output], + job: params[:job] + ) + + key = "#{test_case.suite_name}_#{test_case.classname}_#{test_case.name}" + expect(test_case.key).to eq(Digest::SHA256.hexdigest(key)) end end @@ -53,6 +40,10 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do end end + context 'when suite_name is missing' do + it_behaves_like 'param is missing', :suite_name + end + context 'when classname is missing' do it_behaves_like 'param is missing', :classname end diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 15fa78444e5..50d1595da73 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -229,6 +229,20 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do end end + describe '#each_test_case' do + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + test_suite.add_test_case(test_case_skipped) + test_suite.add_test_case(test_case_error) + end + + it 'yields each test case to given block' do + expect { |b| test_suite.each_test_case(&b) } + .to yield_successive_args(test_case_success, test_case_failed, test_case_skipped, test_case_error) + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}_count" do subject { test_suite.public_send("#{status_type}_count") } diff --git a/spec/lib/gitlab/ci/runner/backoff_spec.rb b/spec/lib/gitlab/ci/runner/backoff_spec.rb new file mode 100644 index 00000000000..f147d69f7cd --- /dev/null +++ b/spec/lib/gitlab/ci/runner/backoff_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' +require 'active_support/testing/time_helpers' + +RSpec.describe Gitlab::Ci::Runner::Backoff do + include ActiveSupport::Testing::TimeHelpers + + describe '#duration' do + it 'returns backoff duration from start' do + freeze_time do + described_class.new(5.minutes.ago).then do |backoff| + expect(backoff.duration).to eq 5.minutes + end + end + end + + it 'returns an integer value' do + freeze_time do + described_class.new(5.seconds.ago).then do |backoff| + expect(backoff.duration).to be 5 + end + end + end + + it 'returns the smallest number greater than or equal to duration' do + freeze_time do + described_class.new(0.5.seconds.ago).then do |backoff| + expect(backoff.duration).to be 1 + end + end + end + end + + describe '#slot' do + using RSpec::Parameterized::TableSyntax + + where(:started, :slot) do + 0 | 0 + 0.1 | 0 + 0.9 | 0 + 1 | 0 + 1.1 | 0 + 1.9 | 0 + 2 | 0 + 2.9 | 0 + 3 | 0 + 4 | 1 + 5 | 1 + 6 | 1 + 7 | 1 + 8 | 2 + 9 | 2 + 9.9 | 2 + 10 | 2 + 15 | 2 + 16 | 3 + 31 | 3 + 32 | 4 + 63 | 4 + 64 | 5 + 127 | 5 + 128 | 6 + 250 | 6 + 310 | 7 + 520 | 8 + 999 | 8 + end + + with_them do + it 'falls into an appropaite backoff slot' do + freeze_time do + backoff = described_class.new(started.seconds.ago) + expect(backoff.slot).to eq slot + end + end + end + end + + describe '#to_seconds' do + using RSpec::Parameterized::TableSyntax + + where(:started, :backoff) do + 0 | 1 + 0.1 | 1 + 0.9 | 1 + 1 | 1 + 1.1 | 1 + 1.9 | 1 + 2 | 1 + 3 | 1 + 4 | 2 + 5 | 2 + 6 | 2 + 6.5 | 2 + 7 | 2 + 8 | 4 + 9 | 4 + 9.9 | 4 + 10 | 4 + 15 | 4 + 16 | 8 + 31 | 8 + 32 | 16 + 63 | 16 + 64 | 32 + 127 | 32 + 128 | 64 + 250 | 64 + 310 | 64 + 520 | 64 + 999 | 64 + end + + with_them do + it 'calculates backoff based on an appropriate slot' do + freeze_time do + described_class.new(started.seconds.ago).then do |delay| + expect(delay.to_seconds).to eq backoff + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb index 92600b21afc..37524afc83d 100644 --- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb @@ -30,15 +30,6 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do it { expect(subject).to have_details } it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" } - - context 'when ci_bridge_pipeline_details is disabled' do - before do - stub_feature_flags(ci_bridge_pipeline_details: false) - end - - it { expect(subject).not_to have_details } - it { expect(subject.details_path).to be_nil } - end end context 'when user does not have access to read downstream pipeline' do diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb index 021b777a0ff..d27bb98ba9a 100644 --- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do end context 'when bridge is created' do - let(:bridge) { create(:ci_bridge) } + let(:bridge) { create_bridge(:created) } it 'matches correct core status' do expect(factory.core_status).to be_a Gitlab::Ci::Status::Created @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do end context 'when bridge is failed' do - let(:bridge) { create(:ci_bridge, :failed) } + let(:bridge) { create_bridge(:failed) } it 'matches correct core status' do expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed @@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do end end end + + context 'when bridge is a manual action' do + let(:bridge) { create_bridge(:playable) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Bridge::Manual, + Gitlab::Ci::Status::Bridge::Play, + Gitlab::Ci::Status::Bridge::Action] + end + + it 'fabricates action detailed status' do + expect(status).to be_a Gitlab::Ci::Status::Bridge::Action + end + + it 'fabricates status with correct details' do + expect(status.text).to eq s_('CiStatusText|manual') + expect(status.group).to eq 'manual' + expect(status.icon).to eq 'status_manual' + expect(status.favicon).to eq 'favicon_status_manual' + expect(status.illustration).to include(:image, :size, :title, :content) + expect(status.label).to include 'manual play action' + expect(status).not_to have_details + expect(status.action_path).to include 'play' + end + + context 'when user has ability to play action' do + before do + bridge.downstream_project.add_developer(user) + end + + it 'fabricates status that has action' do + expect(status).to have_action + end + end + + context 'when user does not have ability to play action' do + it 'fabricates status that has no action' do + expect(status).not_to have_action + end + end + end + + private + + def create_bridge(trait) + upstream_project = create(:project, :repository) + downstream_project = create(:project, :repository) + upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project) + trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } } + + create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline) + end end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index a35efae5c57..7fae76f61ea 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Canceled do describe '#group' do it { expect(subject.group).to eq 'canceled' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 1ddced923f6..1e54d1ed8c5 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Created do describe '#group' do it { expect(subject.group).to eq 'created' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index e8bd728b740..f3f3304b04d 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Failed do describe '#group' do it { expect(subject.group).to eq 'failed' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 0e47b19d9c1..1c062a0133d 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Pending do describe '#group' do it { expect(subject.group).to eq 'pending' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb index 6d33eb77560..ec1850c1959 100644 --- a/spec/lib/gitlab/ci/status/preparing_spec.rb +++ b/spec/lib/gitlab/ci/status/preparing_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Preparing do describe '#group' do it { expect(subject.group).to eq 'preparing' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index fbc7bfd81b3..e40d696ee4d 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Running do describe '#group' do it { expect(subject.group).to eq 'running' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb index 4a1dae937ca..8a923faf3f9 100644 --- a/spec/lib/gitlab/ci/status/scheduled_spec.rb +++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Scheduled do describe '#group' do it { expect(subject.group).to eq 'scheduled' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index f402bbe5221..ac3c2f253f7 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Skipped do describe '#group' do it { expect(subject.group).to eq 'skipped' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 2d1c50448d4..f2069334abd 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::Success do describe '#group' do it { expect(subject.group).to eq 'success' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb index de18198c6c2..bb6139accaf 100644 --- a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb +++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb @@ -26,4 +26,8 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do describe '#group' do it { expect(subject.group).to eq 'waiting-for-resource' } end + + describe '#details_path' do + it { expect(subject.details_path).to be_nil } + end end diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..8df739d9245 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') } + + describe 'the created pipeline' do + let(:user) { create(:admin) } + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + it 'does not create any jobs' do + expect(build_names).to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..5eec021b9d7 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform.latest.gitlab-ci.yml' do + before do + allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([]) + end + + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform.latest') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on master branch' do + it 'creates init, validate and build jobs' do + expect(build_names).to include('init', 'validate', 'build', 'deploy') + end + end + + context 'outside the master branch' do + let(:pipeline_branch) { 'patch-1' } + + before do + project.repository.create_branch(pipeline_branch) + end + + it 'does not creates a deploy and a test job' do + expect(build_names).not_to include('deploy') + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace/checksum_spec.rb b/spec/lib/gitlab/ci/trace/checksum_spec.rb new file mode 100644 index 00000000000..794794c3f69 --- /dev/null +++ b/spec/lib/gitlab/ci/trace/checksum_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Trace::Checksum do + let(:build) { create(:ci_build, :running) } + + subject { described_class.new(build) } + + context 'when build pending state exists' do + before do + create(:ci_build_pending_state, build: build, trace_checksum: 'crc32:d4777540') + end + + context 'when matching persisted trace chunks exist' do + before do + create_chunk(index: 0, data: 'a' * 128.kilobytes) + create_chunk(index: 1, data: 'b' * 128.kilobytes) + create_chunk(index: 2, data: 'ccccccccccccccccc') + end + + it 'calculates combined trace chunks CRC32 correctly' do + expect(subject.chunks_crc32).to eq 3564598592 + expect(subject).to be_valid + end + end + + context 'when trace chunks were persisted in a wrong order' do + before do + create_chunk(index: 0, data: 'b' * 128.kilobytes) + create_chunk(index: 1, data: 'a' * 128.kilobytes) + create_chunk(index: 2, data: 'ccccccccccccccccc') + end + + it 'makes trace checksum invalid' do + expect(subject).not_to be_valid + end + end + + context 'when one of the trace chunks is missing' do + before do + create_chunk(index: 0, data: 'a' * 128.kilobytes) + create_chunk(index: 2, data: 'ccccccccccccccccc') + end + + it 'makes trace checksum invalid' do + expect(subject).not_to be_valid + end + end + + context 'when checksums of persisted trace chunks do not match' do + before do + create_chunk(index: 0, data: 'a' * 128.kilobytes) + create_chunk(index: 1, data: 'X' * 128.kilobytes) + create_chunk(index: 2, data: 'ccccccccccccccccc') + end + + it 'makes trace checksum invalid' do + expect(subject).not_to be_valid + end + end + + context 'when persisted trace chunks are missing' do + it 'makes trace checksum invalid' do + expect(subject.state_crc32).to eq 3564598592 + expect(subject).not_to be_valid + end + end + end + + context 'when build pending state is missing' do + describe '#state_crc32' do + it 'returns nil' do + expect(subject.state_crc32).to be_nil + end + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + end + + describe '#trace_chunks' do + before do + create_chunk(index: 0, data: 'abcdefg') + end + + it 'does not load raw_data from a database store' do + subject.trace_chunks.first.then do |chunk| + expect(chunk).to be_database + expect { chunk.raw_data } + .to raise_error ActiveModel::MissingAttributeError + end + end + end + + describe '#last_chunk' do + context 'when there are no chunks' do + it 'returns nil' do + expect(subject.last_chunk).to be_nil + end + end + + context 'when there are multiple chunks' do + before do + create_chunk(index: 1, data: '1234') + create_chunk(index: 0, data: 'abcd') + end + + it 'returns chunk with the highest index' do + expect(subject.last_chunk.chunk_index).to eq 1 + end + end + end + + def create_chunk(index:, data:) + create(:ci_build_trace_chunk, :persisted, build: build, + chunk_index: index, + initial_data: data) + end +end diff --git a/spec/lib/gitlab/ci/trace/metrics_spec.rb b/spec/lib/gitlab/ci/trace/metrics_spec.rb new file mode 100644 index 00000000000..6518d0ab075 --- /dev/null +++ b/spec/lib/gitlab/ci/trace/metrics_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Trace::Metrics, :prometheus do + describe '#increment_trace_bytes' do + context 'when incrementing by more than one' do + it 'increments a single counter' do + subject.increment_trace_bytes(10) + subject.increment_trace_bytes(20) + subject.increment_trace_bytes(30) + + expect(described_class.trace_bytes.get).to eq 60 + expect(described_class.trace_bytes.values.count).to eq 1 + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 171877dbaee..92bf2519588 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -2,8 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do - let(:build) { create(:ci_build) } +RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_default: :keep do + let_it_be(:project) { create_default(:project) } + let_it_be_with_reload(:build) { create(:ci_build) } let(:trace) { described_class.new(build) } describe "associations" do @@ -32,6 +33,16 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do expect(artifact2.job.trace.raw).to eq(test_data) end + + it 'reloads the trace in case of a chunk error' do + chunk_error = described_class::ChunkedIO::FailedToGetChunkError + + allow_any_instance_of(described_class::Stream) + .to receive(:raw).and_raise(chunk_error) + + expect(build).to receive(:reset).and_return(build) + expect { trace.raw }.to raise_error(chunk_error) + end end context 'when live trace feature is disabled' do @@ -111,4 +122,13 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do end end end + + describe '#lock' do + it 'acquires an exclusive lease on the trace' do + trace.lock do + expect { trace.lock } + .to raise_error described_class::LockedError + end + end + end end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb new file mode 100644 index 00000000000..7e3cd7ec254 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Gitlab + module Ci + class YamlProcessor + RSpec.describe Result do + include StubRequests + + let(:user) { create(:user) } + let(:ci_config) { Gitlab::Ci::Config.new(config_content, user: user) } + let(:result) { described_class.new(ci_config: ci_config, warnings: ci_config&.warnings) } + + describe '#merged_yaml' do + subject(:merged_yaml) { result.merged_yaml } + + let(:config_content) do + YAML.dump( + include: { remote: 'https://example.com/sample.yml' }, + test: { stage: 'test', script: 'echo' } + ) + end + + let(:included_yml) do + YAML.dump( + another_test: { stage: 'test', script: 'echo 2' } + ) + end + + before do + stub_full_request('https://example.com/sample.yml').to_return(body: included_yml) + end + + it 'returns expanded yaml config' do + expanded_config = YAML.safe_load(merged_yaml, [Symbol]) + included_config = YAML.safe_load(included_yml, [Symbol]) + + expect(expanded_config).to include(*included_config.keys) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index d596494a987..fb6395e888a 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1361,7 +1361,8 @@ module Gitlab paths: ["logs/", "binaries/"], untracked: true, key: 'key', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1383,7 +1384,8 @@ module Gitlab paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1402,7 +1404,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: 'key', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1425,7 +1428,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: { files: ['file'] }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1448,7 +1452,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: { files: ['file'], prefix: 'prefix' }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1468,7 +1473,8 @@ module Gitlab paths: ["test/"], untracked: false, key: 'local', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end end @@ -2240,47 +2246,49 @@ module Gitlab end describe 'with parent-child pipeline' do + let(:config) do + YAML.dump({ + build1: { stage: 'build', script: 'test' }, + test1: { + stage: 'test', + trigger: { + include: includes + } + } + }) + end + context 'when artifact and job are specified' do - let(:config) do - YAML.dump({ - build1: { stage: 'build', script: 'test' }, - test1: { stage: 'test', trigger: { - include: [{ artifact: 'generated.yml', job: 'build1' }] - } } - }) - end + let(:includes) { [{ artifact: 'generated.yml', job: 'build1' }] } it { is_expected.to be_valid } end - context 'when job is not specified specified while artifact is' do - let(:config) do - YAML.dump({ - build1: { stage: 'build', script: 'test' }, - test1: { stage: 'test', trigger: { - include: [{ artifact: 'generated.yml' }] - } } - }) - end + context 'when job is not specified while artifact is' do + let(:includes) { [{ artifact: 'generated.yml' }] } it_behaves_like 'returns errors', /include config must specify the job where to fetch the artifact from/ end - context 'when include is a string' do - let(:config) do - YAML.dump({ - build1: { stage: 'build', script: 'test' }, - test1: { - stage: 'test', - trigger: { - include: 'generated.yml' - } - } - }) + context 'when project and file are specified' do + let(:includes) do + [{ file: 'generated.yml', project: 'my-namespace/my-project' }] end it { is_expected.to be_valid } end + + context 'when file is not specified while project is' do + let(:includes) { [{ project: 'something' }] } + + it_behaves_like 'returns errors', /include config must specify the file where to fetch the config from/ + end + + context 'when include is a string' do + let(:includes) { 'generated.yml' } + + it { is_expected.to be_valid } + end end describe "Error handling" do @@ -2457,13 +2465,13 @@ module Gitlab context 'returns errors if variables is not a map' do let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) } - it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs' + it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash' end context 'returns errors if variables is not a map of key-value strings' do let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) } - it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs' + it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash' end context 'returns errors if job when is not on_success, on_failure or always' do diff --git a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb index efdfc0a980b..6b568320953 100644 --- a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb +++ b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb @@ -42,12 +42,24 @@ RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do expect(null_logger).to receive(:info).with("Looking for orphan LFS files for project #{project.name_with_namespace}") expect(null_logger).to receive(:info).with("Removed invalid references: 1") expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:lfs_objects_size]) + expect(service).to receive(:remove_orphan_references).and_call_original expect { service.run! }.to change { project.lfs_objects.count }.from(2).to(1) expect(LfsObjectsProject.exists?(invalid_reference.id)).to be_falsey end + it 'does nothing if the project has no LFS objects' do + expect(null_logger).to receive(:info).with(/Looking for orphan LFS files/) + expect(null_logger).to receive(:info).with(/Nothing to do/) + + project.lfs_objects_projects.delete_all + + expect(service).not_to receive(:remove_orphan_references) + + service.run! + end + context 'LFS object is in design repository' do before do expect(project.design_repository).to receive(:exists?).and_return(true) diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index f2bc6390032..37349c30224 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -3,18 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::ClosingIssueExtractor do - let(:project) { create(:project) } - let(:project2) { create(:project) } - let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute } - let(:issue) { create(:issue, project: project) } - let(:issue2) { create(:issue, project: project2) } + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, project: project2) } let(:reference) { issue.to_reference } let(:cross_reference) { issue2.to_reference(project) } - let(:fork_cross_reference) { issue.to_reference(forked_project) } subject { described_class.new(project, project.creator) } - before do + before_all do project.add_developer(project.creator) project.add_developer(project2.creator) project2.add_maintainer(project.creator) @@ -325,6 +323,9 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end context "with a cross-project fork reference" do + let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute } + let(:fork_cross_reference) { issue.to_reference(forked_project) } + subject { described_class.new(forked_project, forked_project.creator) } it do @@ -348,8 +349,8 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end context 'with multiple references' do - let(:other_issue) { create(:issue, project: project) } - let(:third_issue) { create(:issue, project: project) } + let_it_be(:other_issue) { create(:issue, project: project) } + let_it_be(:third_issue) { create(:issue, project: project) } let(:reference2) { other_issue.to_reference } let(:reference3) { third_issue.to_reference } diff --git a/spec/lib/gitlab/code_navigation_path_spec.rb b/spec/lib/gitlab/code_navigation_path_spec.rb index 4dc864b158d..206541f7c0d 100644 --- a/spec/lib/gitlab/code_navigation_path_spec.rb +++ b/spec/lib/gitlab/code_navigation_path_spec.rb @@ -16,10 +16,6 @@ RSpec.describe Gitlab::CodeNavigationPath do subject { described_class.new(project, commit_sha).full_json_path_for(path) } - before do - stub_feature_flags(code_navigation: project) - end - context 'when a pipeline exist for a sha' do it 'returns path to a file in the artifact' do expect(subject).to eq(lsif_path) @@ -41,15 +37,5 @@ RSpec.describe Gitlab::CodeNavigationPath do expect(subject).to eq(lsif_path) end end - - context 'when code_navigation feature is disabled' do - before do - stub_feature_flags(code_navigation: false) - end - - it 'returns nil' do - expect(subject).to be_nil - end - end end end diff --git a/spec/lib/gitlab/config/entry/composable_array_spec.rb b/spec/lib/gitlab/config/entry/composable_array_spec.rb new file mode 100644 index 00000000000..77766cb3b0a --- /dev/null +++ b/spec/lib/gitlab/config/entry/composable_array_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Config::Entry::ComposableArray, :aggregate_failures do + let(:valid_config) do + [ + { + DATABASE_SECRET: 'passw0rd' + }, + { + API_TOKEN: 'passw0rd2' + } + ] + end + + let(:config) { valid_config } + let(:entry) { described_class.new(config) } + + before do + allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node) + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + + context 'is invalid' do + let(:config) { { hello: :world } } + + it { expect(entry).not_to be_valid } + end + end + + describe '#compose!' do + before do + entry.compose! + end + + it 'composes child entry with configured value' do + expect(entry.value).to eq(config) + end + + it 'composes child entries with configured values' do + expect(entry[0]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[0].description).to eq('node definition') + expect(entry[0].key).to eq('node') + expect(entry[0].metadata).to eq({}) + expect(entry[0].parent.class).to eq(Gitlab::Config::Entry::ComposableArray) + expect(entry[0].value).to eq(DATABASE_SECRET: 'passw0rd') + expect(entry[1]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[1].description).to eq('node definition') + expect(entry[1].key).to eq('node') + expect(entry[1].metadata).to eq({}) + expect(entry[1].parent.class).to eq(Gitlab::Config::Entry::ComposableArray) + expect(entry[1].value).to eq(API_TOKEN: 'passw0rd2') + end + + describe '#descendants' do + it 'creates descendant nodes' do + expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.first.value).to eq(DATABASE_SECRET: 'passw0rd') + expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.second.value).to eq(API_TOKEN: 'passw0rd2') + end + end + end +end diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb new file mode 100644 index 00000000000..15bbf2047c5 --- /dev/null +++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do + let(:valid_config) do + { + DATABASE_SECRET: 'passw0rd', + API_TOKEN: 'passw0rd2' + } + end + + let(:config) { valid_config } + + shared_examples 'composes a hash' do + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + + context 'is invalid' do + let(:config) { %w[one two] } + + it { expect(entry).not_to be_valid } + end + end + + describe '#value' do + context 'when config is a hash' do + it 'returns key value' do + expect(entry.value).to eq config + end + end + end + + describe '#compose!' do + before do + entry.compose! + end + + it 'composes child entry with configured value' do + expect(entry.value).to eq(config) + end + + it 'composes child entries with configured values' do + expect(entry[:DATABASE_SECRET]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[:DATABASE_SECRET].description).to eq('DATABASE_SECRET node definition') + expect(entry[:DATABASE_SECRET].key).to eq(:DATABASE_SECRET) + expect(entry[:DATABASE_SECRET].metadata).to eq(name: :DATABASE_SECRET) + expect(entry[:DATABASE_SECRET].parent.class).to eq(Gitlab::Config::Entry::ComposableHash) + expect(entry[:DATABASE_SECRET].value).to eq('passw0rd') + expect(entry[:API_TOKEN]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[:API_TOKEN].description).to eq('API_TOKEN node definition') + expect(entry[:API_TOKEN].key).to eq(:API_TOKEN) + expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN) + expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash) + expect(entry[:API_TOKEN].value).to eq('passw0rd2') + end + + describe '#descendants' do + it 'creates descendant nodes' do + expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.first.value).to eq('passw0rd') + expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.second.value).to eq('passw0rd2') + end + end + end + end + + context 'when ComposableHash is instantiated' do + let(:entry) { described_class.new(config) } + + before do + allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node) + end + + it_behaves_like 'composes a hash' + end + + context 'when ComposableHash entry is configured in the parent class' do + let(:composable_hash_parent_class) do + Class.new(Gitlab::Config::Entry::Node) do + include ::Gitlab::Config::Entry::Configurable + + entry :secrets, ::Gitlab::Config::Entry::ComposableHash, + description: 'Configured secrets for this job', + inherit: false, + default: { hello: :world }, + metadata: { composable_class: Gitlab::Config::Entry::Node } + end + end + + let(:entry) do + parent_entry = composable_hash_parent_class.new(secrets: config) + parent_entry.compose! + + parent_entry[:secrets] + end + + it_behaves_like 'composes a hash' + + it 'creates entry with configuration from parent class' do + expect(entry.default).to eq({ hello: :world }) + expect(entry.metadata).to eq(composable_class: Gitlab::Config::Entry::Node) + end + end +end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index b54fe40bb5f..80bd517ec92 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -262,7 +262,7 @@ RSpec.describe Gitlab::Conflict::File do end it 'includes the blob icon for the file' do - expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o') + expect(conflict_file.as_json[:blob_icon]).to eq('doc-text') end context 'with the full_content option passed' do diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index e0a8e2c17a3..a31f34d82d7 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -2,16 +2,20 @@ require 'spec_helper' -RSpec.describe 'cycle analytics events' do - let(:project) { create(:project, :repository) } +RSpec.describe 'cycle analytics events', :aggregate_failures do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user, :admin) } let(:from_date) { 10.days.ago } - let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let(:events) do - CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user })[stage].events + CycleAnalytics::ProjectLevel + .new(project, options: { from: from_date, current_user: user })[stage] + .events end + let(:event) { events.first } + before do setup(context) end @@ -19,36 +23,15 @@ RSpec.describe 'cycle analytics events' do describe '#issue_events' do let(:stage) { :issue } - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty - end - - it 'has a title' do - expect(events.first[:title]).to eq(context.title) - end - - it 'has the URL' do - expect(events.first[:url]).not_to be_nil - end - - it 'has an iid' do - expect(events.first[:iid]).to eq(context.iid.to_s) - end - - it 'has a created_at timestamp' do - expect(events.first[:created_at]).to end_with('ago') - end - - it "has the author's URL" do - expect(events.first[:author][:web_url]).not_to be_nil - end - - it "has the author's avatar URL" do - expect(events.first[:author][:avatar_url]).not_to be_nil - end - - it "has the author's name" do - expect(events.first[:author][:name]).to eq(context.author.name) + it 'has correct attributes' do + expect(event[:total_time]).not_to be_empty + expect(event[:title]).to eq(context.title) + expect(event[:url]).not_to be_nil + expect(event[:iid]).to eq(context.iid.to_s) + expect(event[:created_at]).to end_with('ago') + expect(event[:author][:web_url]).not_to be_nil + expect(event[:author][:avatar_url]).not_to be_nil + expect(event[:author][:name]).to eq(context.author.name) end end @@ -59,36 +42,15 @@ RSpec.describe 'cycle analytics events' do create_commit_referencing_issue(context) end - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty - end - - it 'has a title' do - expect(events.first[:title]).to eq(context.title) - end - - it 'has the URL' do - expect(events.first[:url]).not_to be_nil - end - - it 'has an iid' do - expect(events.first[:iid]).to eq(context.iid.to_s) - end - - it 'has a created_at timestamp' do - expect(events.first[:created_at]).to end_with('ago') - end - - it "has the author's URL" do - expect(events.first[:author][:web_url]).not_to be_nil - end - - it "has the author's avatar URL" do - expect(events.first[:author][:avatar_url]).not_to be_nil - end - - it "has the author's name" do - expect(events.first[:author][:name]).to eq(context.author.name) + it 'has correct attributes' do + expect(event[:total_time]).not_to be_empty + expect(event[:title]).to eq(context.title) + expect(event[:url]).not_to be_nil + expect(event[:iid]).to eq(context.iid.to_s) + expect(event[:created_at]).to end_with('ago') + expect(event[:author][:web_url]).not_to be_nil + expect(event[:author][:avatar_url]).not_to be_nil + expect(event[:author][:name]).to eq(context.author.name) end end @@ -100,32 +62,14 @@ RSpec.describe 'cycle analytics events' do create_commit_referencing_issue(context) end - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty - end - - it 'has a title' do - expect(events.first[:title]).to eq('Awesome merge_request') - end - - it 'has an iid' do - expect(events.first[:iid]).to eq(context.iid.to_s) - end - - it 'has a created_at timestamp' do - expect(events.first[:created_at]).to end_with('ago') - end - - it "has the author's URL" do - expect(events.first[:author][:web_url]).not_to be_nil - end - - it "has the author's avatar URL" do - expect(events.first[:author][:avatar_url]).not_to be_nil - end - - it "has the author's name" do - expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + it 'has correct attributes' do + expect(event[:total_time]).not_to be_empty + expect(event[:title]).to eq('Awesome merge_request') + expect(event[:iid]).to eq(context.iid.to_s) + expect(event[:created_at]).to end_with('ago') + expect(event[:author][:web_url]).not_to be_nil + expect(event[:author][:avatar_url]).not_to be_nil + expect(event[:author][:name]).to eq(MergeRequest.first.author.name) end end @@ -152,40 +96,16 @@ RSpec.describe 'cycle analytics events' do merge_merge_requests_closing_issue(user, project, context) end - it 'has the name' do - expect(events.first[:name]).not_to be_nil - end - - it 'has the ID' do - expect(events.first[:id]).not_to be_nil - end - - it 'has the URL' do - expect(events.first[:url]).not_to be_nil - end - - it 'has the branch name' do - expect(events.first[:branch]).not_to be_nil - end - - it 'has the branch URL' do - expect(events.first[:branch][:url]).not_to be_nil - end - - it 'has the short SHA' do - expect(events.first[:short_sha]).not_to be_nil - end - - it 'has the commit URL' do - expect(events.first[:commit_url]).not_to be_nil - end - - it 'has the date' do - expect(events.first[:date]).not_to be_nil - end - - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty + it 'has correct attributes' do + expect(event[:name]).not_to be_nil + expect(event[:id]).not_to be_nil + expect(event[:url]).not_to be_nil + expect(event[:branch]).not_to be_nil + expect(event[:branch][:url]).not_to be_nil + expect(event[:short_sha]).not_to be_nil + expect(event[:commit_url]).not_to be_nil + expect(event[:date]).not_to be_nil + expect(event[:total_time]).not_to be_empty end end @@ -197,40 +117,16 @@ RSpec.describe 'cycle analytics events' do merge_merge_requests_closing_issue(user, project, context) end - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty - end - - it 'has a title' do - expect(events.first[:title]).to eq('Awesome merge_request') - end - - it 'has an iid' do - expect(events.first[:iid]).to eq(context.iid.to_s) - end - - it 'has the URL' do - expect(events.first[:url]).not_to be_nil - end - - it 'has a state' do - expect(events.first[:state]).not_to be_nil - end - - it 'has a created_at timestamp' do - expect(events.first[:created_at]).not_to be_nil - end - - it "has the author's URL" do - expect(events.first[:author][:web_url]).not_to be_nil - end - - it "has the author's avatar URL" do - expect(events.first[:author][:avatar_url]).not_to be_nil - end - - it "has the author's name" do - expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + it 'has correct attributes' do + expect(event[:total_time]).not_to be_empty + expect(event[:title]).to eq('Awesome merge_request') + expect(event[:iid]).to eq(context.iid.to_s) + expect(event[:url]).not_to be_nil + expect(event[:state]).not_to be_nil + expect(event[:created_at]).not_to be_nil + expect(event[:author][:web_url]).not_to be_nil + expect(event[:author][:avatar_url]).not_to be_nil + expect(event[:author][:name]).to eq(MergeRequest.first.author.name) end end @@ -257,58 +153,25 @@ RSpec.describe 'cycle analytics events' do deploy_master(user, project) end - it 'has the name' do - expect(events.first[:name]).not_to be_nil - end - - it 'has the ID' do - expect(events.first[:id]).not_to be_nil - end - - it 'has the URL' do - expect(events.first[:url]).not_to be_nil - end - - it 'has the branch name' do - expect(events.first[:branch]).not_to be_nil - end - - it 'has the branch URL' do - expect(events.first[:branch][:url]).not_to be_nil - end - - it 'has the short SHA' do - expect(events.first[:short_sha]).not_to be_nil - end - - it 'has the commit URL' do - expect(events.first[:commit_url]).not_to be_nil - end - - it 'has the date' do - expect(events.first[:date]).not_to be_nil - end - - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty - end - - it "has the author's URL" do - expect(events.first[:author][:web_url]).not_to be_nil - end - - it "has the author's avatar URL" do - expect(events.first[:author][:avatar_url]).not_to be_nil - end - - it "has the author's name" do - expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) + it 'has correct attributes' do + expect(event[:name]).not_to be_nil + expect(event[:id]).not_to be_nil + expect(event[:url]).not_to be_nil + expect(event[:branch]).not_to be_nil + expect(event[:branch][:url]).not_to be_nil + expect(event[:short_sha]).not_to be_nil + expect(event[:commit_url]).not_to be_nil + expect(event[:date]).not_to be_nil + expect(event[:total_time]).not_to be_empty + expect(event[:author][:web_url]).not_to be_nil + expect(event[:author][:avatar_url]).not_to be_nil + expect(event[:author][:name]).to eq(MergeRequest.first.author.name) end end def setup(context) milestone = create(:milestone, project: project) - context.update(milestone: milestone) + context.update!(milestone: milestone) mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}") ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb index c31522c538d..882cede759b 100644 --- a/spec/lib/gitlab/danger/commit_linter_spec.rb +++ b/spec/lib/gitlab/danger/commit_linter_spec.rb @@ -323,6 +323,16 @@ RSpec.describe Gitlab::Danger::CommitLinter do end end + context 'when message includes a value that is surrounded by backticks' do + let(:commit_message) { "A commit message `%20`" } + + it 'does not add a problem' do + expect(commit_linter).not_to receive(:add_problem) + + commit_linter.lint + end + end + context 'when message includes a short reference' do [ 'A commit message to fix #1234', @@ -336,7 +346,9 @@ RSpec.describe Gitlab::Danger::CommitLinter do 'A commit message to fix gitlab-org/gitlab#1234', 'A commit message to fix gitlab-org/gitlab!1234', 'A commit message to fix gitlab-org/gitlab&1234', - 'A commit message to fix gitlab-org/gitlab%1234' + 'A commit message to fix gitlab-org/gitlab%1234', + 'A commit message to fix "gitlab-org/gitlab%1234"', + 'A commit message to fix `gitlab-org/gitlab%1234' ].each do |message| let(:commit_message) { message } diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index c7d55c396ef..509649f08c6 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -284,7 +284,8 @@ RSpec.describe Gitlab::Danger::Helper do '.codeclimate.yml' | [:engineering_productivity] '.gitlab/CODEOWNERS' | [:engineering_productivity] - 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:backend] + 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template] + 'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template] 'ee/FOO_VERSION' | [:unknown] @@ -376,6 +377,7 @@ RSpec.describe Gitlab::Danger::Helper do :none | '' :qa | '~QA' :engineering_productivity | '~"Engineering Productivity" for CI, Danger' + :ci_template | '~"ci::templates"' end with_them do @@ -435,6 +437,28 @@ RSpec.describe Gitlab::Danger::Helper do end end + describe '#draft_mr?' do + it 'returns false when `gitlab_helper` is unavailable' do + expect(helper).to receive(:gitlab_helper).and_return(nil) + + expect(helper).not_to be_draft_mr + end + + it 'returns true for a draft MR' do + expect(fake_gitlab).to receive(:mr_json) + .and_return('title' => 'Draft: My MR title') + + expect(helper).to be_draft_mr + end + + it 'returns false for non draft MR' do + expect(fake_gitlab).to receive(:mr_json) + .and_return('title' => 'My MR title') + + expect(helper).not_to be_draft_mr + end + end + describe '#cherry_pick_mr?' do it 'returns false when `gitlab_helper` is unavailable' do expect(helper).to receive(:gitlab_helper).and_return(nil) diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index b471e17e2e7..1a900dfba22 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -4,10 +4,13 @@ require 'webmock/rspec' require 'timecop' require 'gitlab/danger/roulette' +require 'active_support/testing/time_helpers' RSpec.describe Gitlab::Danger::Roulette do + include ActiveSupport::Testing::TimeHelpers + around do |example| - Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run } + travel_to(Time.utc(2020, 06, 22, 10)) { example.run } end let(:backend_available) { true } @@ -67,14 +70,30 @@ RSpec.describe Gitlab::Danger::Roulette do ) end - let(:teammate_json) do + let(:ci_template_reviewer) do + Gitlab::Danger::Teammate.new( + 'username' => 'ci-template-maintainer', + 'name' => 'CI Template engineer', + 'role' => '~"ci::templates"', + 'projects' => { 'gitlab' => 'reviewer ci_template' }, + 'available' => true, + 'tz_offset_hours' => 2.0 + ) + end + + let(:teammates) do [ backend_maintainer.to_h, frontend_maintainer.to_h, frontend_reviewer.to_h, software_engineer_in_test.to_h, - engineering_productivity_reviewer.to_h - ].to_json + engineering_productivity_reviewer.to_h, + ci_template_reviewer.to_h + ] + end + + let(:teammate_json) do + teammates.to_json end subject(:roulette) { Object.new.extend(described_class) } @@ -162,6 +181,14 @@ RSpec.describe Gitlab::Danger::Roulette do end end + context 'when change contains CI/CD Template category' do + let(:categories) { [:ci_template] } + + it 'assigns CI/CD Template reviewer and fallback to backend maintainer' do + expect(spins).to eq([described_class::Spin.new(:ci_template, ci_template_reviewer, backend_maintainer, false, false)]) + end + end + context 'when change contains test category' do let(:categories) { [:test] } @@ -210,6 +237,69 @@ RSpec.describe Gitlab::Danger::Roulette do end end end + + describe 'reviewer suggestion probability' do + let(:reviewer) { teammate_with_capability('reviewer', 'reviewer backend') } + let(:hungry_reviewer) { teammate_with_capability('hungry_reviewer', 'reviewer backend', hungry: true) } + let(:traintainer) { teammate_with_capability('traintainer', 'trainee_maintainer backend') } + let(:hungry_traintainer) { teammate_with_capability('hungry_traintainer', 'trainee_maintainer backend', hungry: true) } + let(:teammates) do + [ + reviewer.to_h, + hungry_reviewer.to_h, + traintainer.to_h, + hungry_traintainer.to_h + ] + end + + let(:categories) { [:backend] } + + # This test is testing probability with inherent randomness. + # The variance is inversely related to sample size + # Given large enough sample size, the variance would be smaller, + # but the test would take longer. + # Given smaller sample size, the variance would be larger, + # but the test would take less time. + let!(:sample_size) { 500 } + let!(:variance) { 0.1 } + + before do + # This test needs actual randomness to simulate probabilities + allow(subject).to receive(:new_random).and_return(Random.new) + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(body: teammate_json) + end + + it 'has 1:2:3:4 probability of picking reviewer, hungry_reviewer, traintainer, hungry_traintainer' do + picks = Array.new(sample_size).map do + spins = subject.spin(project, categories, timezone_experiment: timezone_experiment) + spins.first.reviewer.name + end + + expect(probability(picks, 'reviewer')).to be_within(variance).of(0.1) + expect(probability(picks, 'hungry_reviewer')).to be_within(variance).of(0.2) + expect(probability(picks, 'traintainer')).to be_within(variance).of(0.3) + expect(probability(picks, 'hungry_traintainer')).to be_within(variance).of(0.4) + end + + def probability(picks, role) + picks.count(role).to_f / picks.length + end + + def teammate_with_capability(name, capability, hungry: false) + Gitlab::Danger::Teammate.new( + { + 'name' => name, + 'projects' => { + 'gitlab' => capability + }, + 'available' => true, + 'hungry' => hungry + } + ) + end + end end RSpec::Matchers.define :match_teammates do |expected| @@ -265,7 +355,8 @@ RSpec.describe Gitlab::Danger::Roulette do frontend_reviewer, frontend_maintainer, software_engineer_in_test, - engineering_productivity_reviewer + engineering_productivity_reviewer, + ci_template_reviewer ]) end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index 6fd32493d6b..eebe14ed5e1 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -4,6 +4,7 @@ require 'timecop' require 'rspec-parameterized' require 'gitlab/danger/teammate' +require 'active_support/testing/time_helpers' RSpec.describe Gitlab::Danger::Teammate do using RSpec::Parameterized::TableSyntax @@ -148,8 +149,10 @@ RSpec.describe Gitlab::Danger::Teammate do end describe '#local_hour' do + include ActiveSupport::Testing::TimeHelpers + around do |example| - Timecop.freeze(Time.utc(2020, 6, 23, 10)) { example.run } + travel_to(Time.utc(2020, 6, 23, 10)) { example.run } end context 'when author is given' do diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index 155e66e2fcd..8fb7ab25b17 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project) deployable = deployment.deployable expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable) - expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user) + expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.deployed_by) expected_commit_url = Gitlab::UrlBuilder.build(commit) data = described_class.build(deployment) @@ -30,7 +30,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do expect(data[:environment]).to eq("somewhere") expect(data[:project]).to eq(project.hook_attrs) expect(data[:short_sha]).to eq(deployment.short_sha) - expect(data[:user]).to eq(deployment.user.hook_attrs) + expect(data[:user]).to eq(deployment.deployed_by.hook_attrs) expect(data[:user_url]).to eq(expected_user_url) expect(data[:commit_url]).to eq(expected_commit_url) expect(data[:commit_title]).to eq(commit.title) diff --git a/spec/lib/gitlab/database/background_migration_job_spec.rb b/spec/lib/gitlab/database/background_migration_job_spec.rb index dd5bf8b512f..42695925a1c 100644 --- a/spec/lib/gitlab/database/background_migration_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration_job_spec.rb @@ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::BackgroundMigrationJob do let!(:job1) { create(:background_migration_job, :succeeded, created_at: initial_time, updated_at: initial_time) } it 'does not update non-pending jobs' do - Timecop.freeze(initial_time + 1.day) do + travel_to(initial_time + 1.day) do expect { described_class.mark_all_as_succeeded('TestJob', [1, 100]) } .to change { described_class.succeeded.count }.from(1).to(2) end diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index 71d3666602f..31a8b4afa03 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BatchCount do let_it_be(:fallback) { ::Gitlab::Database::BatchCounter::FALLBACK } - let_it_be(:small_batch_size) { ::Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE - 1 } + let_it_be(:small_batch_size) { calculate_batch_size(::Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE) } let(:model) { Issue } let(:column) { :author_id } @@ -22,6 +22,12 @@ RSpec.describe Gitlab::Database::BatchCount do allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction) end + def calculate_batch_size(batch_size) + zero_offset_modifier = -1 + + batch_size + zero_offset_modifier + end + shared_examples 'disallowed configurations' do |method| it 'returns fallback if start is bigger than finish' do expect(described_class.public_send(method, *args, start: 1, finish: 0)).to eq(fallback) @@ -45,6 +51,46 @@ RSpec.describe Gitlab::Database::BatchCount do end end + shared_examples 'when batch fetch query is canceled' do + let(:batch_size) { 22_000 } + let(:relation) { instance_double(ActiveRecord::Relation) } + + it 'reduces batch size by half and retry fetch' do + too_big_batch_relation_mock = instance_double(ActiveRecord::Relation) + allow(model).to receive_message_chain(:select, public_send: relation) + allow(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size)).and_return(too_big_batch_relation_mock) + allow(too_big_batch_relation_mock).to receive(:send).and_raise(ActiveRecord::QueryCanceled) + + expect(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size / 2)).and_return(double(send: 1)) + + subject.call(model, column, batch_size: batch_size, start: 0) + end + + context 'when all retries fail' do + let(:batch_count_query) { 'SELECT COUNT(id) FROM relation WHERE id BETWEEN 0 and 1' } + + before do + allow(model).to receive_message_chain(:select, :public_send, where: relation) + allow(relation).to receive(:send).and_raise(ActiveRecord::QueryCanceled.new('query timed out')) + allow(relation).to receive(:to_sql).and_return(batch_count_query) + end + + it 'logs failing query' do + expect(Gitlab::AppJsonLogger).to receive(:error).with( + event: 'batch_count', + relation: model.table_name, + operation: operation, + operation_args: operation_args, + start: 0, + mode: mode, + query: batch_count_query, + message: 'Query has been canceled with message: query timed out' + ) + expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(-1) + end + end + end + describe '#batch_count' do it 'counts table' do expect(described_class.batch_count(model)).to eq(5) @@ -86,10 +132,11 @@ RSpec.describe Gitlab::Database::BatchCount do it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE}" do min_id = model.minimum(:id) + relation = instance_double(ActiveRecord::Relation) + allow(model).to receive_message_chain(:select, public_send: relation) + batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE) - expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter| - expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE + min_id, :itself).once.and_call_original - end + expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1)) described_class.batch_count(model) end @@ -98,6 +145,15 @@ RSpec.describe Gitlab::Database::BatchCount do subject { described_class.batch_count(model) } end + it_behaves_like 'when batch fetch query is canceled' do + let(:mode) { :itself } + let(:operation) { :count } + let(:operation_args) { nil } + let(:column) { nil } + + subject { described_class.method(:batch_count) } + end + context 'disallowed_configurations' do include_examples 'disallowed configurations', :batch_count do let(:args) { [Issue] } @@ -108,6 +164,24 @@ RSpec.describe Gitlab::Database::BatchCount do expect { described_class.batch_count(model.distinct(column)) }.to raise_error 'Use distinct count for optimized distinct counting' end end + + context 'when a relation is grouped' do + let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) } + + before do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1) + end + + context 'count by default column' do + let(:count) do + described_class.batch_count(model.group(column), batch_size: 2) + end + + it 'counts grouped records' do + expect(count).to eq({ user.id => 4, another_user.id => 2 }) + end + end + end end describe '#batch_distinct_count' do @@ -151,10 +225,11 @@ RSpec.describe Gitlab::Database::BatchCount do it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE}" do min_id = model.minimum(:id) + relation = instance_double(ActiveRecord::Relation) + allow(model).to receive_message_chain(:select, public_send: relation) + batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE) - expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter| - expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE + min_id, :distinct).once.and_call_original - end + expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1)) described_class.batch_distinct_count(model) end @@ -175,6 +250,33 @@ RSpec.describe Gitlab::Database::BatchCount do end.to raise_error 'Use distinct count only with non id fields' end end + + context 'when a relation is grouped' do + let!(:one_more_issue) { create(:issue, author: user, project: model.first.project) } + + before do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 1) + end + + context 'distinct count by non-unique column' do + let(:count) do + described_class.batch_distinct_count(model.group(column), :project_id, batch_size: 2) + end + + it 'counts grouped records' do + expect(count).to eq({ user.id => 3, another_user.id => 2 }) + end + end + end + + it_behaves_like 'when batch fetch query is canceled' do + let(:mode) { :distinct } + let(:operation) { :count } + let(:operation_args) { nil } + let(:column) { nil } + + subject { described_class.method(:batch_distinct_count) } + end end describe '#batch_sum' do @@ -209,10 +311,11 @@ RSpec.describe Gitlab::Database::BatchCount do it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE}" do min_id = model.minimum(:id) + relation = instance_double(ActiveRecord::Relation) + allow(model).to receive_message_chain(:select, public_send: relation) + batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE) - expect_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter| - expect(batch_counter).to receive(:batch_fetch).with(min_id, Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE + min_id, :itself).once.and_call_original - end + expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1)) described_class.batch_sum(model, column) end @@ -226,5 +329,13 @@ RSpec.describe Gitlab::Database::BatchCount do let(:default_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE } let(:small_batch_size) { Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE - 1 } end + + it_behaves_like 'when batch fetch query is canceled' do + let(:mode) { :itself } + let(:operation) { :sum } + let(:operation_args) { [column] } + + subject { described_class.method(:batch_sum) } + end end end diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb new file mode 100644 index 00000000000..f2a7d6e69d8 --- /dev/null +++ b/spec/lib/gitlab/database/bulk_update_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BulkUpdate do + describe 'error states' do + let(:columns) { %i[title] } + + let_it_be(:mapping) do + create_default(:user) + create_default(:project) + + i_a, i_b = create_list(:issue, 2) + + { + i_a => { title: 'Issue a' }, + i_b => { title: 'Issue b' } + } + end + + it 'does not raise errors on valid inputs' do + expect { described_class.execute(columns, mapping) }.not_to raise_error + end + + it 'expects a non-empty list of column names' do + expect { described_class.execute([], mapping) }.to raise_error(ArgumentError) + end + + it 'expects all columns to be symbols' do + expect { described_class.execute([1], mapping) }.to raise_error(ArgumentError) + end + + it 'expects all columns to be valid columns on the tables' do + expect { described_class.execute([:foo], mapping) }.to raise_error(ArgumentError) + end + + it 'refuses to set ID' do + expect { described_class.execute([:id], mapping) }.to raise_error(ArgumentError) + end + + it 'expects a non-empty mapping' do + expect { described_class.execute(columns, []) }.to raise_error(ArgumentError) + end + + it 'expects all map values to be Hash instances' do + bad_map = mapping.merge(build(:issue) => 2) + + expect { described_class.execute(columns, bad_map) }.to raise_error(ArgumentError) + end + end + + it 'is possible to update all objects in a single query' do + users = create_list(:user, 3) + mapping = users.zip(%w(foo bar baz)).to_h do |u, name| + [u, { username: name, admin: true }] + end + + expect do + described_class.execute(%i[username admin], mapping) + end.not_to exceed_query_limit(1) + + # We have optimistically updated the values + expect(users).to all(be_admin) + expect(users.map(&:username)).to eq(%w(foo bar baz)) + + users.each(&:reset) + + # The values are correct on reset + expect(users).to all(be_admin) + expect(users.map(&:username)).to eq(%w(foo bar baz)) + end + + it 'is possible to update heterogeneous sets' do + create_default(:user) + create_default(:project) + + mr_a = create(:merge_request) + i_a, i_b = create_list(:issue, 2) + + mapping = { + mr_a => { title: 'MR a' }, + i_a => { title: 'Issue a' }, + i_b => { title: 'Issue b' } + } + + expect do + described_class.execute(%i[title], mapping) + end.not_to exceed_query_limit(2) + + expect([mr_a, i_a, i_b].map { |x| x.reset.title }) + .to eq(['MR a', 'Issue a', 'Issue b']) + end + + shared_examples 'basic functionality' do + it 'sets multiple values' do + create_default(:user) + create_default(:project) + + i_a, i_b = create_list(:issue, 2) + + mapping = { + i_a => { title: 'Issue a' }, + i_b => { title: 'Issue b' } + } + + described_class.execute(%i[title], mapping) + + expect([i_a, i_b].map { |x| x.reset.title }) + .to eq(['Issue a', 'Issue b']) + end + end + + include_examples 'basic functionality' + + context 'when prepared statements are configured differently to the normal test environment' do + # rubocop: disable RSpec/LeakyConstantDeclaration + # This cop is disabled because you cannot call establish_connection on + # an anonymous class. + class ActiveRecordBasePreparedStatementsInverted < ActiveRecord::Base + def self.abstract_class? + true # So it gets its own connection + end + end + # rubocop: enable RSpec/LeakyConstantDeclaration + + before_all do + c = ActiveRecord::Base.connection.instance_variable_get(:@config) + inverted = c.merge(prepared_statements: !ActiveRecord::Base.connection.prepared_statements) + ActiveRecordBasePreparedStatementsInverted.establish_connection(inverted) + end + + before do + allow(ActiveRecord::Base).to receive(:connection_specification_name) + .and_return(ActiveRecordBasePreparedStatementsInverted.connection_specification_name) + end + + include_examples 'basic functionality' + end +end diff --git a/spec/lib/gitlab/database/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/concurrent_reindex_spec.rb deleted file mode 100644 index 4e2c3f547d4..00000000000 --- a/spec/lib/gitlab/database/concurrent_reindex_spec.rb +++ /dev/null @@ -1,207 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do - subject { described_class.new(index_name, logger: logger) } - - let(:table_name) { '_test_reindex_table' } - let(:column_name) { '_test_column' } - let(:index_name) { '_test_reindex_index' } - let(:logger) { double('logger', debug: nil, info: nil, error: nil ) } - let(:connection) { ActiveRecord::Base.connection } - - before do - connection.execute(<<~SQL) - CREATE TABLE #{table_name} ( - id serial NOT NULL PRIMARY KEY, - #{column_name} integer NOT NULL); - - CREATE INDEX #{index_name} ON #{table_name} (#{column_name}); - SQL - end - - context 'when the index does not exist' do - before do - connection.execute(<<~SQL) - DROP INDEX #{index_name} - SQL - end - - it 'raises an error' do - expect { subject.execute }.to raise_error(described_class::ReindexError, /does not exist/) - end - end - - context 'when the index is unique' do - before do - connection.execute(<<~SQL) - DROP INDEX #{index_name}; - CREATE UNIQUE INDEX #{index_name} ON #{table_name} (#{column_name}) - SQL - end - - it 'raises an error' do - expect do - subject.execute - end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/) - end - end - - context 'replacing the original index with a rebuilt copy' do - let(:replacement_name) { 'tmp_reindex__test_reindex_index' } - let(:replaced_name) { 'old_reindex__test_reindex_index' } - - let(:create_index) { "CREATE INDEX CONCURRENTLY #{replacement_name} ON public.#{table_name} USING btree (#{column_name})" } - let(:drop_index) { "DROP INDEX CONCURRENTLY IF EXISTS #{replacement_name}" } - - let!(:original_index) { find_index_create_statement } - - before do - allow(subject).to receive(:connection).and_return(connection) - allow(subject).to receive(:disable_statement_timeout).and_yield - end - - it 'replaces the existing index with an identical index' do - expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield - - expect_to_execute_concurrently_in_order(drop_index) - expect_to_execute_concurrently_in_order(create_index) - - expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| - expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield - end - - expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}") - expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}") - expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}") - - expect_to_execute_concurrently_in_order(drop_index) - - subject.execute - - check_index_exists - end - - context 'when a dangling index is left from a previous run' do - before do - connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})") - end - - it 'replaces the existing index with an identical index' do - expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield - - expect_to_execute_concurrently_in_order(drop_index) - expect_to_execute_concurrently_in_order(create_index) - - expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| - expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield - end - - expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}") - expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}") - expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}") - - expect_to_execute_concurrently_in_order(drop_index) - - subject.execute - - check_index_exists - end - end - - context 'when it fails to create the replacement index' do - it 'safely cleans up and signals the error' do - expect_to_execute_concurrently_in_order(drop_index) - - expect(connection).to receive(:execute).with(create_index).ordered - .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') - - expect_to_execute_concurrently_in_order(drop_index) - - expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/) - - check_index_exists - end - end - - context 'when the replacement index is not valid' do - it 'safely cleans up and signals the error' do - expect_to_execute_concurrently_in_order(drop_index) - expect_to_execute_concurrently_in_order(create_index) - - expect(subject).to receive(:replacement_index_valid?).and_return(false) - - expect_to_execute_concurrently_in_order(drop_index) - - expect { subject.execute }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/) - - check_index_exists - end - end - - context 'when a database error occurs while swapping the indexes' do - it 'safely cleans up and signals the error' do - expect_to_execute_concurrently_in_order(drop_index) - expect_to_execute_concurrently_in_order(create_index) - - expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| - expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield - end - - expect(connection).to receive(:execute).ordered - .with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}") - .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') - - expect_to_execute_concurrently_in_order(drop_index) - - expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/) - - check_index_exists - end - end - - context 'when with_lock_retries fails to acquire the lock' do - it 'safely cleans up and signals the error' do - expect_to_execute_concurrently_in_order(drop_index) - expect_to_execute_concurrently_in_order(create_index) - - expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| - expect(instance).to receive(:run).with(raise_on_exhaustion: true) - .and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted') - end - - expect_to_execute_concurrently_in_order(drop_index) - - expect { subject.execute }.to raise_error(described_class::ReindexError, /exhausted/) - - check_index_exists - end - end - end - - def expect_to_execute_concurrently_in_order(sql) - # Indexes cannot be created CONCURRENTLY in a transaction. Since the tests are wrapped in transactions, - # verify the original call but pass through the non-concurrent form. - expect(connection).to receive(:execute).with(sql).ordered.and_wrap_original do |method, sql| - method.call(sql.sub(/CONCURRENTLY/, '')) - end - end - - def expect_to_execute_in_order(sql) - expect(connection).to receive(:execute).with(sql).ordered.and_call_original - end - - def find_index_create_statement - ActiveRecord::Base.connection.select_value(<<~SQL) - SELECT indexdef - FROM pg_indexes - WHERE schemaname = 'public' - AND indexname = #{ActiveRecord::Base.connection.quote(index_name)} - SQL - end - - def check_index_exists - expect(find_index_create_statement).to eq(original_index) - end -end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 0bdcca630aa..a8edcc5f7e5 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -699,6 +699,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:copy_indexes).with(:users, :old, :new) expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) + expect(model).to receive(:copy_check_constraints).with(:users, :old, :new) model.rename_column_concurrently(:users, :old, :new) end @@ -761,6 +762,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:change_column_default) .with(:users, :new, old_column.default) + expect(model).to receive(:copy_check_constraints) + .with(:users, :old, :new) + model.rename_column_concurrently(:users, :old, :new) end end @@ -856,6 +860,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:copy_indexes).with(:users, :new, :old) expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old) + expect(model).to receive(:copy_check_constraints).with(:users, :new, :old) model.undo_cleanup_concurrent_column_rename(:users, :old, :new) end @@ -894,6 +899,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:change_column_default) .with(:users, :old, new_column.default) + expect(model).to receive(:copy_check_constraints) + .with(:users, :new, :old) + model.undo_cleanup_concurrent_column_rename(:users, :old, :new) end end @@ -925,6 +933,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#undo_change_column_type_concurrently' do + it 'reverses the operations of change_column_type_concurrently' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:remove_rename_triggers_for_postgresql) + .with(:users, /trigger_.{12}/) + + expect(model).to receive(:remove_column).with(:users, "old_for_type_change") + + model.undo_change_column_type_concurrently(:users, :old) + end + end + describe '#cleanup_concurrent_column_type_change' do it 'cleans up the type changing procedure' do expect(model).to receive(:cleanup_concurrent_column_rename) @@ -937,6 +958,94 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#undo_cleanup_concurrent_column_type_change' do + context 'in a transaction' do + it 'raises RuntimeError' do + allow(model).to receive(:transaction_open?).and_return(true) + + expect { model.undo_cleanup_concurrent_column_type_change(:users, :old, :new) } + .to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + let(:temp_column) { "old_for_type_change" } + + let(:temp_undo_cleanup_column) do + identifier = "users_old_for_type_change" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "tmp_undo_cleanup_column_#{hashed_identifier}" + end + + let(:trigger_name) { model.rename_trigger_name(:users, :old, :old_for_type_change) } + + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + it 'reverses the operations of cleanup_concurrent_column_type_change' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:create_column_from).with( + :users, + :old, + temp_undo_cleanup_column, + type: :string, + batch_column_name: :id, + type_cast_function: nil + ).and_return(true) + + expect(model).to receive(:rename_column) + .with(:users, :old, temp_column) + + expect(model).to receive(:rename_column) + .with(:users, temp_undo_cleanup_column, :old) + + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + + model.undo_cleanup_concurrent_column_type_change(:users, :old, :string) + end + + it 'passes the type_cast_function and batch_column_name' do + expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:create_column_from).with( + :users, + :old, + temp_undo_cleanup_column, + type: :string, + batch_column_name: :other_batch_column, + type_cast_function: :custom_type_cast_function + ).and_return(true) + + expect(model).to receive(:rename_column) + .with(:users, :old, temp_column) + + expect(model).to receive(:rename_column) + .with(:users, temp_undo_cleanup_column, :old) + + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + + model.undo_cleanup_concurrent_column_type_change( + :users, + :old, + :string, + type_cast_function: :custom_type_cast_function, + batch_column_name: :other_batch_column + ) + end + + it 'raises an error with invalid batch_column_name' do + expect do + model.undo_cleanup_concurrent_column_type_change(:users, :old, :new, batch_column_name: :invalid) + end.to raise_error(RuntimeError, /Column invalid does not exist on users/) + end + end + end + describe '#install_rename_triggers_for_postgresql' do it 'installs the triggers for PostgreSQL' do expect(model).to receive(:execute) @@ -1128,7 +1237,65 @@ RSpec.describe Gitlab::Database::MigrationHelpers do name: 'index_on_issues_gl_project_id', length: [], order: [], - opclasses: { 'gl_project_id' => 'bar' }) + opclass: { 'gl_project_id' => 'bar' }) + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using an index with multiple columns and custom operator classes' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id foobar), + name: 'index_on_issues_project_id_foobar', + using: :gin, + where: nil, + opclasses: { 'project_id' => 'bar', 'foobar' => :gin_trgm_ops }, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) + + expect(model).to receive(:add_concurrent_index) + .with(:issues, + %w(gl_project_id foobar), + unique: false, + name: 'index_on_issues_gl_project_id_foobar', + length: [], + order: [], + opclass: { 'gl_project_id' => 'bar', 'foobar' => :gin_trgm_ops }, + using: :gin) + + model.copy_indexes(:issues, :project_id, :gl_project_id) + end + end + + context 'using an index with multiple columns and a custom operator class on the non affected column' do + it 'copies the index' do + index = double(:index, + columns: %w(project_id foobar), + name: 'index_on_issues_project_id_foobar', + using: :gin, + where: nil, + opclasses: { 'foobar' => :gin_trgm_ops }, + unique: false, + lengths: [], + orders: []) + + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) + + expect(model).to receive(:add_concurrent_index) + .with(:issues, + %w(gl_project_id foobar), + unique: false, + name: 'index_on_issues_gl_project_id_foobar', + length: [], + order: [], + opclass: { 'foobar' => :gin_trgm_ops }, + using: :gin) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1400,15 +1567,32 @@ RSpec.describe Gitlab::Database::MigrationHelpers do ) end - after do - 'DROP INDEX IF EXISTS test_index;' - end - it 'returns true if an index exists' do expect(model.index_exists_by_name?(:projects, 'test_index')) .to be_truthy end end + + context 'when an index exists for a table with the same name in another schema' do + before do + ActiveRecord::Base.connection.execute( + 'CREATE SCHEMA new_test_schema' + ) + + ActiveRecord::Base.connection.execute( + 'CREATE TABLE new_test_schema.projects (id integer, name character varying)' + ) + + ActiveRecord::Base.connection.execute( + 'CREATE INDEX test_index_on_name ON new_test_schema.projects (LOWER(name));' + ) + end + + it 'returns false if the index does not exist in the current schema' do + expect(model.index_exists_by_name?(:projects, 'test_index_on_name')) + .to be_falsy + end + end end describe '#create_or_update_plan_limit' do @@ -1863,11 +2047,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers do ActiveRecord::Base.connection.execute( 'ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID' ) - end - after do ActiveRecord::Base.connection.execute( - 'ALTER TABLE projects DROP CONSTRAINT IF EXISTS check_1' + 'CREATE SCHEMA new_test_schema' + ) + + ActiveRecord::Base.connection.execute( + 'CREATE TABLE new_test_schema.projects (id integer, name character varying)' + ) + + ActiveRecord::Base.connection.execute( + 'ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5)' ) end @@ -1885,6 +2075,11 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model.check_constraint_exists?(:users, 'check_1')) .to be_falsy end + + it 'returns false if a constraint with the same name exists for the same table in another schema' do + expect(model.check_constraint_exists?(:projects, 'check_2')) + .to be_falsy + end end describe '#add_check_constraint' do @@ -2086,6 +2281,138 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#copy_check_constraints' do + context 'inside a transaction' do + it 'raises an error' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + allow(model).to receive(:column_exists?).and_return(true) + end + + let(:old_column_constraints) do + [ + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'check_d7d49d475d', + 'constraint_def' => 'CHECK ((old_column IS NOT NULL))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'check_48560e521e', + 'constraint_def' => 'CHECK ((char_length(old_column) <= 255))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'custom_check_constraint', + 'constraint_def' => 'CHECK (((old_column IS NOT NULL) AND (another_column IS NULL)))' + }, + { + 'schema_name' => 'public', + 'table_name' => 'test_table', + 'column_name' => 'old_column', + 'constraint_name' => 'not_valid_check_constraint', + 'constraint_def' => 'CHECK ((old_column IS NOT NULL)) NOT VALID' + } + ] + end + + it 'copies check constraints from one column to another' do + allow(model).to receive(:check_constraints_for) + .with(:test_table, :old_column, schema: nil) + .and_return(old_column_constraints) + + allow(model).to receive(:not_null_constraint_name).with(:test_table, :new_column) + .and_return('check_1') + + allow(model).to receive(:text_limit_name).with(:test_table, :new_column) + .and_return('check_2') + + allow(model).to receive(:check_constraint_name) + .with(:test_table, :new_column, 'copy_check_constraint') + .and_return('check_3') + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(new_column IS NOT NULL)', + 'check_1', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(char_length(new_column) <= 255)', + 'check_2', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '((new_column IS NOT NULL) AND (another_column IS NULL))', + 'check_3', + validate: true + ).once + + expect(model).to receive(:add_check_constraint) + .with( + :test_table, + '(new_column IS NOT NULL)', + 'check_1', + validate: false + ).once + + model.copy_check_constraints(:test_table, :old_column, :new_column) + end + + it 'does nothing if there are no constraints defined for the old column' do + allow(model).to receive(:check_constraints_for) + .with(:test_table, :old_column, schema: nil) + .and_return([]) + + expect(model).not_to receive(:add_check_constraint) + + model.copy_check_constraints(:test_table, :old_column, :new_column) + end + + it 'raises an error when the orginating column does not exist' do + allow(model).to receive(:column_exists?).with(:test_table, :old_column).and_return(false) + + error_message = /Column old_column does not exist on test_table/ + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError, error_message) + end + + it 'raises an error when the target column does not exist' do + allow(model).to receive(:column_exists?).with(:test_table, :new_column).and_return(false) + + error_message = /Column new_column does not exist on test_table/ + + expect do + model.copy_check_constraints(:test_table, :old_column, :new_column) + end.to raise_error(RuntimeError, error_message) + end + end + end + describe '#add_text_limit' do context 'when it is called with the default options' do it 'calls add_check_constraint with an infered constraint name and validate: true' do diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb index 034bf966db7..8a35d8149ad 100644 --- a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb +++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Gitlab::Database::ObsoleteIgnoredColumns do describe '#execute' do it 'returns a list of class names and columns pairs' do - Timecop.freeze(REMOVE_DATE) do + travel_to(REMOVE_DATE) do expect(subject.execute).to eq([ ['Testing::A', { 'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0'), diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb index 334cac653cf..885eef5723e 100644 --- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb +++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do let(:partitioning_key) { :created_at } around do |example| - Timecop.freeze(Date.parse('2020-08-22')) { example.run } + travel_to(Date.parse('2020-08-22')) { example.run } end context 'with existing partitions' do diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb index ec3d0a6dbcb..c43b51e10a0 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb @@ -116,23 +116,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition expect(jobs_updated).to eq(1) end - context 'when the feature flag is disabled' do - let(:mock_connection) { double('connection') } - - before do - allow(subject).to receive(:connection).and_return(mock_connection) - stub_feature_flags(backfill_partitioned_audit_events: false) - end - - it 'exits without attempting to copy data' do - expect(mock_connection).not_to receive(:execute) - - subject.perform(1, 100, source_table, destination_table, unique_key) - - expect(destination_model.count).to eq(0) - end - end - context 'when the job is run within an explicit transaction block' do let(:mock_connection) { double('connection') } diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 44ef0b307fe..147637cf471 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -213,7 +213,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'creates partitions including the next month from today' do today = Date.new(2020, 5, 8) - Timecop.freeze(today) do + travel_to(today) do migration.partition_table_by_date source_table, partition_column, min_date: min_date expect_range_partitions_for(partitioned_table, { @@ -233,7 +233,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'without min_date, max_date' do it 'creates partitions for the current and next month' do current_date = Date.new(2020, 05, 22) - Timecop.freeze(current_date.to_time) do + travel_to(current_date.to_time) do migration.partition_table_by_date source_table, partition_column expect_range_partitions_for(partitioned_table, { @@ -514,6 +514,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true) allow(migration).to receive(:copy_missed_records) allow(migration).to receive(:execute).with(/VACUUM/) + allow(migration).to receive(:execute).with(/^(RE)?SET/) end it 'finishes remaining jobs for the correct table' do @@ -567,6 +568,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe allow(Gitlab::BackgroundMigration).to receive(:steal) allow(migration).to receive(:execute).with(/VACUUM/) + allow(migration).to receive(:execute).with(/^(RE)?SET/) end it 'idempotently cleans up after failed background migrations' do diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb new file mode 100644 index 00000000000..1da67a5a6c0 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_index_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresIndex do + before do + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE INDEX foo_idx ON public.users (name); + CREATE UNIQUE INDEX bar_key ON public.users (id); + + CREATE TABLE example_table (id serial primary key); + SQL + end + + def find(name) + described_class.by_identifier(name) + end + + describe '.by_identifier' do + it 'finds the index' do + expect(find('public.foo_idx')).to be_a(Gitlab::Database::PostgresIndex) + end + + it 'raises an error if not found' do + expect { find('public.idontexist') }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ArgumentError if given a non-fully qualified index name' do + expect { find('foo') }.to raise_error(ArgumentError, /not fully qualified/) + end + end + + describe '.regular' do + it 'only non-unique indexes' do + expect(described_class.regular).to all(have_attributes(unique: false)) + end + + it 'only non partitioned indexes ' do + expect(described_class.regular).to all(have_attributes(partitioned: false)) + end + + it 'only indexes that dont serve an exclusion constraint' do + expect(described_class.regular).to all(have_attributes(exclusion: false)) + end + end + + describe '.not_match' do + it 'excludes indexes matching the given regex' do + expect(described_class.not_match('^bar_k').map(&:name)).to all(match(/^(?!bar_k).*/)) + end + + it 'matches indexes without this prefix regex' do + expect(described_class.not_match('^bar_k')).not_to be_empty + end + end + + describe '.random_few' do + it 'limits to two records by default' do + expect(described_class.random_few(2).size).to eq(2) + end + end + + describe '#unique?' do + it 'returns true for a unique index' do + expect(find('public.bar_key')).to be_unique + end + + it 'returns false for a regular, non-unique index' do + expect(find('public.foo_idx')).not_to be_unique + end + + it 'returns true for a primary key index' do + expect(find('public.example_table_pkey')).to be_unique + end + end + + describe '#valid_index?' do + it 'returns true if the index is invalid' do + expect(find('public.foo_idx')).to be_valid_index + end + + it 'returns false if the index is marked as invalid' do + ActiveRecord::Base.connection.execute(<<~SQL) + UPDATE pg_index SET indisvalid=false + FROM pg_class + WHERE pg_class.relname = 'foo_idx' AND pg_index.indexrelid = pg_class.oid + SQL + + expect(find('public.foo_idx')).not_to be_valid_index + end + end + + describe '#to_s' do + it 'returns the index name' do + expect(find('public.foo_idx').to_s).to eq('foo_idx') + end + end + + describe '#name' do + it 'returns the name' do + expect(find('public.foo_idx').name).to eq('foo_idx') + end + end + + describe '#schema' do + it 'returns the index schema' do + expect(find('public.foo_idx').schema).to eq('public') + end + end + + describe '#definition' do + it 'returns the index definition' do + expect(find('public.foo_idx').definition).to eq('CREATE INDEX foo_idx ON public.users USING btree (name)') + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb new file mode 100644 index 00000000000..2d6765aac2e --- /dev/null +++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do + subject { described_class.new(index, logger: logger) } + + let(:table_name) { '_test_reindex_table' } + let(:column_name) { '_test_column' } + let(:index_name) { '_test_reindex_index' } + let(:index) { instance_double(Gitlab::Database::PostgresIndex, indexrelid: 42, name: index_name, schema: 'public', partitioned?: false, unique?: false, exclusion?: false, definition: 'CREATE INDEX _test_reindex_index ON public._test_reindex_table USING btree (_test_column)') } + let(:logger) { double('logger', debug: nil, info: nil, error: nil ) } + let(:connection) { ActiveRecord::Base.connection } + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id serial NOT NULL PRIMARY KEY, + #{column_name} integer NOT NULL); + + CREATE INDEX #{index.name} ON #{table_name} (#{column_name}); + SQL + end + + context 'when the index is unique' do + before do + allow(index).to receive(:unique?).and_return(true) + end + + it 'raises an error' do + expect do + subject.perform + end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/) + end + end + + context 'when the index is partitioned' do + before do + allow(index).to receive(:partitioned?).and_return(true) + end + + it 'raises an error' do + expect do + subject.perform + end.to raise_error(described_class::ReindexError, /partitioned indexes are currently not supported/) + end + end + + context 'when the index serves an exclusion constraint' do + before do + allow(index).to receive(:exclusion?).and_return(true) + end + + it 'raises an error' do + expect do + subject.perform + end.to raise_error(described_class::ReindexError, /indexes serving an exclusion constraint are currently not supported/) + end + end + + context 'when the index is a lingering temporary index from a previous reindexing run' do + context 'with the temporary index prefix' do + let(:index_name) { 'tmp_reindex_something' } + + it 'raises an error' do + expect do + subject.perform + end.to raise_error(described_class::ReindexError, /left-over temporary index/) + end + end + + context 'with the replaced index prefix' do + let(:index_name) { 'old_reindex_something' } + + it 'raises an error' do + expect do + subject.perform + end.to raise_error(described_class::ReindexError, /left-over temporary index/) + end + end + end + + context 'replacing the original index with a rebuilt copy' do + let(:replacement_name) { 'tmp_reindex_42' } + let(:replaced_name) { 'old_reindex_42' } + + let(:create_index) { "CREATE INDEX CONCURRENTLY #{replacement_name} ON public.#{table_name} USING btree (#{column_name})" } + let(:drop_index) do + <<~SQL + DROP INDEX CONCURRENTLY + IF EXISTS "public"."#{replacement_name}" + SQL + end + + let!(:original_index) { find_index_create_statement } + + it 'integration test: executing full index replacement without mocks' do + allow(connection).to receive(:execute).and_wrap_original do |method, sql| + method.call(sql.sub(/CONCURRENTLY/, '')) + end + + subject.perform + + check_index_exists + end + + context 'mocked specs' do + before do + allow(subject).to receive(:connection).and_return(connection) + allow(connection).to receive(:execute).and_call_original + end + + it 'replaces the existing index with an identical index' do + expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice + + expect_to_execute_concurrently_in_order(create_index) + + expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| + expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield + end + + expect_index_rename(index.name, replaced_name) + expect_index_rename(replacement_name, index.name) + expect_index_rename(replaced_name, replacement_name) + + expect_to_execute_concurrently_in_order(drop_index) + + subject.perform + + check_index_exists + end + + context 'when a dangling index is left from a previous run' do + before do + connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})") + end + + it 'replaces the existing index with an identical index' do + expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').exactly(3).times + + expect_to_execute_concurrently_in_order(drop_index) + expect_to_execute_concurrently_in_order(create_index) + + expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| + expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield + end + + expect_index_rename(index.name, replaced_name) + expect_index_rename(replacement_name, index.name) + expect_index_rename(replaced_name, replacement_name) + + expect_to_execute_concurrently_in_order(drop_index) + + subject.perform + + check_index_exists + end + end + + context 'when it fails to create the replacement index' do + it 'safely cleans up and signals the error' do + expect(connection).to receive(:execute).with(create_index).ordered + .and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') + + expect_to_execute_concurrently_in_order(drop_index) + + expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/) + + check_index_exists + end + end + + context 'when the replacement index is not valid' do + it 'safely cleans up and signals the error' do + replacement_index = double('replacement index', valid_index?: false) + allow(Gitlab::Database::PostgresIndex).to receive(:find_by).with(schema: 'public', name: replacement_name).and_return(nil, replacement_index) + + expect_to_execute_concurrently_in_order(create_index) + + expect_to_execute_concurrently_in_order(drop_index) + + expect { subject.perform }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/) + + check_index_exists + end + end + + context 'when a database error occurs while swapping the indexes' do + it 'safely cleans up and signals the error' do + replacement_index = double('replacement index', valid_index?: true) + allow(Gitlab::Database::PostgresIndex).to receive(:find_by).with(schema: 'public', name: replacement_name).and_return(nil, replacement_index) + + expect_to_execute_concurrently_in_order(create_index) + + expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| + expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield + end + + expect_index_rename(index.name, replaced_name).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') + + expect_to_execute_concurrently_in_order(drop_index) + + expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/) + + check_index_exists + end + end + + context 'when with_lock_retries fails to acquire the lock' do + it 'safely cleans up and signals the error' do + expect_to_execute_concurrently_in_order(create_index) + + expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| + expect(instance).to receive(:run).with(raise_on_exhaustion: true) + .and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted') + end + + expect_to_execute_concurrently_in_order(drop_index) + + expect { subject.perform }.to raise_error(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, /exhausted/) + + check_index_exists + end + end + end + end + + def expect_to_execute_concurrently_in_order(sql) + # Indexes cannot be created CONCURRENTLY in a transaction. Since the tests are wrapped in transactions, + # verify the original call but pass through the non-concurrent form. + expect(connection).to receive(:execute).with(sql).ordered.and_wrap_original do |method, sql| + method.call(sql.sub(/CONCURRENTLY/, '')) + end + end + + def expect_index_rename(from, to) + expect(connection).to receive(:execute).with(<<~SQL).ordered + ALTER INDEX "public"."#{from}" + RENAME TO "#{to}" + SQL + end + + def find_index_create_statement + ActiveRecord::Base.connection.select_value(<<~SQL) + SELECT indexdef + FROM pg_indexes + WHERE schemaname = 'public' + AND indexname = #{ActiveRecord::Base.connection.quote(index.name)} + SQL + end + + def check_index_exists + expect(find_index_create_statement).to eq(original_index) + end +end diff --git a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb new file mode 100644 index 00000000000..f45d959c0de --- /dev/null +++ b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing::Coordinator do + include ExclusiveLeaseHelpers + + describe '.perform' do + subject { described_class.new(indexes).perform } + + let(:indexes) { [instance_double(Gitlab::Database::PostgresIndex), instance_double(Gitlab::Database::PostgresIndex)] } + let(:reindexers) { [instance_double(Gitlab::Database::Reindexing::ConcurrentReindex), instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)] } + + let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) } + let(:lease_key) { 'gitlab/database/reindexing/coordinator' } + let(:lease_timeout) { 1.day } + let(:uuid) { 'uuid' } + + before do + allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield + + indexes.zip(reindexers).each do |index, reindexer| + allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer) + allow(reindexer).to receive(:perform) + end + end + + it 'performs concurrent reindexing for each index' do + indexes.zip(reindexers).each do |index, reindexer| + expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer) + expect(reindexer).to receive(:perform) + end + + subject + end + + it 'keeps track of actions and creates ReindexAction records' do + indexes.each do |index| + expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield + end + + subject + end + + context 'locking' do + it 'acquires a lock while reindexing' do + indexes.each do |index| + expect(lease).to receive(:try_obtain).ordered.and_return(uuid) + action = instance_double(Gitlab::Database::Reindexing::ConcurrentReindex) + expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).ordered.with(index).and_return(action) + expect(action).to receive(:perform).ordered + expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid) + end + + subject + end + + it 'does does not perform reindexing actions if lease is not granted' do + indexes.each do |index| + expect(lease).to receive(:try_obtain).ordered.and_return(false) + expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new) + end + + subject + end + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb new file mode 100644 index 00000000000..efb5b8463a1 --- /dev/null +++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do + let(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil) } + let(:size_after) { 512 } + + it 'yields to the caller' do + expect { |b| described_class.keep_track_of(index, &b) }.to yield_control + end + + def find_record + described_class.find_by(index_identifier: index.identifier) + end + + it 'creates the record with a start time and updates its end time' do + freeze_time do + described_class.keep_track_of(index) do + expect(find_record.action_start).to be_within(1.second).of(Time.zone.now) + + travel(10.seconds) + end + + duration = find_record.action_end - find_record.action_start + + expect(duration).to be_within(1.second).of(10.seconds) + end + end + + it 'creates the record with its status set to :started and updates its state to :finished' do + described_class.keep_track_of(index) do + expect(find_record).to be_started + end + + expect(find_record).to be_finished + end + + it 'creates the record with the indexes start size and updates its end size' do + described_class.keep_track_of(index) do + expect(find_record.ondisk_size_bytes_start).to eq(index.ondisk_size_bytes) + + expect(index).to receive(:reload).once + allow(index).to receive(:ondisk_size_bytes).and_return(size_after) + end + + expect(find_record.ondisk_size_bytes_end).to eq(size_after) + end + + context 'in case of errors' do + it 'sets the state to failed' do + expect do + described_class.keep_track_of(index) do + raise 'something went wrong' + end + end.to raise_error(/something went wrong/) + + expect(find_record).to be_failed + end + + it 'records the end time' do + freeze_time do + expect do + described_class.keep_track_of(index) do + raise 'something went wrong' + end + end.to raise_error(/something went wrong/) + + expect(find_record.action_end).to be_within(1.second).of(Time.zone.now) + end + end + + it 'records the resulting index size' do + expect(index).to receive(:reload).once + allow(index).to receive(:ondisk_size_bytes).and_return(size_after) + + expect do + described_class.keep_track_of(index) do + raise 'something went wrong' + end + end.to raise_error(/something went wrong/) + + expect(find_record.ondisk_size_bytes_end).to eq(size_after) + end + end +end diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb new file mode 100644 index 00000000000..86b3c029944 --- /dev/null +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing do + include ExclusiveLeaseHelpers + + describe '.perform' do + subject { described_class.perform(indexes) } + + let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) } + let(:indexes) { double } + + it 'delegates to Coordinator' do + expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator) + expect(coordinator).to receive(:perform) + + subject + end + end + + describe '.candidate_indexes' do + subject { described_class.candidate_indexes } + + it 'retrieves regular indexes that are no left-overs from previous runs' do + result = double + expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.not_match.not_match').with(no_args).with('^tmp_reindex_').with('^old_reindex_').and_return(result) + + expect(subject).to eq(result) + end + end +end diff --git a/spec/lib/gitlab/database/similarity_score_spec.rb b/spec/lib/gitlab/database/similarity_score_spec.rb index e36a4f610e1..cf75e5a72d9 100644 --- a/spec/lib/gitlab/database/similarity_score_spec.rb +++ b/spec/lib/gitlab/database/similarity_score_spec.rb @@ -90,4 +90,15 @@ RSpec.describe Gitlab::Database::SimilarityScore do expect(subject).to eq(%w[different same gitlab-danger]) end end + + describe 'annotation' do + it 'annotates the generated SQL expression' do + expression = Gitlab::Database::SimilarityScore.build_expression(search: 'test', rules: [ + { column: Arel.sql('path'), multiplier: 1 }, + { column: Arel.sql('name'), multiplier: 0.8 } + ]) + + expect(Gitlab::Database::SimilarityScore).to be_order_by_similarity(expression) + end + end end diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 2cc6e175500..220ae705e71 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -104,9 +104,69 @@ RSpec.describe Gitlab::Database::WithLockRetries do end context 'after 3 iterations' do - let(:retry_count) { 4 } + it_behaves_like 'retriable exclusive lock on `projects`' do + let(:retry_count) { 4 } + end + + context 'setting the idle transaction timeout' do + context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do + it 'does not disable the idle transaction timeout' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout) + allow(subject).to receive(:run_block_with_transaction).once + + expect(subject).not_to receive(:disable_idle_in_transaction_timeout) + + subject.run {} + end + end - it_behaves_like 'retriable exclusive lock on `projects`' + context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do + it 'disables the idle transaction timeout so the code can sleep and retry' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true) + + n = 0 + allow(subject).to receive(:run_block_with_transaction).twice do + n += 1 + raise(ActiveRecord::LockWaitTimeout) if n == 1 + end + + expect(subject).to receive(:disable_idle_in_transaction_timeout).once + + subject.run {} + end + end + end + end + + context 'after the retries are exhausted' do + let(:timing_configuration) do + [ + [1.second, 1.second] + ] + end + + context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do + it 'does not disable the lock_timeout' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout) + + expect(subject).not_to receive(:disable_lock_timeout) + + subject.run {} + end + end + + context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do + it 'disables the lock_timeout' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true) + allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout) + + expect(subject).to receive(:disable_lock_timeout) + + subject.run {} + end + end end context 'after the retries, without setting lock_timeout' do diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 420aa0a8df6..3175040167b 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -39,6 +39,12 @@ RSpec.describe Gitlab::Database do end end + describe '.system_id' do + it 'returns the PostgreSQL system identifier' do + expect(described_class.system_id).to be_an_instance_of(Integer) + end + end + describe '.postgresql?' do subject { described_class.postgresql? } @@ -70,25 +76,6 @@ RSpec.describe Gitlab::Database do end end - describe '.postgresql_9_or_less?' do - it 'returns true when using postgresql 8.4' do - allow(described_class).to receive(:version).and_return('8.4') - expect(described_class.postgresql_9_or_less?).to eq(true) - end - - it 'returns true when using PostgreSQL 9.6' do - allow(described_class).to receive(:version).and_return('9.6') - - expect(described_class.postgresql_9_or_less?).to eq(true) - end - - it 'returns false when using PostgreSQL 10 or newer' do - allow(described_class).to receive(:version).and_return('10') - - expect(described_class.postgresql_9_or_less?).to eq(false) - end - end - describe '.postgresql_minimum_supported_version?' do it 'returns false when using PostgreSQL 10' do allow(described_class).to receive(:version).and_return('10') @@ -150,68 +137,6 @@ RSpec.describe Gitlab::Database do end end - describe '.pg_wal_lsn_diff' do - it 'returns old name when using PostgreSQL 9.6' do - allow(described_class).to receive(:version).and_return('9.6') - - expect(described_class.pg_wal_lsn_diff).to eq('pg_xlog_location_diff') - end - - it 'returns new name when using PostgreSQL 10 or newer' do - allow(described_class).to receive(:version).and_return('10') - - expect(described_class.pg_wal_lsn_diff).to eq('pg_wal_lsn_diff') - end - end - - describe '.pg_current_wal_insert_lsn' do - it 'returns old name when using PostgreSQL 9.6' do - allow(described_class).to receive(:version).and_return('9.6') - - expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_xlog_insert_location') - end - - it 'returns new name when using PostgreSQL 10 or newer' do - allow(described_class).to receive(:version).and_return('10') - - expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_wal_insert_lsn') - end - end - - describe '.pg_last_wal_receive_lsn' do - it 'returns old name when using PostgreSQL 9.6' do - allow(described_class).to receive(:version).and_return('9.6') - - expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_xlog_receive_location') - end - - it 'returns new name when using PostgreSQL 10 or newer' do - allow(described_class).to receive(:version).and_return('10') - - expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_wal_receive_lsn') - end - end - - describe '.pg_last_wal_replay_lsn' do - it 'returns old name when using PostgreSQL 9.6' do - allow(described_class).to receive(:version).and_return('9.6') - - expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_xlog_replay_location') - end - - it 'returns new name when using PostgreSQL 10 or newer' do - allow(described_class).to receive(:version).and_return('10') - - expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_wal_replay_lsn') - end - end - - describe '.pg_last_xact_replay_timestamp' do - it 'returns pg_last_xact_replay_timestamp' do - expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp') - end - end - describe '.nulls_last_order' do it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} @@ -433,6 +358,20 @@ RSpec.describe Gitlab::Database do end end + describe '.get_write_location' do + it 'returns a string' do + connection = ActiveRecord::Base.connection + + expect(described_class.get_write_location(connection)).to be_a(String) + end + + it 'returns nil if there are no results' do + connection = double(select_all: []) + + expect(described_class.get_write_location(connection)).to be_nil + end + end + describe '#true_value' do it 'returns correct value' do expect(described_class.true_value).to eq "'t'" diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb index bd60c24859c..72a66b0451e 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb @@ -120,7 +120,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do described_class.new(merge_request.merge_request_diff, batch_page, batch_size, - collection_default_args) + **collection_default_args) end end diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index 7e926f86096..f6810d7a966 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -43,7 +43,8 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do describe '#decorate' do # Manually creates a Diff::File object to avoid triggering the cache on - # the FileCollection::MergeRequestDiff + # the FileCollection::MergeRequestDiff + # let(:diff_file) do diffs = merge_request.diffs raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first @@ -73,6 +74,37 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do expect(rich_texts).to all(be_html_safe) end + + context "when diff_file is uncached due to default_max_patch_bytes change" do + before do + expect(cache).to receive(:read_file).at_least(:once).and_return([]) + + # Stub out the application's default and current patch size limits. We + # want them to be different, and the diff file to be sized between + # the 2 values. + # + diff_file_size_kb = (diff_file.diff.diff.bytesize * 10) + + stub_const("#{diff_file.diff.class}::DEFAULT_MAX_PATCH_BYTES", diff_file_size_kb - 1 ) + expect(diff_file.diff.class).to receive(:patch_safe_limit_bytes).and_return(diff_file_size_kb + 1) + expect(diff_file.diff.class) + .to receive(:patch_safe_limit_bytes) + .with(diff_file.diff.class::DEFAULT_MAX_PATCH_BYTES) + .and_call_original + end + + it "manually writes highlighted lines to the cache" do + expect(cache).to receive(:write_to_redis_hash).and_call_original + + cache.decorate(diff_file) + end + + it "assigns highlighted diff lines to the DiffFile" do + expect(diff_file.highlighted_diff_lines.size).to be > 5 + + cache.decorate(diff_file) + end + end end shared_examples 'caches missing entries' do diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 07b8070be30..ef448ee96a4 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -65,24 +65,15 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end end - [true, false].each do |state_tracking_enabled| - context "and current user can update noteable #{state_tracking_enabled ? 'enabled' : 'disabled'}" do - before do - stub_feature_flags(track_resource_state_change_events: state_tracking_enabled) - - project.add_developer(user) - end + context "and current user can update noteable" do + before do + project.add_developer(user) + end - it 'does not raise an error' do - if state_tracking_enabled - expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1) - else - # One system note is created for the 'close' event - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - end + it 'does not raise an error' do + expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1) - expect(noteable.reload).to be_closed - end + expect(noteable.reload).to be_closed end end end diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb index 01e2fe8ce17..40669f06371 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -25,13 +25,17 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d let!(:lease) { stub_exclusive_lease(unique_key, 'uuid') } it 'calls the given block' do - expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false) + expect { |b| class_instance.in_lock(unique_key, &b) } + .to yield_with_args(false, an_instance_of(described_class::SleepingLock)) end it 'calls the given block continuously' do - expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false) - expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false) - expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false) + expect { |b| class_instance.in_lock(unique_key, &b) } + .to yield_with_args(false, an_instance_of(described_class::SleepingLock)) + expect { |b| class_instance.in_lock(unique_key, &b) } + .to yield_with_args(false, an_instance_of(described_class::SleepingLock)) + expect { |b| class_instance.in_lock(unique_key, &b) } + .to yield_with_args(false, an_instance_of(described_class::SleepingLock)) end it 'cancels the exclusive lease after the block' do @@ -74,7 +78,8 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d expect(lease).to receive(:try_obtain).exactly(3).times { nil } expect(lease).to receive(:try_obtain).once { unique_key } - expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(true) + expect { |b| class_instance.in_lock(unique_key, &b) } + .to yield_with_args(true, an_instance_of(described_class::SleepingLock)) end end end diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index 9bc865f4d29..e93593d348f 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Experimentation do +RSpec.describe Gitlab::Experimentation, :snowplow do before do stub_const('Gitlab::Experimentation::EXPERIMENTS', { test_experiment: { @@ -69,12 +69,26 @@ RSpec.describe Gitlab::Experimentation do end end + describe '#push_frontend_experiment' do + it 'pushes an experiment to the frontend' do + gon = instance_double('gon') + experiments = { experiments: { 'myExperiment' => true } } + + stub_experiment_for_user(my_experiment: true) + allow(controller).to receive(:gon).and_return(gon) + + expect(gon).to receive(:push).with(experiments, true) + + controller.push_frontend_experiment(:my_experiment) + end + end + describe '#experiment_enabled?' do subject { controller.experiment_enabled?(:test_experiment) } context 'cookie is not present' do - it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of nil' do - expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, nil) + it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of nil' do + expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, nil) controller.experiment_enabled?(:test_experiment) end end @@ -85,22 +99,22 @@ RSpec.describe Gitlab::Experimentation do get :index end - it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do + it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do # 'abcd1234'.hex % 100 = 76 - expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, 76) + expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, 76) controller.experiment_enabled?(:test_experiment) end end it 'returns true when DNT: 0 is set in the request' do - allow(Gitlab::Experimentation).to receive(:enabled_for_user?) { true } + allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true } controller.request.headers['DNT'] = '0' is_expected.to be_truthy end it 'returns false when DNT: 1 is set in the request' do - allow(Gitlab::Experimentation).to receive(:enabled_for_user?) { true } + allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true } controller.request.headers['DNT'] = '1' is_expected.to be_falsy @@ -127,13 +141,14 @@ RSpec.describe Gitlab::Experimentation do end it 'tracks the event with the right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Team', - 'start', + controller.track_experiment_event(:test_experiment, 'start', 1) + + expect_snowplow_event( + category: 'Team', + action: 'start', property: 'experimental_group', - value: 'team_id' + value: 1 ) - controller.track_experiment_event(:test_experiment, 'start', 'team_id') end end @@ -143,13 +158,43 @@ RSpec.describe Gitlab::Experimentation do end it 'tracks the event with the right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Team', - 'start', + controller.track_experiment_event(:test_experiment, 'start', 1) + + expect_snowplow_event( + category: 'Team', + action: 'start', + property: 'control_group', + value: 1 + ) + end + end + + context 'do not track is disabled' do + before do + request.headers['DNT'] = '0' + end + + it 'does track the event' do + controller.track_experiment_event(:test_experiment, 'start', 1) + + expect_snowplow_event( + category: 'Team', + action: 'start', property: 'control_group', - value: 'team_id' + value: 1 ) - controller.track_experiment_event(:test_experiment, 'start', 'team_id') + end + end + + context 'do not track enabled' do + before do + request.headers['DNT'] = '1' + end + + it 'does not track the event' do + controller.track_experiment_event(:test_experiment, 'start', 1) + + expect_no_snowplow_event end end end @@ -160,8 +205,9 @@ RSpec.describe Gitlab::Experimentation do end it 'does not track the event' do - expect(Gitlab::Tracking).not_to receive(:event) controller.track_experiment_event(:test_experiment, 'start') + + expect_no_snowplow_event end end end @@ -220,6 +266,36 @@ RSpec.describe Gitlab::Experimentation do ) end end + + context 'do not track disabled' do + before do + request.headers['DNT'] = '0' + end + + it 'pushes the right parameters to gon' do + controller.frontend_experimentation_tracking_data(:test_experiment, 'start') + + expect(Gon.tracking_data).to eq( + { + category: 'Team', + action: 'start', + property: 'control_group' + } + ) + end + end + + context 'do not track enabled' do + before do + request.headers['DNT'] = '1' + end + + it 'does not push data to gon' do + controller.frontend_experimentation_tracking_data(:test_experiment, 'start') + + expect(Gon.method_defined?(:tracking_data)).to be_falsey + end + end end context 'when the experiment is disabled' do @@ -294,6 +370,39 @@ RSpec.describe Gitlab::Experimentation do controller.record_experiment_user(:test_experiment) end end + + context 'do not track' do + before do + allow(controller).to receive(:current_user).and_return(user) + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false) + end + end + + context 'is disabled' do + before do + request.headers['DNT'] = '0' + end + + it 'calls add_user on the Experiment model' do + expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user) + + controller.record_experiment_user(:test_experiment) + end + end + + context 'is enabled' do + before do + request.headers['DNT'] = '1' + end + + it 'does not call add_user on the Experiment model' do + expect(::Experiment).not_to receive(:add_user) + + controller.record_experiment_user(:test_experiment) + end + end + end end describe '#experiment_tracking_category_and_group' do @@ -336,8 +445,8 @@ RSpec.describe Gitlab::Experimentation do end end - describe '.enabled_for_user?' do - subject { described_class.enabled_for_user?(:test_experiment, experimentation_subject_index) } + describe '.enabled_for_value?' do + subject { described_class.enabled_for_value?(:test_experiment, experimentation_subject_index) } let(:experimentation_subject_index) { 9 } @@ -377,4 +486,32 @@ RSpec.describe Gitlab::Experimentation do end end end + + describe '.enabled_for_attribute?' do + subject { described_class.enabled_for_attribute?(:test_experiment, attribute) } + + let(:attribute) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7 + + context 'experiment is disabled' do + before do + allow(described_class).to receive(:enabled?).and_return(false) + end + + it { is_expected.to be false } + end + + context 'experiment is enabled' do + before do + allow(described_class).to receive(:enabled?).and_return(true) + end + + it { is_expected.to be true } + + context 'outside enabled ratio' do + let(:attribute) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17 + + it { is_expected.to be false } + end + end + end end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index e1bcf4aeeb1..9271f635b14 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -85,9 +85,9 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do } end - let(:stale_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } - let(:active_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } - let(:future_sha) { Timecop.freeze(100.days.since) { create_commit } } + 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 } } before do repository.create_branch('stale-1', stale_sha) diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index b202015464f..1a3c332a21b 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -9,8 +9,11 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do MutatingConstantIterator.class_eval do include Enumerable + attr_reader :size + def initialize(count, value) @count = count + @size = count @value = value end @@ -517,21 +520,39 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end - it 'prunes diffs that are quite big' do - diff = nil + context 'single-file collections' do + it 'does not prune diffs' do + diff = nil - subject.each do |d| - diff = d + subject.each do |d| + diff = d + end + + expect(diff.diff).not_to eq('') end + end + + context 'multi-file collections' do + let(:iterator) { [{ diff: 'b' }, { diff: 'a' * 20480 }]} + + it 'prunes diffs that are quite big' do + diff = nil - expect(diff.diff).to eq('') + subject.each do |d| + diff = d + end + + expect(diff.diff).to eq('') + end end context 'when go over safe limits on files' do let(:iterator) { [fake_diff(1, 1)] * 4 } before do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) + allow(Gitlab::Git::DiffCollection) + .to receive(:default_limits) + .and_return({ max_files: 2, max_lines: max_lines }) end it 'prunes diffs by default even little ones' do @@ -556,7 +577,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end before do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + allow(Gitlab::Git::DiffCollection) + .to receive(:default_limits) + .and_return({ max_files: max_files, max_lines: 80 }) end it 'prunes diffs by default even little ones' do @@ -581,7 +604,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end before do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) + allow(Gitlab::Git::DiffCollection) + .to receive(:default_limits) + .and_return({ max_files: max_files, max_lines: 80 }) end it 'prunes diffs by default even little ones' do @@ -665,8 +690,9 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end before do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', - { max_files: max_files, max_lines: 80 }) + allow(Gitlab::Git::DiffCollection) + .to receive(:default_limits) + .and_return({ max_files: max_files, max_lines: 80 }) end it 'considers size of diffs before the offset for prunning' do diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 117c519e98d..980a52bb61e 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -284,13 +284,21 @@ EOT end describe '#line_count' do - it 'returns the correct number of lines' do - diff = described_class.new(gitaly_diff) + let(:diff) { described_class.new(gitaly_diff) } + it 'returns the correct number of lines' do expect(diff.line_count).to eq(7) end end + describe "#diff_bytesize" do + let(:diff) { described_class.new(gitaly_diff) } + + it "returns the size of the diff in bytes" do + expect(diff.diff_bytesize).to eq(diff.diff.bytesize) + end + end + describe '#too_large?' do it 'returns true for a diff that is too large' do diff = described_class.new(diff: 'a' * 204800) diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index c8fbc674c73..e1873c6ddb5 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Git::ObjectPool do describe '#create' do before do - subject.create + subject.create # rubocop:disable Rails/SaveBang end context "when the pool doesn't exist yet" do @@ -31,7 +31,7 @@ RSpec.describe Gitlab::Git::ObjectPool do context 'when the pool already exists' do it 'raises an FailedPrecondition' do expect do - subject.create + subject.create # rubocop:disable Rails/SaveBang end.to raise_error(GRPC::FailedPrecondition) end end diff --git a/spec/lib/gitlab/git/remote_mirror_spec.rb b/spec/lib/gitlab/git/remote_mirror_spec.rb index 423c4aa9620..92504b7aafe 100644 --- a/spec/lib/gitlab/git/remote_mirror_spec.rb +++ b/spec/lib/gitlab/git/remote_mirror_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::Git::RemoteMirror do .to receive(:update_remote_mirror) .with(ref_name, ['master'], ssh_key: 'KEY', known_hosts: 'KNOWN HOSTS', keep_divergent_refs: true) - remote_mirror.update + remote_mirror.update # rubocop:disable Rails/SaveBang end it 'wraps gitaly errors' do @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Git::RemoteMirror do .to receive(:update_remote_mirror) .and_raise(StandardError) - expect { remote_mirror.update }.to raise_error(StandardError) + expect { remote_mirror.update }.to raise_error(StandardError) # rubocop:disable Rails/SaveBang end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 73eecd3401a..6dfa791f70b 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -120,7 +120,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:expected_extension) { 'tar.gz' } let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" } - let(:expected_path) { File.join(storage_path, cache_key, expected_filename) } + let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) } let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) } @@ -133,12 +133,32 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do expect(metadata['ArchivePrefix']).to eq(expected_prefix) end - it 'sets ArchivePath to the expected globally-unique path' do - # This is really important from a security perspective. Think carefully - # before changing it: https://gitlab.com/gitlab-org/gitlab-foss/issues/45689 - expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID)) + context 'when :include_lfs_blobs_in_archive feature flag is disabled' do + let(:expected_path) { File.join(storage_path, cache_key, expected_filename) } - expect(metadata['ArchivePath']).to eq(expected_path) + before do + stub_feature_flags(include_lfs_blobs_in_archive: false) + end + + it 'sets ArchivePath to the expected globally-unique path' do + # This is really important from a security perspective. Think carefully + # before changing it: https://gitlab.com/gitlab-org/gitlab-foss/issues/45689 + expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID)) + + expect(metadata['ArchivePath']).to eq(expected_path) + end + end + + context 'when :include_lfs_blobs_in_archive feature flag is enabled' do + before do + stub_feature_flags(include_lfs_blobs_in_archive: true) + end + + it 'sets ArchivePath to the expected globally-unique path' do + expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID)) + + expect(metadata['ArchivePath']).to eq(expected_path) + end end context 'path is set' do @@ -1630,13 +1650,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:right_branch) { 'test-master' } let(:first_parent_ref) { 'refs/heads/test-master' } let(:target_ref) { 'refs/merge-requests/999/merge' } + let(:allow_conflicts) { false } before do repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) end def merge_to_ref - repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref) + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref, allow_conflicts) end it 'generates a commit in the target_ref' do @@ -2079,7 +2100,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) } before do - object_pool.create + object_pool.create # rubocop:disable Rails/SaveBang end it 'does not raise an error when disconnecting a non-linked repository' do diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb index 4f6a3fb823e..16cea1dc1a3 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -7,7 +7,7 @@ require 'tempfile' RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:feature_flag_name) { 'feature-flag-name' } + let(:feature_flag_name) { wrapper.rugged_feature_keys.first } let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file } before(:all) do @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do end end - context 'when feature flag is not persisted' do + context 'when feature flag is not persisted', stub_feature_flags: false do context 'when running puma with multiple threads' do before do allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(true) diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index a88097705f6..36bff42d937 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -51,6 +51,11 @@ RSpec.describe Gitlab::Git::Wiki do expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1' expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' end + + it 'returns nil for invalid arguments' do + expect(subject.page(title: '')).to be_nil + expect(subject.page(title: 'foo', version: ':')).to be_nil + end end describe '#delete_page' do diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb index 3b8b5fd82c6..8c481cdee08 100644 --- a/spec/lib/gitlab/git_access_snippet_spec.rb +++ b/spec/lib/gitlab/git_access_snippet_spec.rb @@ -232,29 +232,6 @@ RSpec.describe Gitlab::GitAccessSnippet do end end - context 'when geo is enabled', if: Gitlab.ee? do - let(:user) { snippet.author } - let!(:primary_node) { FactoryBot.create(:geo_node, :primary) } - - before do - allow(::Gitlab::Database).to receive(:read_only?).and_return(true) - allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true) - end - - # Without override, push access would return Gitlab::GitAccessResult::CustomAction - it 'skips geo for snippet' do - expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/) - end - - context 'when user is migration bot' do - let(:user) { migration_bot } - - it 'skips geo for snippet' do - expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/) - end - end - end - context 'when changes are specific' do let(:changes) { "2d1db523e11e777e49377cfb22d368deec3f0793 ddd0f15ae83993f5cb66a927a28673882e99100b master" } let(:user) { snippet.author } @@ -283,7 +260,7 @@ RSpec.describe Gitlab::GitAccessSnippet do service = double expect(service).to receive(:validate!).and_return(nil) - expect(Snippet).to receive(:max_file_limit).with(user).and_return(5) + expect(Snippet).to receive(:max_file_limit).and_return(5) expect(Gitlab::Checks::PushFileCountCheck).to receive(:new).with(anything, hash_including(limit: 5)).and_return(service) push_access_check diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 85567ab2e55..21607edbc32 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -420,6 +420,13 @@ RSpec.describe Gitlab::GitAccess do expect { pull_access_check }.to raise_forbidden('Your account has been blocked.') end + it 'disallows users that are blocked pending approval to pull' do + project.add_maintainer(user) + user.block_pending_approval + + expect { pull_access_check }.to raise_forbidden('Your account is pending approval from your administrator and hence blocked.') + end + it 'disallows deactivated users to pull' do project.add_maintainer(user) user.deactivate! @@ -428,14 +435,12 @@ RSpec.describe Gitlab::GitAccess do end context 'when the project repository does not exist' do - it 'returns not found' do + before do project.add_guest(user) - repo = project.repository - Gitlab::GitalyClient::StorageSettings.allow_disk_access { FileUtils.rm_rf(repo.path) } - - # Sanity check for rm_rf - expect(repo.exists?).to eq(false) + allow(project.repository).to receive(:exists?).and_return(false) + end + it 'returns not found' do expect { pull_access_check }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.') end end @@ -917,6 +922,12 @@ RSpec.describe Gitlab::GitAccess do project.add_developer(user) end + it 'disallows users that are blocked pending approval to push' do + user.block_pending_approval + + expect { push_access_check }.to raise_forbidden('Your account is pending approval from your administrator and hence blocked.') + end + it 'does not allow deactivated users to push' do user.deactivate! diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 688089f4862..b78d99269d3 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' RSpec.describe Gitlab::GitAccessWiki do - let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } - let_it_be(:project) { create(:project, :wiki_repo) } let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :wiki_repo) } + let_it_be(:wiki) { create(:project_wiki, project: project) } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } + let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } - let(:authentication_abilities) do - [ - :read_project, - :download_code, - :push_code - ] + + let(:access) do + described_class.new(user, wiki, 'web', + authentication_abilities: authentication_abilities, + redirected_path: redirected_path) end describe '#push_access_check' do @@ -64,7 +64,7 @@ RSpec.describe Gitlab::GitAccessWiki do context 'when the repository does not exist' do before do - allow(project.wiki).to receive(:repository).and_return(double('Repository', exists?: false)) + allow(wiki.repository).to receive(:exists?).and_return(false) end it_behaves_like 'not-found git access' do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 9581b017839..f977fe1638f 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -13,6 +13,10 @@ RSpec.describe Gitlab::GitalyClient::CommitService do let(:client) { described_class.new(repository) } describe '#diff_from_parent' do + before do + stub_feature_flags(increased_diff_limits: false) + end + context 'when a commit has a parent' do it 'sends an RPC request with the parent ID as left commit' do request = Gitaly::CommitDiffRequest.new( diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index b974f456914..ce01566b870 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -88,9 +88,10 @@ RSpec.describe Gitlab::GitalyClient::OperationService do let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validación' } + let(:allow_conflicts) { false } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) } + subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref, allow_conflicts) } it 'sends a user_merge_to_ref message' do expect_any_instance_of(Gitaly::OperationService::Stub) diff --git a/spec/lib/gitlab/gitpod_spec.rb b/spec/lib/gitlab/gitpod_spec.rb index f4dda42aeb4..717e396f942 100644 --- a/spec/lib/gitlab/gitpod_spec.rb +++ b/spec/lib/gitlab/gitpod_spec.rb @@ -4,30 +4,29 @@ require 'spec_helper' RSpec.describe Gitlab::Gitpod do let_it_be(:user) { create(:user) } - let(:feature_scope) { true } before do stub_feature_flags(gitpod: feature_scope) end - describe '.feature_conditional?' do - subject { described_class.feature_conditional? } - - context 'when feature is enabled globally' do - it { is_expected.to be_falsey } - end + describe '.feature_available?' do + subject { described_class.feature_available? } - context 'when feature is enabled only to a resource' do - let(:feature_scope) { user } + context 'when feature has not been set' do + let(:feature_scope) { nil } it { is_expected.to be_truthy } end - end - describe '.feature_available?' do - subject { described_class.feature_available? } + context 'when feature is disabled' do + let(:feature_scope) { false } + + it { is_expected.to be_falsey } + end context 'when feature is enabled globally' do + let(:feature_scope) { true } + it { is_expected.to be_truthy } end @@ -43,7 +42,15 @@ RSpec.describe Gitlab::Gitpod do subject { described_class.feature_enabled?(current_user) } + context 'when feature has not been set' do + let(:feature_scope) { nil } + + it { is_expected.to be_truthy } + end + context 'when feature is enabled globally' do + let(:feature_scope) { true } + it { is_expected.to be_truthy } end diff --git a/spec/lib/gitlab/gl_repository/identifier_spec.rb b/spec/lib/gitlab/gl_repository/identifier_spec.rb index e95aaaa6690..e0622e30e7a 100644 --- a/spec/lib/gitlab/gl_repository/identifier_spec.rb +++ b/spec/lib/gitlab/gl_repository/identifier_spec.rb @@ -35,14 +35,14 @@ RSpec.describe Gitlab::GlRepository::Identifier do it_behaves_like 'parsing gl_repository identifier' do let(:record_id) { project.id } let(:identifier) { "wiki-#{record_id}" } - let(:expected_container) { project } + let(:expected_container) { project.wiki } let(:expected_type) { Gitlab::GlRepository::WIKI } end it_behaves_like 'parsing gl_repository identifier' do let(:record_id) { project.id } let(:identifier) { "project-#{record_id}-wiki" } - let(:expected_container) { project } + let(:expected_container) { project.wiki } let(:expected_type) { Gitlab::GlRepository::WIKI } end end @@ -87,7 +87,8 @@ RSpec.describe Gitlab::GlRepository::Identifier do 'project-wibble-wiki', 'wiki-1-project', 'snippet', - 'project-1-wiki-bar' + 'project-1-wiki-bar', + 'project-1-project' ] end @@ -96,10 +97,5 @@ RSpec.describe Gitlab::GlRepository::Identifier do expect { described_class.parse(identifier) }.to raise_error(described_class::InvalidIdentifier) end end - - it 'raises InvalidIdentifier on project-1-project' do - pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/219192' - expect { described_class.parse('project-1-project') }.to raise_error(described_class::InvalidIdentifier) - end end end diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb index 3fa636a1cf0..629e6c96858 100644 --- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb +++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb @@ -41,12 +41,14 @@ RSpec.describe Gitlab::GlRepository::RepoType do end describe Gitlab::GlRepository::WIKI do + let(:wiki) { project.wiki } + it_behaves_like 'a repo type' do - let(:expected_id) { project.id } + let(:expected_id) { wiki.project.id } let(:expected_identifier) { "wiki-#{expected_id}" } let(:expected_suffix) { '.wiki' } - let(:expected_container) { project } - let(:expected_repository) { ::Repository.new(project.wiki.full_path, project, shard: project.wiki.repository_storage, disk_path: project.wiki.disk_path, repo_type: Gitlab::GlRepository::WIKI) } + let(:expected_container) { wiki } + let(:expected_repository) { ::Repository.new(wiki.full_path, wiki, shard: wiki.repository_storage, disk_path: wiki.disk_path, repo_type: Gitlab::GlRepository::WIKI) } end it 'knows its type' do diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb index 3733d545155..05914f92c01 100644 --- a/spec/lib/gitlab/gl_repository_spec.rb +++ b/spec/lib/gitlab/gl_repository_spec.rb @@ -12,7 +12,7 @@ RSpec.describe ::Gitlab::GlRepository do end it 'parses a project wiki gl_repository' do - expect(described_class.parse("wiki-#{project.id}")).to eq([project, project, Gitlab::GlRepository::WIKI]) + expect(described_class.parse("wiki-#{project.id}")).to eq([project.wiki, project, Gitlab::GlRepository::WIKI]) end it 'parses a snippet gl_repository' do diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index 95db6b2b4e0..3d3f381b6d2 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Gitlab::GonHelper do end describe '#push_frontend_feature_flag' do + before do + skip_feature_flags_yaml_validation + end + it 'pushes a feature flag to the frontend' do gon = instance_double('gon') thing = stub_feature_flag_gate('thing') diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb index e68c1446502..9538c4bae2b 100644 --- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do end it 'returns the correct duration in seconds' do - Timecop.freeze(start_time) do + travel_to(start_time) do subject.before expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f }) diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb index efe6c27c463..7576523ce52 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb @@ -19,24 +19,29 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do options.reverse_merge!(null: true) field :test_field, field_type, authorize: field_authorizations, - resolve: -> (_, _, _) { resolved_value }, **options + + define_method :test_field do + resolved_value + end end end - let(:current_user) { double(:current_user) } - subject(:service) { described_class.new(field) } describe '#authorized_resolve' do - let(:presented_object) { double('presented object') } - let(:presented_type) { double('parent type', object: presented_object) } - let(:query_type) { GraphQL::ObjectType.new } - let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} - let(:query_context) { OpenStruct.new(schema: schema) } - let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema, context: query_context), values: { current_user: current_user }, object: nil) } + let_it_be(:current_user) { build(:user) } + let_it_be(:presented_object) { 'presented object' } + let_it_be(:query_type) { GraphQL::ObjectType.new } + let_it_be(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} + let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) } + let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) } + + let(:type_class) { type_with_field(custom_type, :read_field, presented_object) } + let(:type_instance) { type_class.authorized_new(presented_object, context) } + let(:field) { type_class.fields['testField'].to_graphql } - subject(:resolved) { service.authorized_resolve.call(presented_type, {}, context) } + subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) } context 'scalar types' do shared_examples 'checking permissions on the presented object' do @@ -48,7 +53,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do expect(resolved).to eq('Resolved value') end - it "returns nil if the value wasn't authorized" do + it 'returns nil if the value was not authorized' do allow(Ability).to receive(:allowed?).and_return false expect(resolved).to be_nil @@ -56,28 +61,28 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do end context 'when the field is a built-in scalar type' do - let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields['testField'].to_graphql } + let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) } let(:expected_permissions) { [:read_field] } it_behaves_like 'checking permissions on the presented object' end context 'when the field is a list of scalar types' do - let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields['testField'].to_graphql } + let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) } let(:expected_permissions) { [:read_field] } it_behaves_like 'checking permissions on the presented object' end context 'when the field is sub-classed scalar type' do - let(:field) { type_with_field(Types::TimeType, :read_field).fields['testField'].to_graphql } + let(:type_class) { type_with_field(Types::TimeType, :read_field) } let(:expected_permissions) { [:read_field] } it_behaves_like 'checking permissions on the presented object' end context 'when the field is a list of sub-classed scalar types' do - let(:field) { type_with_field([Types::TimeType], :read_field).fields['testField'].to_graphql } + let(:type_class) { type_with_field([Types::TimeType], :read_field) } let(:expected_permissions) { [:read_field] } it_behaves_like 'checking permissions on the presented object' @@ -86,7 +91,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do context 'when the field is a connection' do context 'when it resolves to nil' do - let(:field) { type_with_field(Types::QueryType.connection_type, :read_field, nil).fields['testField'].to_graphql } + let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) } it 'does not fail when authorizing' do expect(resolved).to be_nil @@ -97,7 +102,11 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do context 'when the field is a specific type' do let(:custom_type) { type(:read_type) } let(:object_in_field) { double('presented in field') } - let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields['testField'].to_graphql } + + let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) } + let(:type_instance) { type_class.authorized_new(object_in_field, context) } + + subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) } it 'checks both field & type permissions' do spy_ability_check_for(:read_field, object_in_field, passed: true) @@ -114,7 +123,7 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do end context 'when the field is not nullable' do - let(:field) { type_with_field(custom_type, [], object_in_field, null: false).fields['testField'].to_graphql } + let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) } it 'returns nil when viewing is not allowed' do spy_ability_check_for(:read_type, object_in_field, passed: false) @@ -127,7 +136,9 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do let(:object_1) { double('presented in field 1') } let(:object_2) { double('presented in field 2') } let(:presented_types) { [double(object: object_1), double(object: object_2)] } - let(:field) { type_with_field([custom_type], :read_field, presented_types).fields['testField'].to_graphql } + + let(:type_class) { type_with_field([custom_type], :read_field, presented_types) } + let(:type_instance) { type_class.authorized_new(presented_types, context) } it 'checks all permissions' do allow(Ability).to receive(:allowed?) { true } diff --git a/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb b/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb deleted file mode 100644 index af604e1c7d5..00000000000 --- a/spec/lib/gitlab/graphql/markdown_field/resolver_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::MarkdownField::Resolver do - include Gitlab::Routing - let(:resolver) { described_class.new(:note) } - - describe '#proc' do - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } - let(:note) do - create(:note, - note: "Referencing #{issue.to_reference(full: true)}") - end - - it 'renders markdown correctly' do - expect(resolver.proc.call(note, {}, {})).to include(issue_path(issue)) - end - - context 'when the issue is not publicly accessible' do - let(:project) { create(:project, :private) } - - it 'hides the references from users that are not allowed to see the reference' do - expect(resolver.proc.call(note, {}, {})).not_to include(issue_path(issue)) - end - - it 'shows the reference to users that are allowed to see it' do - expect(resolver.proc.call(note, {}, { current_user: project.owner })) - .to include(issue_path(issue)) - end - end - end -end diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb index e3da925376e..82090f992eb 100644 --- a/spec/lib/gitlab/graphql/markdown_field_spec.rb +++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::MarkdownField do + include Gitlab::Routing + describe '.markdown_field' do it 'creates the field with some default attributes' do field = class_with_markdown_field(:test_html, null: true, method: :hello).fields['testHtml'] @@ -13,7 +15,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do end context 'developer warnings' do - let(:expected_error) { /Only `method` is allowed to specify the markdown field/ } + let_it_be(:expected_error) { /Only `method` is allowed to specify the markdown field/ } it 'raises when passing a resolver' do expect { class_with_markdown_field(:test_html, null: true, resolver: 'not really') } @@ -27,30 +29,61 @@ RSpec.describe Gitlab::Graphql::MarkdownField do end context 'resolving markdown' do - let(:note) { build(:note, note: '# Markdown!') } - let(:thing_with_markdown) { double('markdown thing', object: note) } - let(:expected_markdown) { '<h1 data-sourcepos="1:1-1:11" dir="auto">Markdown!</h1>' } - let(:query_type) { GraphQL::ObjectType.new } - let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} - let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) } + let_it_be(:note) { build(:note, note: '# Markdown!') } + let_it_be(:expected_markdown) { '<h1 data-sourcepos="1:1-1:11" dir="auto">Markdown!</h1>' } + let_it_be(:query_type) { GraphQL::ObjectType.new } + let_it_be(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} + let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) } + let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: {}, object: nil) } + + let(:type_class) { class_with_markdown_field(:note_html, null: false) } + let(:type_instance) { type_class.authorized_new(note, context) } + let(:field) { type_class.fields['noteHtml'] } it 'renders markdown from the same property as the field name without the `_html` suffix' do - field = class_with_markdown_field(:note_html, null: false).fields['noteHtml'] + expect(field.to_graphql.resolve(type_instance, {}, context)).to eq(expected_markdown) + end + + context 'when a `method` argument is passed' do + let(:type_class) { class_with_markdown_field(:test_html, null: false, method: :note) } + let(:field) { type_class.fields['testHtml'] } - expect(field.to_graphql.resolve(thing_with_markdown, {}, context)).to eq(expected_markdown) + it 'renders markdown from a specific property' do + expect(field.to_graphql.resolve(type_instance, {}, context)).to eq(expected_markdown) + end end - it 'renders markdown from a specific property when a `method` argument is passed' do - field = class_with_markdown_field(:test_html, null: false, method: :note).fields['testHtml'] + describe 'basic verification that references work' do + let_it_be(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:note) { build(:note, note: "Referencing #{issue.to_reference(full: true)}") } + + it 'renders markdown correctly' do + expect(field.to_graphql.resolve(type_instance, {}, context)).to include(issue_path(issue)) + end + + context 'when the issue is not publicly accessible' do + let_it_be(:project) { create(:project, :private) } + + it 'hides the references from users that are not allowed to see the reference' do + expect(field.to_graphql.resolve(type_instance, {}, context)).not_to include(issue_path(issue)) + end + + it 'shows the reference to users that are allowed to see it' do + context = GraphQL::Query::Context.new(query: query, values: { current_user: project.owner }, object: nil) + type_instance = type_class.authorized_new(note, context) - expect(field.to_graphql.resolve(thing_with_markdown, {}, context)).to eq(expected_markdown) + expect(field.to_graphql.resolve(type_instance, {}, context)).to include(issue_path(issue)) + end + end end end end def class_with_markdown_field(name, **args) - Class.new(GraphQL::Schema::Object) do + Class.new(Types::BaseObject) do prepend Gitlab::Graphql::MarkdownField + graphql_name 'MarkdownFieldTest' markdown_field name, **args end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb new file mode 100644 index 00000000000..b45bb8b79d9 --- /dev/null +++ b/spec/lib/gitlab/graphql/pagination/keyset/last_items_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Graphql::Pagination::Keyset::LastItems do + let_it_be(:merge_request) { create(:merge_request) } + let(:scope) { MergeRequest.order_merged_at_asc.with_order_id_desc } + + subject { described_class.take_items(*args) } + + context 'when the `count` parameter is nil' do + let(:args) { [scope, nil] } + + it 'returns a single record' do + expect(subject).to eq(merge_request) + end + end + + context 'when the `count` parameter is given' do + let(:args) { [scope, 1] } + + it 'returns an array' do + expect(subject).to eq([merge_request]) + end + end +end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb index 444c10074a0..eb28e6c8c0a 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb @@ -63,6 +63,29 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do expect(order_list.first.sort_direction).to eq :desc end end + + context 'when ordering by CASE', :aggregate_failuers do + let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) } + + it 'assigns the right attribute name, named function, and direction' do + expect(order_list.count).to eq 1 + expect(order_list.first.attribute_name).to eq 'case_order_value' + expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case) + expect(order_list.first.sort_direction).to eq :asc + end + end + + context 'when ordering by ARRAY_POSITION', :aggregate_failuers do + let(:array_position) { Arel::Nodes::NamedFunction.new('ARRAY_POSITION', [Arel.sql("ARRAY[1,0]::smallint[]"), Project.arel_table[:auto_cancel_pending_pipelines]]) } + let(:relation) { Project.order(array_position.asc) } + + it 'assigns the right attribute name, named function, and direction' do + expect(order_list.count).to eq 1 + expect(order_list.first.attribute_name).to eq 'array_position' + expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction) + expect(order_list.first.sort_direction).to eq :asc + end + end end describe '#validate_ordering' do diff --git a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb index c7e7db4d535..fa631aa5666 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb @@ -136,11 +136,12 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::QueryBuilder do let(:relation) { Project.sorted_by_similarity_desc('test', include_in_select: true) } let(:arel_table) { Project.arel_table } let(:decoded_cursor) { { 'similarity' => 0.5, 'id' => 100 } } + let(:similarity_function_call) { Gitlab::Database::SimilarityScore::SIMILARITY_FUNCTION_CALL_WITH_ANNOTATION } let(:similarity_sql) do [ - '(SIMILARITY(COALESCE("projects"."path", \'\'), \'test\') * CAST(\'1\' AS numeric))', - '(SIMILARITY(COALESCE("projects"."name", \'\'), \'test\') * CAST(\'0.7\' AS numeric))', - '(SIMILARITY(COALESCE("projects"."description", \'\'), \'test\') * CAST(\'0.2\' AS numeric))' + "(#{similarity_function_call}(COALESCE(\"projects\".\"path\", ''), 'test') * CAST('1' AS numeric))", + "(#{similarity_function_call}(COALESCE(\"projects\".\"name\", ''), 'test') * CAST('0.7' AS numeric))", + "(#{similarity_function_call}(COALESCE(\"projects\".\"description\", ''), 'test') * CAST('0.2' AS numeric))" ].join(' + ') end diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb index 89d2ab8bb87..c8432513185 100644 --- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb +++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do end it 'returns a duration in seconds' do - allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2]) + allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2, [[], []]]) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) allow(Gitlab::GraphqlLogger).to receive(:info) diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb index 045c922783a..009f66d2108 100644 --- a/spec/lib/gitlab/group_search_results_spec.rb +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -17,10 +17,17 @@ RSpec.describe Gitlab::GroupSearchResults do describe 'issues search' do let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') } let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') } + let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') } + let(:query) { 'foo' } let(:scope) { 'issues' } + before do + project.add_developer(user) + end + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' end describe 'merge_requests search' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 3126d87a0d6..5ee7fb2adbf 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -30,6 +30,7 @@ issues: - metrics - timelogs - issuable_severity +- issuable_sla - issue_assignees - closed_by - epic_issue @@ -51,6 +52,7 @@ issues: - status_page_published_incident - namespace - note_authors +- issue_email_participants events: - author - project @@ -158,6 +160,8 @@ merge_requests: - assignees - reviews - approval_rules +- approval_merge_request_rule_sources +- approval_project_rules - approvals - approvers - approver_users @@ -242,6 +246,7 @@ ci_pipelines: - latest_builds_report_results - messages - pipeline_artifacts +- latest_statuses ci_refs: - project - ci_pipelines @@ -300,6 +305,7 @@ protected_branches: - push_access_levels - unprotect_access_levels - approval_project_rules +- required_code_owners_sections protected_tags: - project - create_access_levels @@ -408,6 +414,7 @@ project: - stages - ci_refs - builds +- processables - runner_projects - runners - variables @@ -465,6 +472,8 @@ project: - feature_usage - approval_rules - approval_merge_request_rules +- approval_merge_request_rule_sources +- approval_project_rules - approvers - approver_users - audit_events @@ -536,6 +545,8 @@ project: - vulnerability_historical_statistics - product_analytics_events - pipeline_artifacts +- terraform_states +- alert_management_http_integrations award_emoji: - awardable - user @@ -703,3 +714,5 @@ system_note_metadata: - description_version status_page_published_incident: - issue +issuable_sla: + - issue diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index 93b6f93f0ec..d084b9d7f7e 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -10,14 +10,17 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do # all items are properly serialized while traversing the simple hash. subject { Gitlab::Json.parse(Gitlab::Json.generate(described_class.new(project, tree).execute)) } - let!(:project) { setup_project } - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { setup_project } let(:shared) { project.import_export_shared } let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:tree) { reader.project_tree } - before do + before_all do project.add_maintainer(user) + end + + before do allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD') allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA') end @@ -224,7 +227,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do group: group, approvals_before_merge: 1 ) - allow(project).to receive(:commit).and_return(Commit.new(RepoHelpers.sample_commit, project)) issue = create(:issue, assignees: [user], project: project) snippet = create(:project_snippet, project: project) diff --git a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb index eb9a3fa9bd8..6b2f80cc80a 100644 --- a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb @@ -5,16 +5,19 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::Group::RelationFactory do let(:group) { create(:group) } let(:members_mapper) { double('members_mapper').as_null_object } - let(:user) { create(:admin) } + let(:admin) { create(:admin) } + let(:importer_user) { admin } let(:excluded_keys) { [] } let(:created_object) do - described_class.create(relation_sym: relation_sym, - relation_hash: relation_hash, - members_mapper: members_mapper, - object_builder: Gitlab::ImportExport::Group::ObjectBuilder, - user: user, - importable: group, - excluded_keys: excluded_keys) + described_class.create( + relation_sym: relation_sym, + relation_hash: relation_hash, + members_mapper: members_mapper, + object_builder: Gitlab::ImportExport::Group::ObjectBuilder, + user: importer_user, + importable: group, + excluded_keys: excluded_keys + ) end context 'label object' do @@ -24,18 +27,18 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do let(:relation_hash) do { - 'id' => 123456, - 'title' => 'Bruffefunc', - 'color' => '#1d2da4', - 'project_id' => nil, - 'created_at' => '2019-11-20T17:02:20.546Z', - 'updated_at' => '2019-11-20T17:02:20.546Z', - 'template' => false, + 'id' => 123456, + 'title' => 'Bruffefunc', + 'color' => '#1d2da4', + 'project_id' => nil, + 'created_at' => '2019-11-20T17:02:20.546Z', + 'updated_at' => '2019-11-20T17:02:20.546Z', + 'template' => false, 'description' => 'Description', - 'group_id' => original_group_id, - 'type' => 'GroupLabel', - 'priorities' => [], - 'textColor' => '#FFFFFF' + 'group_id' => original_group_id, + 'type' => 'GroupLabel', + 'priorities' => [], + 'textColor' => '#FFFFFF' } end @@ -60,58 +63,28 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do end end - context 'Notes user references' do - let(:relation_sym) { :notes } - let(:new_user) { create(:user) } - let(:exported_member) do - { - 'id' => 111, - 'access_level' => 30, - 'source_id' => 1, - 'source_type' => 'Namespace', - 'user_id' => 3, - 'notification_level' => 3, - 'created_at' => '2016-11-18T09:29:42.634Z', - 'updated_at' => '2016-11-18T09:29:42.634Z', - 'user' => { - 'id' => 999, - 'email' => new_user.email, - 'username' => new_user.username - } - } - end - + it_behaves_like 'Notes user references' do + let(:importable) { group } let(:relation_hash) do { - 'id' => 4947, - 'note' => 'note', + 'id' => 4947, + 'note' => 'note', 'noteable_type' => 'Epic', - 'author_id' => 999, - 'created_at' => '2016-11-18T09:29:42.634Z', - 'updated_at' => '2016-11-18T09:29:42.634Z', - 'project_id' => 1, - 'attachment' => { + 'author_id' => 999, + 'created_at' => '2016-11-18T09:29:42.634Z', + 'updated_at' => '2016-11-18T09:29:42.634Z', + 'project_id' => 1, + 'attachment' => { 'url' => nil }, - 'noteable_id' => 377, - 'system' => true, - 'author' => { + 'noteable_id' => 377, + 'system' => true, + 'author' => { 'name' => 'Administrator' }, 'events' => [] } end - - let(:members_mapper) do - Gitlab::ImportExport::MembersMapper.new( - exported_members: [exported_member], - user: user, - importable: group) - end - - it 'maps the right author to the imported note' do - expect(created_object.author).to eq(new_user) - end end def random_id diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb index 9737a0f39fc..7a9e7d8afba 100644 --- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb +++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb @@ -23,6 +23,7 @@ RSpec.describe 'Test coverage of the Project Import' do project.issues.notes.events project.issues.notes.events.push_event_payload project.issues.milestone.events.push_event_payload + project.issues.issuable_sla project.issues.issue_milestones project.issues.issue_milestones.milestone project.issues.resource_label_events.label.priorities diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb index a347d835428..e208a1c383c 100644 --- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb +++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb @@ -102,4 +102,14 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do end end end + + describe '#clear_consumed_relations' do + let(:dir_path) { fixture } + + subject { ndjson_reader.clear_consumed_relations } + + it 'returns empty set' do + expect(subject).to be_empty + end + end end diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb index db76eb9538b..55b4f7479b8 100644 --- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb +++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb @@ -74,14 +74,6 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do } ) end - - it 'does not save a json file if feature is disabled' do - stub_feature_flags(export_lfs_objects_projects: false) - - saver.save - - expect(File.exist?(lfs_json_file)).to eq(false) - end end end diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index 31cf2362628..50bc6a30044 100644 --- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -3,19 +3,22 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::Project::RelationFactory do - let(:group) { create(:group) } + let(:group) { create(:group) } let(:project) { create(:project, :repository, group: group) } let(:members_mapper) { double('members_mapper').as_null_object } - let(:user) { create(:admin) } + let(:admin) { create(:admin) } + let(:importer_user) { admin } let(:excluded_keys) { [] } let(:created_object) do - described_class.create(relation_sym: relation_sym, - relation_hash: relation_hash, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - user: user, - importable: project, - excluded_keys: excluded_keys) + described_class.create( + relation_sym: relation_sym, + relation_hash: relation_hash, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + user: importer_user, + importable: project, + excluded_keys: excluded_keys + ) end before do @@ -113,9 +116,9 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do "created_at" => "2016-11-18T09:29:42.634Z", "updated_at" => "2016-11-18T09:29:42.634Z", "user" => { - "id" => user.id, - "email" => user.email, - "username" => user.username + "id" => admin.id, + "email" => admin.email, + "username" => admin.username } } end @@ -123,7 +126,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do let(:members_mapper) do Gitlab::ImportExport::MembersMapper.new( exported_members: [exported_member], - user: user, + user: importer_user, importable: project) end @@ -134,9 +137,9 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do 'source_branch' => "feature_conflict", 'source_project_id' => project.id, 'target_project_id' => project.id, - 'author_id' => user.id, - 'assignee_id' => user.id, - 'updated_by_id' => user.id, + 'author_id' => admin.id, + 'assignee_id' => admin.id, + 'updated_by_id' => admin.id, 'title' => "MR1", 'created_at' => "2016-06-14T15:02:36.568Z", 'updated_at' => "2016-06-14T15:02:56.815Z", @@ -151,11 +154,11 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do end it 'has preloaded author' do - expect(created_object.author).to equal(user) + expect(created_object.author).to equal(admin) end it 'has preloaded updated_by' do - expect(created_object.updated_by).to equal(user) + expect(created_object.updated_by).to equal(admin) end it 'has preloaded source project' do @@ -264,27 +267,8 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do end end - context 'Notes user references' do - let(:relation_sym) { :notes } - let(:new_user) { create(:user) } - let(:exported_member) do - { - "id" => 111, - "access_level" => 30, - "source_id" => 1, - "source_type" => "Project", - "user_id" => 3, - "notification_level" => 3, - "created_at" => "2016-11-18T09:29:42.634Z", - "updated_at" => "2016-11-18T09:29:42.634Z", - "user" => { - "id" => 999, - "email" => new_user.email, - "username" => new_user.username - } - } - end - + it_behaves_like 'Notes user references' do + let(:importable) { project } let(:relation_hash) do { "id" => 4947, @@ -305,17 +289,6 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do "events" => [] } end - - let(:members_mapper) do - Gitlab::ImportExport::MembersMapper.new( - exported_members: [exported_member], - user: user, - importable: project) - end - - it 'maps the right author to the imported note' do - expect(created_object.author).to eq(new_user) - end end context 'encrypted attributes' do diff --git a/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb new file mode 100644 index 00000000000..82f59245519 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::Sample::DateCalculator do + describe '#closest date to average' do + subject { described_class.new(dates).closest_date_to_average } + + context 'when dates are empty' do + let(:dates) { [] } + + it { is_expected.to be_nil } + end + + context 'when dates are not empty' do + let(:dates) { [[nil, '2020-01-01 00:00:00 +0000'], [nil, '2021-01-01 00:00:00 +0000'], [nil, '2022-01-01 23:59:59 +0000']] } + + it { is_expected.to eq(Time.zone.parse('2021-01-01 00:00:00 +0000')) } + end + end + + describe '#calculate_by_closest_date_to_average' do + let(:calculator) { described_class.new([]) } + let(:date) { Time.current } + + subject { calculator.calculate_by_closest_date_to_average(date) } + + context 'when average date is nil' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(nil) + end + + it { is_expected.to eq(date) } + end + + context 'when average date is in the past' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(date - 365.days) + allow(Time).to receive(:current).and_return(date) + end + + it { is_expected.to eq(date + 365.days) } + end + + context 'when average date is in the future' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(date + 10.days) + end + + it { is_expected.to eq(date) } + end + end +end diff --git a/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb new file mode 100644 index 00000000000..f173345a4c6 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# This spec is a lightweight version of: +# * project/tree_restorer_spec.rb +# +# In depth testing is being done in the above specs. +# This spec tests that restore of the sample project works +# but does not have 100% relation coverage. + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer do + include_context 'relation tree restorer shared context' + + let(:sample_data_relation_tree_restorer) do + described_class.new( + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: object_builder, + members_mapper: members_mapper, + relation_factory: relation_factory, + reader: reader, + importable: importable, + importable_path: importable_path, + importable_attributes: attributes + ) + end + + subject { sample_data_relation_tree_restorer.restore } + + shared_examples 'import project successfully' do + it 'restores project tree' do + expect(subject).to eq(true) + end + + describe 'imported project' do + let(:project) { Project.find_by_path('project') } + + before do + subject + end + + it 'has the project attributes and relations', :aggregate_failures do + expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.') + expect(project.issues.count).to eq(10) + expect(project.milestones.count).to eq(3) + expect(project.labels.count).to eq(2) + expect(project.project_feature).not_to be_nil + end + + it 'has issues with correctly updated due dates' do + due_dates = due_dates(project.issues) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + it 'has milestones with correctly updated due dates' do + due_dates = due_dates(project.milestones) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + def due_dates(relations) + due_dates = relations.map { |relation| relation['due_date'] } + due_dates.compact! + due_dates.sort + end + end + end + + context 'when restoring a project' do + let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } + let(:importable_name) { 'project' } + let(:importable_path) { 'project' } + let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } + let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } + let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } + + context 'using ndjson reader' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' } + let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } + + it_behaves_like 'import project successfully' + end + end +end 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 f75494aa7c7..c05968c9a85 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -1040,6 +1040,41 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do it_behaves_like 'project tree restorer work properly', :legacy_reader, true it_behaves_like 'project tree restorer work properly', :ndjson_reader, true + + context 'Sample Data JSON' do + let(:user) { create(:user) } + let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + + before do + setup_import_export_config('sample_data') + setup_reader(:ndjson_reader) + end + + context 'with sample_data_template' do + before do + allow(project).to receive_message_chain(:import_data, :data, :dig).with('sample_data') { true } + end + + it 'initialize SampleDataRelationTreeRestorer' do + expect_next_instance_of(Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer) do |restorer| + expect(restorer).to receive(:restore).and_return(true) + end + + expect(project_tree_restorer.restore).to eq(true) + end + end + + context 'without sample_data_template' do + it 'initialize RelationTreeRestorer' do + expect_next_instance_of(Gitlab::ImportExport::RelationTreeRestorer) do |restorer| + expect(restorer).to receive(:restore).and_return(true) + end + + expect(project_tree_restorer.restore).to eq(true) + end + end + end end context 'disable ndjson import' do diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb index ddc96b83208..bd9ac6d6697 100644 --- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb @@ -10,15 +10,7 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do - include ImportExport::CommonUtil - - let(:user) { create(:user) } - let(:shared) { Gitlab::ImportExport::Shared.new(importable) } - let(:attributes) { relation_reader.consume_attributes(importable_name) } - - let(:members_mapper) do - Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable) - end + include_context 'relation tree restorer shared context' let(:relation_tree_restorer) do described_class.new( diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index ace4449042e..b32ae60fbcc 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -36,21 +36,20 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do expect(subject.restore).to be_truthy end - context 'when the repository creation fails' do - before do - allow_next_instance_of(Repositories::DestroyService) do |instance| + context 'when the repository already exists' do + it 'deletes the existing repository before importing' do + allow(project.repository).to receive(:exists?).and_return(true) + allow(project.repository).to receive(:path).and_return('repository_path') + + expect_next_instance_of(Repositories::DestroyService) do |instance| expect(instance).to receive(:execute).and_call_original end - end - - it 'logs the error' do - allow(project.repository) - .to receive(:create_from_bundle) - .and_raise('9:CreateRepositoryFromBundle: target directory is non-empty') - expect(shared).to receive(:error).and_call_original + expect(shared.logger).to receive(:info).with( + message: 'Deleting existing "repository_path" to re-import it.' + ) - expect(subject.restore).to be_falsey + expect(subject.restore).to be_truthy end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 5ca7c5b7a91..e3d1f2c9368 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -855,3 +855,6 @@ ProjectSecuritySetting: - auto_fix_sast - created_at - updated_at +IssuableSla: + - issue_id + - due_at diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb index d96152e47ea..a6170c146ab 100644 --- a/spec/lib/gitlab/issuables_count_for_state_spec.rb +++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb @@ -4,14 +4,15 @@ require 'spec_helper' RSpec.describe Gitlab::IssuablesCountForState do let(:finder) do - double(:finder, count_by_state: { opened: 2, closed: 1 }) + double(:finder, current_user: nil, params: {}, count_by_state: { opened: 2, closed: 1 }) end - let(:counter) { described_class.new(finder) } + let(:project) { nil } + let(:fast_fail) { nil } + let(:counter) { described_class.new(finder, project, fast_fail: fast_fail) } describe 'project given' do let(:project) { build(:project) } - let(:counter) { described_class.new(finder, project) } it 'provides the project' do expect(counter.project).to eq(project) @@ -50,5 +51,19 @@ RSpec.describe Gitlab::IssuablesCountForState do it 'returns 0 when using an invalid state name as a String' do expect(counter['kittens']).to be_zero end + + context 'fast_fail enabled' do + let(:fast_fail) { true } + + it 'returns the expected value' do + expect(counter[:closed]).to eq(1) + end + + it 'returns -1 when the database times out' do + expect(finder).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled) + + expect(counter[:closed]).to eq(-1) + end + end end end diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb index 7aa0a3485fb..a9edb2b530b 100644 --- a/spec/lib/gitlab/job_waiter_spec.rb +++ b/spec/lib/gitlab/job_waiter_spec.rb @@ -2,23 +2,26 @@ require 'spec_helper' -RSpec.describe Gitlab::JobWaiter do +RSpec.describe Gitlab::JobWaiter, :redis do describe '.notify' do it 'pushes the jid to the named queue' do - key = 'gitlab:job_waiter:foo' - jid = 1 + key = described_class.new.key - redis = double('redis') - expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) - expect(redis).to receive(:lpush).with(key, jid) + described_class.notify(key, 123) - described_class.notify(key, jid) + Gitlab::Redis::SharedState.with do |redis| + expect(redis.ttl(key)).to be > 0 + end end end describe '#wait' do let(:waiter) { described_class.new(2) } + before do + allow_any_instance_of(described_class).to receive(:wait).and_call_original + end + it 'returns when all jobs have been completed' do described_class.notify(waiter.key, 'a') described_class.notify(waiter.key, 'b') diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 90c11f29855..7b6d143dda9 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do let(:api_url) { 'https://kubernetes.example.com/prefix' } let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } - let(:client) { described_class.new(api_url, kubeclient_options) } + let(:client) { described_class.new(api_url, **kubeclient_options) } before do stub_kubeclient_discover(api_url) @@ -133,7 +133,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do end it 'falls back to default options, but allows overriding' do - client = Gitlab::Kubernetes::KubeClient.new(api_url, {}) + client = described_class.new(api_url) defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS expect(client.kubeclient_options[:timeouts]).to eq(defaults[:timeouts]) @@ -347,6 +347,34 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do end end + describe '#get_ingresses' do + let(:extensions_client) { client.extensions_client } + let(:networking_client) { client.networking_client } + + include_examples 'redirection not allowed', 'get_ingresses' + include_examples 'dns rebinding not allowed', 'get_ingresses' + + it 'delegates to the extensions client' do + expect(extensions_client).to receive(:get_ingresses) + + client.get_ingresses + end + + context 'extensions does not have deployments for Kubernetes 1.22+ clusters' do + before do + WebMock + .stub_request(:get, api_url + '/apis/extensions/v1beta1') + .to_return(kube_response(kube_1_22_extensions_v1beta1_discovery_body)) + end + + it 'delegates to the apps client' do + expect(networking_client).to receive(:get_ingresses) + + client.get_ingresses + end + end + end + describe 'istio API group' do let(:istio_client) { client.istio_client } diff --git a/spec/lib/gitlab/lfs/client_spec.rb b/spec/lib/gitlab/lfs/client_spec.rb index 03563a632d6..1c50a2a7500 100644 --- a/spec/lib/gitlab/lfs/client_spec.rb +++ b/spec/lib/gitlab/lfs/client_spec.rb @@ -7,6 +7,8 @@ RSpec.describe Gitlab::Lfs::Client do let(:username) { 'user' } let(:password) { 'password' } let(:credentials) { { user: username, password: password, auth_method: 'password' } } + let(:git_lfs_content_type) { 'application/vnd.git-lfs+json' } + let(:git_lfs_user_agent) { "GitLab #{Gitlab::VERSION} LFS client" } let(:basic_auth_headers) do { 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}" } @@ -21,6 +23,18 @@ RSpec.describe Gitlab::Lfs::Client do } end + let(:verify_action) do + { + "href" => "#{base_url}/some/file/verify", + "header" => { + "Key" => "value" + } + } + end + + let(:authorized_upload_action) { upload_action.tap { |action| action['header']['Authorization'] = 'foo' } } + let(:authorized_verify_action) { verify_action.tap { |action| action['header']['Authorization'] = 'foo' } } + subject(:lfs_client) { described_class.new(base_url, credentials: credentials) } describe '#batch' do @@ -34,10 +48,10 @@ RSpec.describe Gitlab::Lfs::Client do ).to_return( status: 200, body: { 'objects' => 'anything', 'transfer' => 'basic' }.to_json, - headers: { 'Content-Type' => 'application/vnd.git-lfs+json' } + headers: { 'Content-Type' => git_lfs_content_type } ) - result = lfs_client.batch('upload', objects) + result = lfs_client.batch!('upload', objects) expect(stub).to have_been_requested expect(result).to eq('objects' => 'anything', 'transfer' => 'basic') @@ -48,7 +62,7 @@ RSpec.describe Gitlab::Lfs::Client do it 'raises an error' do stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400) - expect { lfs_client.batch('upload', objects) }.to raise_error(/Failed/) + expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/) end end @@ -56,7 +70,7 @@ RSpec.describe Gitlab::Lfs::Client do it 'raises an error' do stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400) - expect { lfs_client.batch('upload', objects) }.to raise_error(/Failed/) + expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/) end end @@ -68,17 +82,23 @@ RSpec.describe Gitlab::Lfs::Client do ).to_return( status: 200, body: { 'transfer' => 'carrier-pigeon' }.to_json, - headers: { 'Content-Type' => 'application/vnd.git-lfs+json' } + headers: { 'Content-Type' => git_lfs_content_type } ) - expect { lfs_client.batch('upload', objects) }.to raise_error(/Unsupported transfer/) + expect { lfs_client.batch!('upload', objects) }.to raise_error(/Unsupported transfer/) end end def stub_batch(objects:, headers:, operation: 'upload', transfer: 'basic') - objects = objects.map { |o| { oid: o.oid, size: o.size } } + objects = objects.as_json(only: [:oid, :size]) body = { operation: operation, 'transfers': [transfer], objects: objects }.to_json + headers = { + 'Accept' => git_lfs_content_type, + 'Content-Type' => git_lfs_content_type, + 'User-Agent' => git_lfs_user_agent + }.merge(headers) + stub_request(:post, base_url + '/info/lfs/objects/batch').with(body: body, headers: headers) end end @@ -90,7 +110,7 @@ RSpec.describe Gitlab::Lfs::Client do it "makes an HTTP PUT with expected parameters" do stub_upload(object: object, headers: upload_action['header']).to_return(status: 200) - lfs_client.upload(object, upload_action, authenticated: true) + lfs_client.upload!(object, upload_action, authenticated: true) end end @@ -101,7 +121,20 @@ RSpec.describe Gitlab::Lfs::Client do headers: basic_auth_headers.merge(upload_action['header']) ).to_return(status: 200) - lfs_client.upload(object, upload_action, authenticated: false) + lfs_client.upload!(object, upload_action, authenticated: false) + + expect(stub).to have_been_requested + end + end + + context 'request is not marked as authenticated but includes an authorization header' do + it 'prefers the provided authorization header' do + stub = stub_upload( + object: object, + headers: authorized_upload_action['header'] + ).to_return(status: 200) + + lfs_client.upload!(object, authorized_upload_action, authenticated: false) expect(stub).to have_been_requested end @@ -110,13 +143,13 @@ RSpec.describe Gitlab::Lfs::Client do context 'LFS object has no file' do let(:object) { LfsObject.new } - it 'makes an HJTT PUT with expected parameters' do + it 'makes an HTTP PUT with expected parameters' do stub = stub_upload( object: object, headers: upload_action['header'] ).to_return(status: 200) - lfs_client.upload(object, upload_action, authenticated: true) + lfs_client.upload!(object, upload_action, authenticated: true) expect(stub).to have_been_requested end @@ -126,7 +159,7 @@ RSpec.describe Gitlab::Lfs::Client do it 'raises an error' do stub_upload(object: object, headers: upload_action['header']).to_return(status: 400) - expect { lfs_client.upload(object, upload_action, authenticated: true) }.to raise_error(/Failed/) + expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/) end end @@ -134,15 +167,88 @@ RSpec.describe Gitlab::Lfs::Client do it 'raises an error' do stub_upload(object: object, headers: upload_action['header']).to_return(status: 500) - expect { lfs_client.upload(object, upload_action, authenticated: true) }.to raise_error(/Failed/) + expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/) end end def stub_upload(object:, headers:) + headers = { + 'Content-Type' => 'application/octet-stream', + 'Content-Length' => object.size.to_s, + 'User-Agent' => git_lfs_user_agent + }.merge(headers) + stub_request(:put, upload_action['href']).with( body: object.file.read, headers: headers.merge('Content-Length' => object.size.to_s) ) end end + + describe "#verify" do + let_it_be(:object) { create(:lfs_object) } + + context 'server returns 200 OK to an authenticated request' do + it "makes an HTTP POST with expected parameters" do + stub_verify(object: object, headers: verify_action['header']).to_return(status: 200) + + lfs_client.verify!(object, verify_action, authenticated: true) + end + end + + context 'server returns 200 OK to an unauthenticated request' do + it "makes an HTTP POST with expected parameters" do + stub = stub_verify( + object: object, + headers: basic_auth_headers.merge(upload_action['header']) + ).to_return(status: 200) + + lfs_client.verify!(object, verify_action, authenticated: false) + + expect(stub).to have_been_requested + end + end + + context 'request is not marked as authenticated but includes an authorization header' do + it 'prefers the provided authorization header' do + stub = stub_verify( + object: object, + headers: authorized_verify_action['header'] + ).to_return(status: 200) + + lfs_client.verify!(object, authorized_verify_action, authenticated: false) + + expect(stub).to have_been_requested + end + end + + context 'server returns 400 error' do + it 'raises an error' do + stub_verify(object: object, headers: verify_action['header']).to_return(status: 400) + + expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/) + end + end + + context 'server returns 500 error' do + it 'raises an error' do + stub_verify(object: object, headers: verify_action['header']).to_return(status: 500) + + expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/) + end + end + + def stub_verify(object:, headers:) + headers = { + 'Accept' => git_lfs_content_type, + 'Content-Type' => git_lfs_content_type, + 'User-Agent' => git_lfs_user_agent + }.merge(headers) + + stub_request(:post, verify_action['href']).with( + body: object.to_json(only: [:oid, :size]), + headers: headers + ) + end + end end diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 9b8b2c1417a..4b40e8960b2 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -104,7 +104,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do # Needs to be at least LfsToken::DEFAULT_EXPIRE_TIME + 60 seconds # in order to check whether it is valid 1 minute after it has expired - Timecop.freeze(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do + travel_to(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do expect(lfs_token.token_valid?(expired_token)).to be false end end diff --git a/spec/lib/gitlab/manifest_import/manifest_spec.rb b/spec/lib/gitlab/manifest_import/manifest_spec.rb index 2e8753b0880..352120c079d 100644 --- a/spec/lib/gitlab/manifest_import/manifest_spec.rb +++ b/spec/lib/gitlab/manifest_import/manifest_spec.rb @@ -12,19 +12,7 @@ RSpec.describe Gitlab::ManifestImport::Manifest do end context 'missing or invalid attributes' do - let(:file) { Tempfile.new('foo') } - - before do - content = <<~EOS - <manifest> - <remote review="invalid-url" /> - <project name="platform/build"/> - </manifest> - EOS - - file.write(content) - file.rewind - end + let(:file) { File.open(Rails.root.join('spec/fixtures/invalid_manifest.xml')) } it { expect(manifest.valid?).to be false } diff --git a/spec/lib/gitlab/manifest_import/metadata_spec.rb b/spec/lib/gitlab/manifest_import/metadata_spec.rb new file mode 100644 index 00000000000..c8158d3e148 --- /dev/null +++ b/spec/lib/gitlab/manifest_import/metadata_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_state do + let(:user) { double(id: 1) } + let(:repositories) do + [ + { id: 'test1', url: 'http://demo.host/test1' }, + { id: 'test2', url: 'http://demo.host/test2' } + ] + end + + describe '#save' do + it 'stores data in Redis with an expiry of EXPIRY_TIME' do + status = described_class.new(user) + repositories_key = 'manifest_import:metadata:user:1:repositories' + group_id_key = 'manifest_import:metadata:user:1:group_id' + + status.save(repositories, 2) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.ttl(repositories_key)).to be_within(5).of(described_class::EXPIRY_TIME) + expect(redis.ttl(group_id_key)).to be_within(5).of(described_class::EXPIRY_TIME) + end + end + end + + describe '#repositories' do + it 'allows repositories to round-trip with symbol keys' do + status = described_class.new(user) + + status.save(repositories, 2) + + expect(status.repositories).to eq(repositories) + end + + it 'uses the fallback when there is nothing in Redis' do + fallback = { manifest_import_repositories: repositories } + status = described_class.new(user, fallback: fallback) + + expect(status.repositories).to eq(repositories) + end + end + + describe '#group_id' do + it 'returns the group ID as an integer' do + status = described_class.new(user) + + status.save(repositories, 2) + + expect(status.group_id).to eq(2) + end + + it 'uses the fallback when there is nothing in Redis' do + fallback = { manifest_import_group_id: 3 } + status = described_class.new(user, fallback: fallback) + + expect(status.group_id).to eq(3) + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb index 09d5e048f6a..ff8f5797f9d 100644 --- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb @@ -8,9 +8,16 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do describe '#execute' do let(:project) { create(:project) } let(:dashboard_path) { 'path/to/dashboard.yml' } + let(:prometheus_adapter) { double('adapter', clear_prometheus_reactive_cache!: nil) } subject { described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path) } + before do + allow_next_instance_of(::Clusters::Applications::ScheduleUpdateService) do |update_service| + allow(update_service).to receive(:execute) + end + end + context 'valid dashboard' do let(:dashboard_hash) { load_sample_dashboard } @@ -21,20 +28,32 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do end context 'with existing metrics' do + let(:existing_metric_attributes) do + { + project: project, + identifier: 'metric_b', + title: 'overwrite', + y_label: 'overwrite', + query: 'overwrite', + unit: 'overwrite', + legend: 'overwrite', + dashboard_path: dashboard_path + } + end + let!(:existing_metric) do - create(:prometheus_metric, { - project: project, - identifier: 'metric_b', - title: 'overwrite', - y_label: 'overwrite', - query: 'overwrite', - unit: 'overwrite', - legend: 'overwrite' - }) + create(:prometheus_metric, existing_metric_attributes) + end + + let!(:existing_alert) do + alert = create(:prometheus_alert, project: project, prometheus_metric: existing_metric) + existing_metric.prometheus_alerts << alert + + alert end it 'updates existing PrometheusMetrics' do - described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute + subject.execute expect(existing_metric.reload.attributes.with_indifferent_access).to include({ title: 'Super Chart B', @@ -49,6 +68,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do expect { subject.execute }.to change { PrometheusMetric.count }.by(2) end + it 'updates affected environments' do + expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with( + existing_alert.environment.cluster_prometheus_adapter, + project + ).and_return(double('ScheduleUpdateService', execute: true)) + + subject.execute + end + context 'with stale metrics' do let!(:stale_metric) do create(:prometheus_metric, @@ -59,11 +87,45 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do ) end + let!(:stale_alert) do + alert = create(:prometheus_alert, project: project, prometheus_metric: stale_metric) + stale_metric.prometheus_alerts << alert + + alert + end + + it 'updates existing PrometheusMetrics' do + subject.execute + + expect(existing_metric.reload.attributes.with_indifferent_access).to include({ + title: 'Super Chart B', + y_label: 'y_label', + query: 'query', + unit: 'unit', + legend: 'Legend Label' + }) + end + it 'deletes stale metrics' do subject.execute expect { stale_metric.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'deletes stale alert' do + subject.execute + + expect { stale_alert.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'updates affected environments' do + expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with( + existing_alert.environment.cluster_prometheus_adapter, + project + ).and_return(double('ScheduleUpdateService', execute: true)) + + subject.execute + end end end end diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 69b779d36eb..631325402d9 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do end it 'increments requests count' do - expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get') + expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'unknown') subject.call(env) end @@ -32,75 +32,55 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do end it 'measures execution time' do - expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: '200', method: 'get' }, a_positive_execution_time) + expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time) Timecop.scale(3600) { subject.call(env) } end context 'request is a health check endpoint' do - it 'increments health endpoint counter' do - env['PATH_INFO'] = '/-/liveness' + ['/-/liveness', '/-/liveness/', '/-/%6D%65%74%72%69%63%73'].each do |path| + context "when path is #{path}" do + before do + env['PATH_INFO'] = path + end - expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get') + it 'increments health endpoint counter rather than overall counter' do + expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get', status: 200) + expect(described_class).not_to receive(:http_request_total) - subject.call(env) - end - - context 'with trailing slash' do - before do - env['PATH_INFO'] = '/-/liveness/' - end - - it 'increments health endpoint counter' do - expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get') - - subject.call(env) - end - end - - context 'with percent encoded values' do - before do - env['PATH_INFO'] = '/-/%6D%65%74%72%69%63%73' # /-/metrics - end + subject.call(env) + end - it 'increments health endpoint counter' do - expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get') + it 'does not record the request duration' do + expect(described_class).not_to receive(:http_request_duration_seconds) - subject.call(env) + subject.call(env) + end end end end context 'request is not a health check endpoint' do - it 'does not increment health endpoint counter' do - env['PATH_INFO'] = '/-/ordinary-requests' - - expect(described_class).not_to receive(:http_health_requests_total) - - subject.call(env) - end - - context 'path info is a root path' do - before do - env['PATH_INFO'] = '/-/' - end - - it 'does not increment health endpoint counter' do - expect(described_class).not_to receive(:http_health_requests_total) - - subject.call(env) - end - end - - context 'path info is a subpath' do - before do - env['PATH_INFO'] = '/-/health/subpath' - end - - it 'does not increment health endpoint counter' do - expect(described_class).not_to receive(:http_health_requests_total) - - subject.call(env) + ['/-/ordinary-requests', '/-/', '/-/health/subpath'].each do |path| + context "when path is #{path}" do + before do + env['PATH_INFO'] = path + end + + it 'increments overall counter rather than health endpoint counter' do + expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'unknown') + expect(described_class).not_to receive(:http_health_requests_total) + + subject.call(env) + end + + it 'records the request duration' do + expect(described_class) + .to receive_message_chain(:http_request_duration_seconds, :observe) + .with({ method: 'get' }, a_positive_execution_time) + + subject.call(env) + end end end end @@ -121,7 +101,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do end it 'increments requests count' do - expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get') + expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'unknown') expect { subject.call(env) }.to raise_error(StandardError) end @@ -133,13 +113,32 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do end end + context 'when a feature category header is present' do + before do + allow(app).to receive(:call).and_return([200, { described_class::FEATURE_CATEGORY_HEADER => 'issue_tracking' }, nil]) + end + + it 'adds the feature category to the labels for http_request_total' do + expect(described_class).to receive_message_chain(:http_request_total, :increment).with(method: 'get', status: 200, feature_category: 'issue_tracking') + + subject.call(env) + end + + it 'does not record a feature category for health check endpoints' do + env['PATH_INFO'] = '/-/liveness' + + expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get', status: 200) + expect(described_class).not_to receive(:http_request_total) + + subject.call(env) + end + end + describe '.initialize_http_request_duration_seconds' do it "sets labels" do expected_labels = [] - described_class::HTTP_METHODS.each do |method, statuses| - statuses.each do |status| - expected_labels << { method: method, status: status.to_s } - end + described_class::HTTP_METHODS.each do |method| + expected_labels << { method: method } end described_class.initialize_http_request_duration_seconds diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 1fffef53a82..7bac041cd65 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -135,6 +135,17 @@ RSpec.describe Gitlab::Middleware::Go do it_behaves_like 'unauthorized' end + + context 'with a blacklisted ip' do + it 'returns forbidden' do + expect(Gitlab::Auth).to receive(:find_for_git_client).and_raise(Gitlab::Auth::IpBlacklisted) + response = go + + expect(response[0]).to eq(403) + expect(response[1]['Content-Length']).to be_nil + expect(response[2]).to eq(['']) + end + end end end end @@ -176,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do it 'returns 404' do response = go + expect(response[0]).to eq(404) expect(response[1]['Content-Type']).to eq('text/html') expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>} - expect(response[2].body).to eq([expected_body]) + expect(response[2]).to eq([expected_body]) end end @@ -251,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do expect(response[0]).to eq(200) expect(response[1]['Content-Type']).to eq('text/html') expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>} - expect(response[2].body).to eq([expected_body]) + expect(response[2]).to eq([expected_body]) end end end diff --git a/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb b/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb new file mode 100644 index 00000000000..76a5174817e --- /dev/null +++ b/spec/lib/gitlab/middleware/handle_null_bytes_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' +require "rack/test" + +RSpec.describe Gitlab::Middleware::HandleNullBytes do + let(:null_byte) { "\u0000" } + let(:error_400) { [400, {}, ["Bad Request"]] } + let(:app) { double(:app) } + + subject { described_class.new(app) } + + before do + allow(app).to receive(:call) do |args| + args + end + end + + def env_for(params = {}) + Rack::MockRequest.env_for('/', { params: params }) + end + + context 'with null bytes in params' do + it 'rejects null bytes in a top level param' do + env = env_for(name: "null#{null_byte}byte") + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for hashes with strings" do + env = env_for(name: { inner_key: "I am #{null_byte} bad" }) + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for arrays with strings" do + env = env_for(name: ["I am #{null_byte} bad"]) + + expect(subject.call(env)).to eq error_400 + end + + it "responds with 400 BadRequest for arrays containing hashes with string values" do + env = env_for(name: [ + { + inner_key: "I am #{null_byte} bad" + } + ]) + + expect(subject.call(env)).to eq error_400 + end + + it "gives up and does not 400 with too deeply nested params" do + env = env_for(name: [ + { + inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{null_byte} bad" }] } + } + ]) + + expect(subject.call(env)).not_to eq error_400 + end + end + + context 'without null bytes in params' do + it "does not respond with a 400 for strings" do + env = env_for(name: "safe name") + + expect(subject.call(env)).not_to eq error_400 + end + + it "does not respond with a 400 with no params" do + env = env_for + + expect(subject.call(env)).not_to eq error_400 + end + end + + context 'when disabled via env flag' do + before do + stub_env('REJECT_NULL_BYTES', '1') + end + + it 'does not respond with a 400 no matter what' do + env = env_for(name: "null#{null_byte}byte") + + expect(subject.call(env)).not_to eq error_400 + end + end +end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index cdb48024531..a9dae72f4db 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Middleware::RailsQueueDuration do expect(transaction).to receive(:observe).with(:gitlab_rails_queue_duration_seconds, 1) - Timecop.freeze(Time.at(3)) do + travel_to(Time.at(3)) do expect(middleware.call(env)).to eq('yay') end end diff --git a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb index 2d1a9b2eee2..18342fd78ac 100644 --- a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb +++ b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb @@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do end context 'with no cookies' do - let(:cookies) { nil } + let(:cookies) { "" } it 'does not add headers' do response = do_request - expect(response['Set-Cookie']).to be_nil + expect(response['Set-Cookie']).to eq("") end end diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb index be20f0194f7..c9a23170137 100644 --- a/spec/lib/gitlab/pagination/offset_pagination_spec.rb +++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do let(:request_context) { double("request_context") } - subject do + subject(:paginator) do described_class.new(request_context) end @@ -119,6 +119,34 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do subject.paginate(resource) end end + + it 'does not return the total headers when excluding them' do + expect_no_header('X-Total') + expect_no_header('X-Total-Pages') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + + paginator.paginate(resource, exclude_total_headers: true) + end + end + + context 'when resource is a paginatable array' do + let(:resource) { Kaminari.paginate_array(Project.all.to_a) } + + it_behaves_like 'response with pagination headers' + + it 'only returns the requested resources' do + expect(paginator.paginate(resource).count).to eq(2) + end + + it 'does not return total headers when excluding them' do + expect_no_header('X-Total') + expect_no_header('X-Total-Pages') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + + paginator.paginate(resource, exclude_total_headers: true) + end end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index fe0735b8043..a76ad1f6f4c 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -265,9 +265,15 @@ RSpec.describe Gitlab::ProjectSearchResults do let_it_be(:project) { create(:project, :public) } let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') } let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') } + let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') } let(:query) { 'foo' } + before do + project.add_developer(user) + end + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' end end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index fa45c605b1b..98bd2efdbc6 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -8,9 +8,9 @@ RSpec.describe Gitlab::ProjectTemplate do expected = %w[ rails spring express iosswift dotnetcore android gomicro gatsby hugo jekyll plainhtml gitbook - hexo sse_middleman nfhugo nfjekyll nfplainhtml - nfgitbook nfhexo salesforcedx serverless_framework - jsonnet cluster_management + hexo sse_middleman gitpod_spring_petclinic nfhugo + nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx + serverless_framework jsonnet cluster_management ] expect(described_class.all).to be_an(Array) diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb index 8abc944eeb1..b2350eff9f9 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do around do |example| - Timecop.freeze(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } + travel_to(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } end include_examples 'additional metrics query' do diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb index 4683c4eae28..66b93d0dd72 100644 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Prometheus::Queries::DeploymentQuery do around do |example| time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) - Timecop.freeze(time_without_subsecond_values) { example.run } + travel_to(time_without_subsecond_values) { example.run } end it 'sends appropriate queries to prometheus' do diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb index 1422d48152a..1dbdb892a5d 100644 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -4,12 +4,12 @@ require 'spec_helper' RSpec.describe Gitlab::Prometheus::QueryVariables do describe '.call' do + let_it_be_with_refind(:environment) { create(:environment) } let(:project) { environment.project } - let(:environment) { create(:environment) } let(:slug) { environment.slug } let(:params) { {} } - subject { described_class.call(environment, params) } + subject { described_class.call(environment, **params) } it { is_expected.to include(ci_environment_slug: slug) } it { is_expected.to include(ci_project_name: project.name) } diff --git a/spec/lib/gitlab/redis/hll_spec.rb b/spec/lib/gitlab/redis/hll_spec.rb index cbf78f23036..e452e5b2f52 100644 --- a/spec/lib/gitlab/redis/hll_spec.rb +++ b/spec/lib/gitlab/redis/hll_spec.rb @@ -39,6 +39,24 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do end end end + + context 'when adding entries' do + let(:metric) { 'test-{metric}' } + + it 'supports single value' do + track_event(metric, 1) + + expect(count_unique_events([metric])).to eq(1) + end + + it 'supports multiple values' do + stub_const("#{described_class.name}::HLL_BATCH_SIZE", 2) + + track_event(metric, [1, 2, 3, 4, 5]) + + expect(count_unique_events([metric])).to eq(5) + end + end end describe '.count' do @@ -94,13 +112,13 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do expect(unique_counts).to eq(4) end + end - def track_event(key, value, expiry = 1.day) - described_class.add(key: key, value: value, expiry: expiry) - end + def track_event(key, value, expiry = 1.day) + described_class.add(key: key, value: value, expiry: expiry) + end - def count_unique_events(keys) - described_class.count(keys: keys) - end + def count_unique_events(keys) + described_class.count(keys: keys) end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 88c3315150b..1c56e489a94 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -99,6 +99,36 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('foo-') } end + describe '.build_trace_section_regex' do + subject { described_class.build_trace_section_regex } + + context 'without options' do + example = "section_start:1600445393032:NAME\r\033\[0K" + + it { is_expected.to match(example) } + it { is_expected.to match("section_end:12345678:aBcDeFg1234\r\033\[0K") } + it { is_expected.to match("section_start:0:sect_for_alpha-v1.0\r\033\[0K") } + it { is_expected.not_to match("section_start:section:0\r\033\[0K") } + it { is_expected.not_to match("section_:1600445393032:NAME\r\033\[0K") } + it { is_expected.not_to match(example.upcase) } + end + + context 'with options' do + it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true]\r\033\[0K") } + it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true, example_option=false]\r\033\[0K") } + it { is_expected.to match("section_start:1600445393032:NAME[collapsed=true,example_option=false]\r\033\[0K") } + it { is_expected.to match("section_start:1600445393032:NAME[numeric_option=1234567]\r\033\[0K") } + # Without splitting the regex in one for start and one for end, + # this is possible, however, it is ignored for section_end. + it { is_expected.to match("section_end:1600445393032:NAME[collapsed=true]\r\033\[0K") } + it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed=[]]]\r\033\[0K") } + it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed = true]\r\033\[0K") } + it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed = true, example_option=false]\r\033\[0K") } + it { is_expected.not_to match("section_start:1600445393032:NAME[collapsed=true, example_option=false]\r\033\[0K") } + it { is_expected.not_to match("section_start:1600445393032:NAME[]\r\033\[0K") } + end + end + describe '.container_repository_name_regex' do subject { described_class.container_repository_name_regex } @@ -317,6 +347,22 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('%2e%2e%2f1.2.3') } end + describe '.nuget_version_regex' do + subject { described_class.nuget_version_regex } + + it { is_expected.to match('1.2.3') } + it { is_expected.to match('1.2.3.4') } + it { is_expected.to match('1.2.3.4-stable.1') } + it { is_expected.to match('1.2.3-beta') } + it { is_expected.to match('1.2.3-alpha.3') } + it { is_expected.to match('1.0.7+r3456') } + it { is_expected.not_to match('1') } + it { is_expected.not_to match('1.2') } + it { is_expected.not_to match('1./2.3') } + it { is_expected.not_to match('../../../../../1.2.3') } + it { is_expected.not_to match('%2e%2e%2f1.2.3') } + end + describe '.pypi_version_regex' do subject { described_class.pypi_version_regex } @@ -384,6 +430,140 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('%2e%2e%2f1.2.3') } end + describe '.debian_package_name_regex' do + subject { described_class.debian_package_name_regex } + + it { is_expected.to match('0ad') } + it { is_expected.to match('g++') } + it { is_expected.to match('lua5.1') } + it { is_expected.to match('samba') } + + # may not be empty string + it { is_expected.not_to match('') } + # must start with an alphanumeric character + it { is_expected.not_to match('-a') } + it { is_expected.not_to match('+a') } + it { is_expected.not_to match('.a') } + it { is_expected.not_to match('_a') } + # only letters, digits and characters '-+._' + it { is_expected.not_to match('a~') } + it { is_expected.not_to match('aé') } + + # More strict Lintian regex + # at least 2 chars + it { is_expected.not_to match('a') } + # lowercase only + it { is_expected.not_to match('Aa') } + it { is_expected.not_to match('aA') } + # No underscore + it { is_expected.not_to match('a_b') } + end + + describe '.debian_version_regex' do + subject { described_class.debian_version_regex } + + context 'valid versions' do + it { is_expected.to match('1.0') } + it { is_expected.to match('1.0~alpha1') } + it { is_expected.to match('2:4.9.5+dfsg-5+deb10u1') } + end + + context 'dpkg errors' do + # version string is empty + it { is_expected.not_to match('') } + # version string has embedded spaces + it { is_expected.not_to match('1 0') } + # epoch in version is empty + it { is_expected.not_to match(':1.0') } + # epoch in version is not number + it { is_expected.not_to match('a:1.0') } + # epoch in version is negative + it { is_expected.not_to match('-1:1.0') } + # epoch in version is too big + it { is_expected.not_to match('9999999999:1.0') } + # nothing after colon in version number + it { is_expected.not_to match('2:') } + # revision number is empty + # Note: we are less strict here + # it { is_expected.not_to match('1.0-') } + # version number is empty + it { is_expected.not_to match('-1') } + it { is_expected.not_to match('2:-1') } + end + + context 'dpkg warnings' do + # version number does not start with digit + it { is_expected.not_to match('a') } + it { is_expected.not_to match('a1.0') } + # invalid character in version number + it { is_expected.not_to match('1_0') } + # invalid character in revision number + it { is_expected.not_to match('1.0-1_0') } + end + + context 'dpkg accepts' do + # dpkg accepts leading or trailing space + it { is_expected.not_to match(' 1.0') } + it { is_expected.not_to match('1.0 ') } + # dpkg accepts multiple colons + it { is_expected.not_to match('1:2:3') } + end + end + + describe '.debian_architecture_regex' do + subject { described_class.debian_architecture_regex } + + it { is_expected.to match('amd64') } + it { is_expected.to match('kfreebsd-i386') } + + # may not be empty string + it { is_expected.not_to match('') } + # must start with an alphanumeric + it { is_expected.not_to match('-a') } + it { is_expected.not_to match('+a') } + it { is_expected.not_to match('.a') } + it { is_expected.not_to match('_a') } + # only letters, digits and characters '-' + it { is_expected.not_to match('a+b') } + it { is_expected.not_to match('a.b') } + it { is_expected.not_to match('a_b') } + it { is_expected.not_to match('a~') } + it { is_expected.not_to match('aé') } + + # More strict + # Enforce lowercase + it { is_expected.not_to match('AMD64') } + it { is_expected.not_to match('Amd64') } + it { is_expected.not_to match('aMD64') } + end + + describe '.debian_distribution_regex' do + subject { described_class.debian_distribution_regex } + + it { is_expected.to match('buster') } + it { is_expected.to match('buster-updates') } + it { is_expected.to match('Debian10.5') } + + # Do not allow slash, even if this exists in the wild + it { is_expected.not_to match('jessie/updates') } + + # Do not allow Unicode + it { is_expected.not_to match('hé') } + end + + describe '.debian_component_regex' do + subject { described_class.debian_component_regex } + + it { is_expected.to match('main') } + it { is_expected.to match('non-free') } + + # Do not allow slash + it { is_expected.not_to match('non/free') } + + # Do not allow Unicode + it { is_expected.not_to match('hé') } + end + describe '.semver_regex' do subject { described_class.semver_regex } @@ -434,4 +614,45 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('%2e%2e%2f1.2.3') } it { is_expected.not_to match('') } end + + describe '.generic_package_name_regex' do + subject { described_class.generic_package_name_regex } + + it { is_expected.to match('123') } + it { is_expected.to match('foo') } + it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1') } + it { is_expected.not_to match('../../foo') } + it { is_expected.not_to match('..\..\foo') } + it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') } + it { is_expected.not_to match('$foo/bar') } + it { is_expected.not_to match('my file name') } + it { is_expected.not_to match('!!()()') } + end + + describe '.generic_package_file_name_regex' do + subject { described_class.generic_package_file_name_regex } + + it { is_expected.to match('123') } + it { is_expected.to match('foo') } + it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1.jar') } + it { is_expected.not_to match('../../foo') } + it { is_expected.not_to match('..\..\foo') } + it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') } + it { is_expected.not_to match('$foo/bar') } + it { is_expected.not_to match('my file name') } + it { is_expected.not_to match('!!()()') } + end + + describe '.prefixed_semver_regex' do + subject { described_class.prefixed_semver_regex } + + it { is_expected.to match('v1.2.3') } + it { is_expected.to match('v1.2.3-beta') } + it { is_expected.to match('v1.2.3-alpha.3') } + it { is_expected.not_to match('v1') } + it { is_expected.not_to match('v1.2') } + it { is_expected.not_to match('v1./2.3') } + it { is_expected.not_to match('v../../../../../1.2.3') } + it { is_expected.not_to match('v%2e%2e%2f1.2.3') } + end end diff --git a/spec/lib/gitlab/relative_positioning/mover_spec.rb b/spec/lib/gitlab/relative_positioning/mover_spec.rb index c49230c2415..dafd34585a8 100644 --- a/spec/lib/gitlab/relative_positioning/mover_spec.rb +++ b/spec/lib/gitlab/relative_positioning/mover_spec.rb @@ -37,18 +37,11 @@ RSpec.describe RelativePositioning::Mover do end def set_positions(positions) - vals = issues.zip(positions).map do |issue, pos| - issue.relative_position = pos - "(#{issue.id}, #{pos})" - end.join(', ') - - Issue.connection.exec_query(<<~SQL, 'set-positions') - WITH cte(cte_id, new_pos) AS ( - SELECT * FROM (VALUES #{vals}) as t (id, pos) - ) - UPDATE issues SET relative_position = new_pos FROM cte WHERE id = cte_id - ; - SQL + mapping = issues.zip(positions).to_h do |issue, pos| + [issue, { relative_position: pos }] + end + + ::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping) end def ids_in_position_order diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 05f32459164..912efa6a5db 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -18,7 +18,7 @@ RSpec.describe ::Gitlab::RepoPath do end it 'parses a full wiki project path' do - expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, project, Gitlab::GlRepository::WIKI, nil]) + expect(described_class.parse(project.wiki.repository.full_path)).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, nil]) end it 'parses a personal snippet repository path' do @@ -36,7 +36,7 @@ RSpec.describe ::Gitlab::RepoPath do end it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, nil]) + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, nil]) end it 'parses a relative path starting with /' do @@ -49,7 +49,7 @@ RSpec.describe ::Gitlab::RepoPath do end it 'parses a relative wiki path' do - expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, redirect_route]) + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project.wiki, project, Gitlab::GlRepository::WIKI, redirect_route]) end it 'parses a relative path starting with /' do diff --git a/spec/lib/gitlab/repository_size_checker_spec.rb b/spec/lib/gitlab/repository_size_checker_spec.rb index 9b2c02b1190..bd030d81d97 100644 --- a/spec/lib/gitlab/repository_size_checker_spec.rb +++ b/spec/lib/gitlab/repository_size_checker_spec.rb @@ -3,14 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::RepositorySizeChecker do + let_it_be(:namespace) { nil } let(:current_size) { 0 } let(:limit) { 50 } let(:enabled) { true } subject do described_class.new( - current_size_proc: -> { current_size }, - limit: limit, + current_size_proc: -> { current_size.megabytes }, + limit: limit.megabytes, + namespace: namespace, enabled: enabled ) end @@ -18,7 +20,7 @@ RSpec.describe Gitlab::RepositorySizeChecker do describe '#enabled?' do context 'when enabled' do it 'returns true' do - expect(subject.enabled?).to be_truthy + expect(subject.enabled?).to eq(true) end end @@ -26,7 +28,7 @@ RSpec.describe Gitlab::RepositorySizeChecker do let(:limit) { 0 } it 'returns false' do - expect(subject.enabled?).to be_falsey + expect(subject.enabled?).to eq(false) end end end @@ -35,59 +37,20 @@ RSpec.describe Gitlab::RepositorySizeChecker do let(:current_size) { 49 } it 'returns true when changes go over' do - expect(subject.changes_will_exceed_size_limit?(2)).to be_truthy + expect(subject.changes_will_exceed_size_limit?(2.megabytes)).to eq(true) end it 'returns false when changes do not go over' do - expect(subject.changes_will_exceed_size_limit?(1)).to be_falsey + expect(subject.changes_will_exceed_size_limit?(1.megabytes)).to eq(false) end end describe '#above_size_limit?' do - context 'when size is above the limit' do - let(:current_size) { 100 } - - it 'returns true' do - expect(subject.above_size_limit?).to be_truthy - end - end - - it 'returns false when not over the limit' do - expect(subject.above_size_limit?).to be_falsey - end + include_examples 'checker size above limit' + include_examples 'checker size not over limit' end describe '#exceeded_size' do - context 'when current size is below or equal to the limit' do - let(:current_size) { 50 } - - it 'returns zero' do - expect(subject.exceeded_size).to eq(0) - end - end - - context 'when current size is over the limit' do - let(:current_size) { 51 } - - it 'returns zero' do - expect(subject.exceeded_size).to eq(1) - end - end - - context 'when change size will be over the limit' do - let(:current_size) { 50 } - - it 'returns zero' do - expect(subject.exceeded_size(1)).to eq(1) - end - end - - context 'when change size will not be over the limit' do - let(:current_size) { 49 } - - it 'returns zero' do - expect(subject.exceeded_size(1)).to eq(0) - end - end + include_examples 'checker size exceeded' end end diff --git a/spec/lib/gitlab/repository_size_error_message_spec.rb b/spec/lib/gitlab/repository_size_error_message_spec.rb index b6b975143c9..53b5ed5518f 100644 --- a/spec/lib/gitlab/repository_size_error_message_spec.rb +++ b/spec/lib/gitlab/repository_size_error_message_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' RSpec.describe Gitlab::RepositorySizeErrorMessage do + let_it_be(:namespace) { build(:namespace) } let(:checker) do Gitlab::RepositorySizeChecker.new( current_size_proc: -> { 15.megabytes }, + namespace: namespace, limit: 10.megabytes ) end @@ -13,6 +15,10 @@ RSpec.describe Gitlab::RepositorySizeErrorMessage do let(:message) { checker.error_message } let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' } + before do + allow(namespace).to receive(:total_repository_size_excess).and_return(0) + end + describe 'error messages' do describe '#commit_error' do it 'returns the correct message' do diff --git a/spec/lib/gitlab/sample_data_template_spec.rb b/spec/lib/gitlab/sample_data_template_spec.rb new file mode 100644 index 00000000000..7d0d415b3af --- /dev/null +++ b/spec/lib/gitlab/sample_data_template_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SampleDataTemplate do + describe '.all' do + it 'returns all templates' do + expected = %w[ + basic + serenity_valley + ] + + expect(described_class.all).to be_an(Array) + expect(described_class.all.map(&:name)).to match_array(expected) + end + end + + describe '.find' do + subject { described_class.find(query) } + + context 'when there is a match' do + let(:query) { :basic } + + it { is_expected.to be_a(described_class) } + end + + context 'when there is no match' do + let(:query) { 'no-match' } + + it { is_expected.to be(nil) } + end + end + + describe '.archive_directory' do + subject { described_class.archive_directory } + + it { is_expected.to be_a Pathname } + end + + describe 'validate all templates' do + let_it_be(:admin) { create(:admin) } + + described_class.all.each do |template| + it "#{template.name} has a valid archive" do + archive = template.archive_path + + expect(File.exist?(archive)).to be(true) + end + + context 'with valid parameters' do + it 'can be imported' do + params = { + template_name: template.name, + namespace_id: admin.namespace.id, + path: template.name + } + + project = Projects::CreateFromTemplateService.new(admin, params).execute + + expect(project).to be_valid + expect(project).to be_persisted + end + end + end + end +end diff --git a/spec/lib/gitlab/search/recent_issues_spec.rb b/spec/lib/gitlab/search/recent_issues_spec.rb index 19a41d2aa38..c6d93173dc0 100644 --- a/spec/lib/gitlab/search/recent_issues_spec.rb +++ b/spec/lib/gitlab/search/recent_issues_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' RSpec.describe ::Gitlab::Search::RecentIssues do - def create_item(content:, project:) - create(:issue, title: content, project: project) + let(:parent_type) { :project } + + def create_item(content:, parent:) + create(:issue, title: content, project: parent) end it_behaves_like 'search recent items' diff --git a/spec/lib/gitlab/search/recent_merge_requests_spec.rb b/spec/lib/gitlab/search/recent_merge_requests_spec.rb index c6678ce0342..1da3e1425d9 100644 --- a/spec/lib/gitlab/search/recent_merge_requests_spec.rb +++ b/spec/lib/gitlab/search/recent_merge_requests_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' RSpec.describe ::Gitlab::Search::RecentMergeRequests do - def create_item(content:, project:) - create(:merge_request, :unique_branches, title: content, target_project: project, source_project: project) + let(:parent_type) { :project } + + def create_item(content:, parent:) + create(:merge_request, :unique_branches, title: content, target_project: parent, source_project: parent) end it_behaves_like 'search recent items' diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index b4cf6a568b4..57be9e93af2 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -11,9 +11,11 @@ RSpec.describe Gitlab::SearchResults do let_it_be(:issue) { create(:issue, project: project, title: 'foo') } let_it_be(:milestone) { create(:milestone, project: project, title: 'foo') } let(:merge_request) { create(:merge_request, source_project: project, title: 'foo') } + let(:query) { 'foo' } let(:filters) { {} } + let(:sort) { nil } - subject(:results) { described_class.new(user, 'foo', Project.order(:id), filters: filters) } + subject(:results) { described_class.new(user, query, Project.order(:id), sort: sort, filters: filters) } context 'as a user with access' do before do @@ -58,6 +60,25 @@ RSpec.describe Gitlab::SearchResults do end end + describe '#highlight_map' do + using RSpec::Parameterized::TableSyntax + + where(:scope, :expected) do + 'projects' | {} + 'issues' | {} + 'merge_requests' | {} + 'milestones' | {} + 'users' | {} + 'unknown' | {} + end + + with_them do + it 'returns the expected highlight_map' do + expect(results.highlight_map(scope)).to eq(expected) + end + end + end + describe '#formatted_limited_count' do using RSpec::Parameterized::TableSyntax @@ -137,10 +158,12 @@ RSpec.describe Gitlab::SearchResults do end describe '#merge_requests' do + let(:scope) { 'merge_requests' } + it 'includes project filter by default' do expect(results).to receive(:project_ids_relation).and_call_original - results.objects('merge_requests') + results.objects(scope) end it 'skips project filter if default project context is used' do @@ -148,24 +171,34 @@ RSpec.describe Gitlab::SearchResults do expect(results).not_to receive(:project_ids_relation) - results.objects('merge_requests') + results.objects(scope) end context 'filtering' do let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') } let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') } - let(:scope) { 'merge_requests' } let(:query) { 'foo' } include_examples 'search results filtered by state' end + + context 'ordering' do + let(:query) { 'sorted' } + let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) } + let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) } + let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) } + + include_examples 'search results sorted' + end end describe '#issues' do + let(:scope) { 'issues' } + it 'includes project filter by default' do expect(results).to receive(:project_ids_relation).and_call_original - results.objects('issues') + results.objects(scope) end it 'skips project filter if default project context is used' do @@ -173,16 +206,25 @@ RSpec.describe Gitlab::SearchResults do expect(results).not_to receive(:project_ids_relation) - results.objects('issues') + results.objects(scope) end context 'filtering' do - let(:scope) { 'issues' } - let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') } let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') } + let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') } include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + end + + context 'ordering' do + let(:query) { 'sorted' } + let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) } + let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) } + let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) } + + include_examples 'search results sorted' end end diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/lib/gitlab/sidekiq_cluster_spec.rb index 5dd913aebb0..5517abe1010 100644 --- a/spec/lib/gitlab/sidekiq_cluster_spec.rb +++ b/spec/lib/gitlab/sidekiq_cluster_spec.rb @@ -99,7 +99,7 @@ RSpec.describe Gitlab::SidekiqCluster do allow(Process).to receive(:spawn).and_return(1) expect(described_class).to receive(:wait_async).with(1) - expect(described_class.start_sidekiq(%w(foo), options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo), **options)).to eq(1) end it 'handles duplicate queue names' do @@ -109,7 +109,7 @@ RSpec.describe Gitlab::SidekiqCluster do .and_return(1) expect(described_class).to receive(:wait_async).with(1) - expect(described_class.start_sidekiq(%w(foo foo bar baz), options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(1) end it 'runs the sidekiq process in a new process group' do @@ -119,7 +119,7 @@ RSpec.describe Gitlab::SidekiqCluster do .and_return(1) allow(described_class).to receive(:wait_async) - expect(described_class.start_sidekiq(%w(foo bar baz), options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(1) end end diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb index bde19fa7552..ca473462d2e 100644 --- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb @@ -14,6 +14,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do include ApplicationWorker + feature_category :foo worker_context user: nil def perform(identifier, *args) @@ -56,6 +57,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do expect(TestWorker.contexts['identifier'].keys).not_to include('meta.user') end + it 'takes the feature category from the worker' do + TestWorker.perform_async('identifier', 1) + + expect(TestWorker.contexts['identifier']).to include('meta.feature_category' => 'foo') + end + it "doesn't fail for unknown workers" do expect { OtherWorker.perform_async }.not_to raise_error end diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index e1ae26a4d9e..2177b2be6d6 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -21,6 +21,12 @@ RSpec.describe Gitlab::SnippetSearchResults do end end + describe '#highlight_map' do + it 'returns the expected highlight map' do + expect(results.highlight_map('snippet_titles')).to eq({}) + end + end + describe '#objects' do it 'uses page and per_page to paginate results' do snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo') diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 220ac2ff6da..9bf6f0b82bc 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -3,6 +3,43 @@ require 'spec_helper' RSpec.describe Gitlab::SQL::Pattern do + using RSpec::Parameterized::TableSyntax + + describe '.fuzzy_search' do + let_it_be(:issue1) { create(:issue, title: 'noise foo noise', description: 'noise bar noise') } + let_it_be(:issue2) { create(:issue, title: 'noise baz noise', description: 'noise foo noise') } + let_it_be(:issue3) { create(:issue, title: 'Oh', description: 'Ah') } + + subject(:fuzzy_search) { Issue.fuzzy_search(query, columns) } + + where(:query, :columns, :expected) do + 'foo' | [Issue.arel_table[:title]] | %i[issue1] + + 'foo' | %i[title] | %i[issue1] + 'foo' | %w[title] | %i[issue1] + 'foo' | %i[description] | %i[issue2] + 'foo' | %i[title description] | %i[issue1 issue2] + 'bar' | %i[title description] | %i[issue1] + 'baz' | %i[title description] | %i[issue2] + 'qux' | %i[title description] | [] + + 'oh' | %i[title description] | %i[issue3] + 'OH' | %i[title description] | %i[issue3] + 'ah' | %i[title description] | %i[issue3] + 'AH' | %i[title description] | %i[issue3] + 'oh' | %i[title] | %i[issue3] + 'ah' | %i[description] | %i[issue3] + end + + with_them do + let(:expected_issues) { expected.map { |sym| send(sym) } } + + it 'finds the expected issues' do + expect(fuzzy_search).to match_array(expected_issues) + end + end + end + describe '.to_pattern' do subject(:to_pattern) { User.to_pattern(query) } diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb new file mode 100644 index 00000000000..9ce6007165b --- /dev/null +++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/global_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Global do + let(:global) { described_class.new(hash) } + let(:default_image_upload_path_value) { 'source/images' } + + let(:default_mounts_value) do + [ + { + source: 'source', + target: '' + } + ] + end + + let(:default_static_site_generator_value) { 'middleman' } + + shared_examples_for 'valid default configuration' do + describe '#compose!' do + before do + global.compose! + end + + it 'creates nodes hash' do + expect(global.descendants).to be_an Array + end + + it 'creates node object for each entry' do + expect(global.descendants.count).to eq 3 + end + + it 'creates node object using valid class' do + expect(global.descendants.map(&:class)).to match_array(expected_node_object_classes) + end + + it 'sets a description containing "Static Site Editor" for all nodes' do + expect(global.descendants.map(&:description)).to all(match(/Static Site Editor/)) + end + + describe '#leaf?' do + it 'is not leaf' do + expect(global).not_to be_leaf + end + end + end + + context 'when not composed' do + describe '#static_site_generator_value' do + it 'returns nil' do + expect(global.static_site_generator_value).to be nil + end + end + + describe '#leaf?' do + it 'is leaf' do + expect(global).to be_leaf + end + end + end + + context 'when composed' do + before do + global.compose! + end + + describe '#errors' do + it 'has no errors' do + expect(global.errors).to be_empty + end + end + + describe '#image_upload_path_value' do + it 'returns correct values' do + expect(global.image_upload_path_value).to eq(default_image_upload_path_value) + end + end + + describe '#mounts_value' do + it 'returns correct values' do + expect(global.mounts_value).to eq(default_mounts_value) + end + end + + describe '#static_site_generator_value' do + it 'returns correct values' do + expect(global.static_site_generator_value).to eq(default_static_site_generator_value) + end + end + end + end + + describe '.nodes' do + it 'returns a hash' do + expect(described_class.nodes).to be_a(Hash) + end + + context 'when filtering all the entry/node names' do + it 'contains the expected node names' do + expected_node_names = %i[ + image_upload_path + mounts + static_site_generator + ] + expect(described_class.nodes.keys).to match_array(expected_node_names) + end + end + end + + context 'when configuration is valid' do + context 'when some entries defined' do + let(:expected_node_object_classes) do + [ + Gitlab::StaticSiteEditor::Config::FileConfig::Entry::ImageUploadPath, + Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mounts, + Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator + ] + end + + let(:hash) do + { + image_upload_path: default_image_upload_path_value, + mounts: default_mounts_value, + static_site_generator: default_static_site_generator_value + } + end + + it_behaves_like 'valid default configuration' + end + end + + context 'when value is an empty hash' do + let(:expected_node_object_classes) do + [ + Gitlab::Config::Entry::Unspecified, + Gitlab::Config::Entry::Unspecified, + Gitlab::Config::Entry::Unspecified + ] + end + + let(:hash) { {} } + + it_behaves_like 'valid default configuration' + end + + context 'when configuration is not valid' do + before do + global.compose! + end + + context 'when a single entry is invalid' do + let(:hash) do + { image_upload_path: { not_a_string: true } } + end + + describe '#errors' do + it 'reports errors' do + expect(global.errors) + .to include 'image_upload_path config should be a string' + end + end + end + + context 'when a multiple entries are invalid' do + let(:hash) do + { + image_upload_path: { not_a_string: true }, + static_site_generator: { not_a_string: true } + } + end + + describe '#errors' do + it 'reports errors' do + expect(global.errors) + .to match_array([ + 'image_upload_path config should be a string', + 'static_site_generator config should be a string', + "static_site_generator config should be 'middleman'" + ]) + end + end + end + + context 'when there is an invalid key' do + let(:hash) do + { invalid_key: true } + end + + describe '#errors' do + it 'reports errors' do + expect(global.errors) + .to include 'global config contains unknown keys: invalid_key' + end + end + end + end + + context 'when value is not a hash' do + let(:hash) { [] } + + describe '#valid?' do + it 'is not valid' do + expect(global).not_to be_valid + end + end + + describe '#errors' do + it 'returns error about invalid type' do + expect(global.errors.first).to match /should be a hash/ + end + end + end + + describe '#specified?' do + it 'is concrete entry that is defined' do + expect(global.specified?).to be true + end + end + + describe '#[]' do + before do + global.compose! + end + + let(:hash) do + { static_site_generator: default_static_site_generator_value } + end + + context 'when entry exists' do + it 'returns correct entry' do + expect(global[:static_site_generator]) + .to be_an_instance_of Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator + expect(global[:static_site_generator].value).to eq default_static_site_generator_value + end + end + + context 'when entry does not exist' do + it 'always return unspecified node' do + expect(global[:some][:unknown][:node]) + .not_to be_specified + end + end + end +end diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb new file mode 100644 index 00000000000..c2b7fbf6f98 --- /dev/null +++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/image_upload_path_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::ImageUploadPath do + subject(:image_upload_path_entry) { described_class.new(config) } + + describe 'validations' do + context 'with a valid config' do + let(:config) { 'an-image-upload-path' } + + it { is_expected.to be_valid } + + describe '#value' do + it 'returns a image_upload_path key' do + expect(image_upload_path_entry.value).to eq config + end + end + end + + context 'with an invalid config' do + let(:config) { { not_a_string: true } } + + it { is_expected.not_to be_valid } + + it 'reports errors about wrong type' do + expect(image_upload_path_entry.errors) + .to include 'image upload path config should be a string' + end + end + end + + describe '.default' do + it 'returns default image_upload_path' do + expect(described_class.default).to eq 'source/images' + end + end +end diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb new file mode 100644 index 00000000000..04248fc60a5 --- /dev/null +++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mount do + subject(:entry) { described_class.new(config) } + + describe 'validations' do + context 'with a valid config' do + context 'and target is a non-empty string' do + let(:config) do + { + source: 'source', + target: 'sub-site' + } + end + + it { is_expected.to be_valid } + + describe '#value' do + it 'returns mount configuration' do + expect(entry.value).to eq config + end + end + end + + context 'and target is an empty string' do + let(:config) do + { + source: 'source', + target: '' + } + end + + it { is_expected.to be_valid } + + describe '#value' do + it 'returns mount configuration' do + expect(entry.value).to eq config + end + end + end + end + + context 'with an invalid config' do + context 'when source is not a string' do + let(:config) { { source: 123, target: 'target' } } + + it { is_expected.not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include 'mount source should be a string' + end + end + + context 'when source is not present' do + let(:config) { { target: 'target' } } + + it { is_expected.not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include "mount source can't be blank" + end + end + + context 'when target is not a string' do + let(:config) { { source: 'source', target: 123 } } + + it { is_expected.not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include 'mount target should be a string' + end + end + + context 'when there is an unknown key present' do + let(:config) { { test: 100 } } + + it { is_expected.not_to be_valid } + + it 'reports error' do + expect(entry.errors) + .to include 'mount config contains unknown keys: test' + end + end + end + end + + describe '.default' do + it 'returns default mount' do + expect(described_class.default) + .to eq({ + source: 'source', + target: '' + }) + end + end +end diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb new file mode 100644 index 00000000000..0ae2ece9474 --- /dev/null +++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/mounts_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::Mounts do + subject(:entry) { described_class.new(config) } + + describe 'validations' do + context 'with a valid config' do + let(:config) do + [ + { + source: 'source', + target: '' + }, + { + source: 'sub-site/source', + target: 'sub-site' + } + ] + end + + it { is_expected.to be_valid } + + describe '#value' do + it 'returns mounts configuration' do + expect(entry.value).to eq config + end + end + end + + context 'with an invalid config' do + let(:config) { { not_an_array: true } } + + it { is_expected.not_to be_valid } + + it 'reports errors about wrong type' do + expect(entry.errors) + .to include 'mounts config should be a array' + end + end + end + + describe '.default' do + it 'returns default mounts' do + expect(described_class.default) + .to eq([{ + source: 'source', + target: '' + }]) + end + end +end diff --git a/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb new file mode 100644 index 00000000000..a9c730218cf --- /dev/null +++ b/spec/lib/gitlab/static_site_editor/config/file_config/entry/static_site_generator_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig::Entry::StaticSiteGenerator do + let(:static_site_generator) { described_class.new(config) } + + describe 'validations' do + context 'when value is valid' do + let(:config) { 'middleman' } + + describe '#value' do + it 'returns a static_site_generator key' do + expect(static_site_generator.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(static_site_generator).to be_valid + end + end + end + + context 'when value is invalid' do + let(:config) { 'not-a-valid-generator' } + + describe '#valid?' do + it 'is not valid' do + expect(static_site_generator).not_to be_valid + end + end + end + + context 'when value has a wrong type' do + let(:config) { { not_a_string: true } } + + it 'reports errors about wrong type' do + expect(static_site_generator.errors) + .to include 'static site generator config should be a string' + end + end + end + + describe '.default' do + it 'returns default static_site_generator' do + expect(described_class.default).to eq 'middleman' + end + end +end diff --git a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb index 594425c2dab..d444d4f1df7 100644 --- a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb +++ b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb @@ -3,13 +3,85 @@ require 'spec_helper' RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig do - subject(:config) { described_class.new } + let(:config) do + described_class.new(yml) + end + + context 'when config is valid' do + context 'when config has valid values' do + let(:yml) do + <<-EOS + static_site_generator: middleman + EOS + end + + describe '#to_hash_with_defaults' do + it 'returns hash created from string' do + expect(config.to_hash_with_defaults.fetch(:static_site_generator)).to eq 'middleman' + end + end + + describe '#valid?' do + it 'is valid' do + expect(config).to be_valid + end + + it 'has no errors' do + expect(config.errors).to be_empty + end + end + end + end + + context 'when a config entry has an empty value' do + let(:yml) { 'static_site_generator: ' } + + describe '#to_hash' do + it 'returns default value' do + expect(config.to_hash_with_defaults.fetch(:static_site_generator)).to eq 'middleman' + end + end + + describe '#valid?' do + it 'is valid' do + expect(config).to be_valid + end + + it 'has no errors' do + expect(config.errors).to be_empty + end + end + end + + context 'when config is invalid' do + context 'when yml is incorrect' do + let(:yml) { '// invalid' } + + describe '.new' do + it 'raises error' do + expect { config }.to raise_error(described_class::ConfigError, /Invalid configuration format/) + end + end + end + + context 'when config value exists but is not a valid value' do + let(:yml) { 'static_site_generator: "unsupported-generator"' } + + describe '#valid?' do + it 'is not valid' do + expect(config).not_to be_valid + end - describe '#data' do - subject { config.data } + it 'has errors' do + expect(config.errors).not_to be_empty + end + end - it 'returns hardcoded data for now' do - is_expected.to match(static_site_generator: 'middleman') + describe '#errors' do + it 'returns an array of strings' do + expect(config.errors).to all(be_an_instance_of(String)) + end + end end end end diff --git a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb index 3433a54be9c..2f761b69e60 100644 --- a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb +++ b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do project: 'project', project_id: project.id, return_url: 'http://example.com', - is_supported_content: 'true', + is_supported_content: true, base_url: '/namespace/project/-/sse/master%2FREADME.md', merge_requests_illustration_path: %r{illustrations/merge_requests} }) @@ -65,7 +65,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do stub_feature_flags(sse_erb_support: project) end - it { is_expected.to include(is_supported_content: 'true') } + it { is_expected.to include(is_supported_content: true) } end context 'when feature flag is disabled' do @@ -75,7 +75,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do stub_feature_flags(sse_erb_support: false) end - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end end @@ -88,31 +88,31 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do context 'when branch is not master' do let(:ref) { 'my-branch' } - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end context 'when file does not have a markdown extension' do let(:path) { 'README.txt' } - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end context 'when file does not have an extension' do let(:path) { 'README' } - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end context 'when file does not exist' do let(:path) { 'UNKNOWN.md' } - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end context 'when repository is empty' do let(:repository) { create(:project_empty_repo).repository } - it { is_expected.to include(is_supported_content: 'false') } + it { is_expected.to include(is_supported_content: false) } end context 'when return_url is not a valid URL' do @@ -132,5 +132,11 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do it { is_expected.to include(return_url: nil) } end + + context 'when a commit for the ref cannot be found' do + let(:ref) { 'nonexistent-ref' } + + it { is_expected.to include(commit_id: nil) } + end end end diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb new file mode 100644 index 00000000000..351af3c07d2 --- /dev/null +++ b/spec/lib/gitlab/subscription_portal_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::SubscriptionPortal do + describe '.default_subscriptions_url' do + subject { described_class.default_subscriptions_url } + + context 'on non test and non dev environments' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(false) + allow(Rails).to receive_message_chain(:env, :development?).and_return(false) + end + + it 'returns production subscriptions app URL' do + is_expected.to eq('https://customers.gitlab.com') + end + end + + context 'on dev environment' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(false) + allow(Rails).to receive_message_chain(:env, :development?).and_return(true) + end + + it 'returns staging subscriptions app url' do + is_expected.to eq('https://customers.stg.gitlab.com') + end + end + + context 'on test environment' do + before do + allow(Rails).to receive_message_chain(:env, :test?).and_return(true) + allow(Rails).to receive_message_chain(:env, :development?).and_return(false) + end + + it 'returns staging subscriptions app url' do + is_expected.to eq('https://customers.stg.gitlab.com') + end + end + end +end diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb index 68ff28becfa..6d03cf496b8 100644 --- a/spec/lib/gitlab/themes_spec.rb +++ b/spec/lib/gitlab/themes_spec.rb @@ -47,4 +47,18 @@ RSpec.describe Gitlab::Themes, lib: true do expect(ids).not_to be_empty end end + + describe 'theme.css_filename' do + described_class.each do |theme| + next unless theme.css_filename + + context "for #{theme.name}" do + it 'returns an existing CSS filename' do + css_file_path = Rails.root.join('app/assets/stylesheets/themes', theme.css_filename + '.scss') + + expect(File.exist?(css_file_path)).to eq(true) + end + end + end + end end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index f0bf7b9964f..6ddeaf98370 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do end around do |example| - Timecop.freeze(timestamp) { example.run } + travel_to(timestamp) { example.run } end before do 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 2a674557b76..f2c1d8718d7 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 @@ -41,11 +41,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red context 'for web IDE edit actions' do it_behaves_like 'tracks and counts action' do def track_action(params) - described_class.track_web_ide_edit_action(params) + described_class.track_web_ide_edit_action(**params) end def count_unique(params) - described_class.count_web_ide_edit_actions(params) + described_class.count_web_ide_edit_actions(**params) end end end @@ -53,11 +53,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red context 'for SFE edit actions' do it_behaves_like 'tracks and counts action' do def track_action(params) - described_class.track_sfe_edit_action(params) + described_class.track_sfe_edit_action(**params) end def count_unique(params) - described_class.count_sfe_edit_actions(params) + described_class.count_sfe_edit_actions(**params) end end end @@ -65,11 +65,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red context 'for snippet editor edit actions' do it_behaves_like 'tracks and counts action' do def track_action(params) - described_class.track_snippet_editor_edit_action(params) + described_class.track_snippet_editor_edit_action(**params) end def count_unique(params) - described_class.count_snippet_editor_edit_actions(params) + described_class.count_snippet_editor_edit_actions(**params) end end end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index f881da71251..e84c3c17274 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 @@ -15,12 +15,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s # depending on which day of the week test is run. # Monday 6th of June reference_time = Time.utc(2020, 6, 1) - Timecop.freeze(reference_time) { example.run } + travel_to(reference_time) { example.run } end describe '.categories' do it 'gets all unique category names' do - expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit') + expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit', 'testing') end end @@ -238,16 +238,20 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s it 'returns the number of unique events for all known events' do results = { - 'category1' => { - 'event1_slot' => 1, - 'event2_slot' => 1, - 'category1_total_unique_counts_weekly' => 2, - 'category1_total_unique_counts_monthly' => 3 - }, - 'category2' => { - 'event3' => 1, - 'event4' => 1 - } + "category1" => { + "event1_slot_weekly" => 1, + "event1_slot_monthly" => 1, + "event2_slot_weekly" => 1, + "event2_slot_monthly" => 2, + "category1_total_unique_counts_weekly" => 2, + "category1_total_unique_counts_monthly" => 3 + }, + "category2" => { + "event3_weekly" => 1, + "event3_monthly" => 1, + "event4_weekly" => 1, + "event4_monthly" => 1 + } } expect(subject.unique_events_data).to eq(results) diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index 479fe36bcdd..e08dc41d0cc 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:action) { described_class::ISSUE_TITLE_CHANGED } def track_action(params) - described_class.track_issue_title_changed_action(params) + described_class.track_issue_title_changed_action(**params) end end end @@ -57,7 +57,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:action) { described_class::ISSUE_DESCRIPTION_CHANGED } def track_action(params) - described_class.track_issue_description_changed_action(params) + described_class.track_issue_description_changed_action(**params) end end end @@ -67,7 +67,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:action) { described_class::ISSUE_ASSIGNEE_CHANGED } def track_action(params) - described_class.track_issue_assignee_changed_action(params) + described_class.track_issue_assignee_changed_action(**params) end end end @@ -77,7 +77,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:action) { described_class::ISSUE_MADE_CONFIDENTIAL } def track_action(params) - described_class.track_issue_made_confidential_action(params) + described_class.track_issue_made_confidential_action(**params) end end end @@ -87,7 +87,207 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let(:action) { described_class::ISSUE_MADE_VISIBLE } def track_action(params) - described_class.track_issue_made_visible_action(params) + described_class.track_issue_made_visible_action(**params) + end + end + end + + context 'for Issue created actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_CREATED } + + def track_action(params) + described_class.track_issue_created_action(**params) + end + end + end + + context 'for Issue closed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_CLOSED } + + def track_action(params) + described_class.track_issue_closed_action(**params) + end + end + end + + context 'for Issue reopened actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_REOPENED } + + def track_action(params) + described_class.track_issue_reopened_action(**params) + end + end + end + + context 'for Issue label changed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_LABEL_CHANGED } + + def track_action(params) + described_class.track_issue_label_changed_action(**params) + end + end + end + + context 'for Issue cross-referenced actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_CROSS_REFERENCED } + + def track_action(params) + described_class.track_issue_cross_referenced_action(**params) + end + end + end + + context 'for Issue moved actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_MOVED } + + def track_action(params) + described_class.track_issue_moved_action(**params) + end + end + end + + context 'for Issue relate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_RELATED } + + def track_action(params) + described_class.track_issue_related_action(**params) + end + end + end + + context 'for Issue unrelate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_UNRELATED } + + def track_action(params) + described_class.track_issue_unrelated_action(**params) + end + end + end + + context 'for Issue marked as duplicate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE } + + def track_action(params) + described_class.track_issue_marked_as_duplicate_action(**params) + end + end + end + + context 'for Issue locked actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_LOCKED } + + def track_action(params) + described_class.track_issue_locked_action(**params) + end + end + end + + context 'for Issue unlocked actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_UNLOCKED } + + def track_action(params) + described_class.track_issue_unlocked_action(**params) + end + end + end + + context 'for Issue added to epic actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_ADDED_TO_EPIC} + + def track_action(params) + described_class.track_issue_added_to_epic_action(**params) + end + end + end + + context 'for Issue removed from epic actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC} + + def track_action(params) + described_class.track_issue_removed_from_epic_action(**params) + end + end + end + + context 'for Issue changed epic actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_CHANGED_EPIC} + + def track_action(params) + described_class.track_issue_changed_epic_action(**params) + end + end + end + + context 'for Issue designs added actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_DESIGNS_ADDED } + + def track_action(params) + described_class.track_issue_designs_added_action(**params) + end + end + end + + context 'for Issue designs modified actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_DESIGNS_MODIFIED } + + def track_action(params) + described_class.track_issue_designs_modified_action(**params) + end + end + end + + context 'for Issue designs removed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_DESIGNS_REMOVED } + + def track_action(params) + described_class.track_issue_designs_removed_action(**params) + end + end + end + + context 'for Issue due date changed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_DUE_DATE_CHANGED } + + def track_action(params) + described_class.track_issue_due_date_changed_action(**params) + end + end + end + + context 'for Issue time estimate changed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_TIME_ESTIMATE_CHANGED } + + def track_action(params) + described_class.track_issue_time_estimate_changed_action(**params) + end + end + end + + context 'for Issue time spent changed actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_TIME_SPENT_CHANGED } + + def track_action(params) + described_class.track_issue_time_spent_changed_action(**params) end end end diff --git a/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb new file mode 100644 index 00000000000..aaa576865f6 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/static_site_editor_counter_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::StaticSiteEditorCounter do + it_behaves_like 'a redis usage counter', 'StaticSiteEditor', :views + + it_behaves_like 'a redis usage counter with totals', :static_site_editor, + views: 3 +end diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb index 8f5f1347ce8..d1144dd0bc5 100644 --- a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb @@ -8,11 +8,11 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueEvents, :clean_gitlab_redis let(:time) { Time.zone.now } def track_event(params) - track_unique_events.track_event(params) + track_unique_events.track_event(**params) end def count_unique(params) - track_unique_events.count_unique_events(params) + track_unique_events.count_unique_events(**params) end context 'tracking an event' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 6631a0d3cc6..f64fa2b868d 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do before do stub_usage_data_connections stub_object_store_settings + clear_memoized_values(described_class::CE_MEMOIZED_VALUES) end describe '.uncached_data' do @@ -24,17 +25,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end it 'clears memoized values' do - values = %i(issue_minimum_id issue_maximum_id - project_minimum_id project_maximum_id - user_minimum_id user_maximum_id unique_visit_service - deployment_minimum_id deployment_maximum_id - approval_merge_request_rule_minimum_id - approval_merge_request_rule_maximum_id) - values.each do |key| - expect(described_class).to receive(:clear_memoization).with(key) - end + allow(described_class).to receive(:clear_memoization) subject + + described_class::CE_MEMOIZED_VALUES.each do |key| + expect(described_class).to have_received(:clear_memoization).with(key) + end end it 'merge_requests_users is included only in montly counters' do @@ -174,21 +171,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do for_defined_days_back do user = create(:user) + user2 = create(:user) create(:event, author: user) create(:group_member, user: user) + create(:authentication_event, user: user, provider: :ldapmain, result: :success) + create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user, provider: :group_saml, result: :failed) end expect(described_class.usage_activity_by_stage_manage({})).to include( events: 2, groups: 2, - users_created: 4, - omniauth_providers: ['google_oauth2'] + users_created: 6, + omniauth_providers: ['google_oauth2'], + user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4 } ) expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include( events: 1, groups: 1, - users_created: 2, - omniauth_providers: ['google_oauth2'] + users_created: 3, + omniauth_providers: ['google_oauth2'], + user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2 } ) end @@ -244,6 +249,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ) end + it 'includes group imports usage data' do + for_defined_days_back do + user = create(:user) + group = create(:group) + group.add_owner(user) + create(:group_import_state, group: group, user: user) + end + + expect(described_class.usage_activity_by_stage_manage({})) + .to include(groups_imported: 2) + expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)) + .to include(groups_imported: 1) + end + def omniauth_providers [ OpenStruct.new(name: 'google_oauth2'), @@ -260,17 +279,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do cluster = create(:cluster, user: user) create(:project, creator: user) create(:clusters_applications_prometheus, :installed, cluster: cluster) + create(:project_tracing_setting) end expect(described_class.usage_activity_by_stage_monitor({})).to include( clusters: 2, clusters_applications_prometheus: 2, - operations_dashboard_default_dashboard: 2 + operations_dashboard_default_dashboard: 2, + projects_with_tracing_enabled: 2 ) expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include( clusters: 1, clusters_applications_prometheus: 1, - operations_dashboard_default_dashboard: 1 + operations_dashboard_default_dashboard: 1, + projects_with_tracing_enabled: 1 ) end end @@ -415,11 +437,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:projects_slack_slash_commands_active]).to eq(1) expect(count_data[:projects_custom_issue_tracker_active]).to eq(1) expect(count_data[:projects_mattermost_active]).to eq(1) + expect(count_data[:groups_mattermost_active]).to eq(1) expect(count_data[:templates_mattermost_active]).to eq(1) expect(count_data[:instances_mattermost_active]).to eq(1) - expect(count_data[:projects_inheriting_instance_mattermost_active]).to eq(1) + expect(count_data[:projects_inheriting_mattermost_active]).to eq(1) + 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_alerts_service_enabled]).to eq(1) expect(count_data[:projects_with_prometheus_alerts]).to eq(2) expect(count_data[:projects_with_terraform_reports]).to eq(2) @@ -472,8 +497,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:personal_snippets]).to eq(2) expect(count_data[:project_snippets]).to eq(4) + expect(count_data[:projects_creating_incidents]).to eq(2) expect(count_data[:projects_with_packages]).to eq(2) expect(count_data[:packages]).to eq(4) + expect(count_data[:user_preferences_user_gitpod_enabled]).to eq(1) end it 'gathers object store usage correctly' do @@ -549,8 +576,17 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end describe '.system_usage_data_monthly' do + let_it_be(:project) { create(:project) } let!(:ud) { build(:usage_data) } + before do + stub_application_setting(self_monitoring_project: project) + + for_defined_days_back do + create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote') + end + end + subject { described_class.system_usage_data_monthly } it 'gathers monthly usage counts correctly' do @@ -563,6 +599,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(counts_monthly[:personal_snippets]).to eq(1) expect(counts_monthly[:project_snippets]).to eq(2) expect(counts_monthly[:packages]).to eq(3) + expect(counts_monthly[:promoted_issues]).to eq(1) end end @@ -570,6 +607,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.usage_counters } it { is_expected.to include(:kubernetes_agent_gitops_sync) } + it { is_expected.to include(:static_site_editor_views) } end describe '.usage_data_counters' do @@ -628,6 +666,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) expect(subject[:web_ide_clientside_preview_enabled]).to eq(Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?) expect(subject[:grafana_link_enabled]).to eq(Gitlab::CurrentSettings.grafana_enabled?) + expect(subject[:gitpod_enabled]).to eq(Gitlab::CurrentSettings.gitpod_enabled?) end context 'with embedded Prometheus' do @@ -657,6 +696,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(subject[:grafana_link_enabled]).to eq(false) end end + + context 'with Gitpod' do + it 'returns true when is enabled' do + stub_application_setting(gitpod_enabled: true) + + expect(subject[:gitpod_enabled]).to eq(true) + end + + it 'returns false when is disabled' do + stub_application_setting(gitpod_enabled: false) + + expect(subject[:gitpod_enabled]).to eq(false) + end + end end describe '.components_usage_data' do @@ -670,6 +723,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(subject[:git][:version]).to eq(Gitlab::Git.version) expect(subject[:database][:adapter]).to eq(Gitlab::Database.adapter_name) expect(subject[:database][:version]).to eq(Gitlab::Database.version) + expect(subject[:database][:pg_system_id]).to eq(Gitlab::Database.system_id) expect(subject[:mail][:smtp_server]).to eq(ActionMailer::Base.smtp_settings[:address]) expect(subject[:gitaly][:version]).to be_present expect(subject[:gitaly][:servers]).to be >= 1 @@ -979,9 +1033,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - def for_defined_days_back(days: [29, 2]) + def for_defined_days_back(days: [31, 3]) days.each do |n| - Timecop.travel(n.days.ago) do + travel_to(n.days.ago) do yield end end @@ -1078,8 +1132,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.compliance_unique_visits_data } before do - described_class.clear_memoization(:unique_visit_service) - allow_next_instance_of(::Gitlab::Analytics::UniqueVisits) do |instance| ::Gitlab::Analytics::UniqueVisits.compliance_events.each do |target| allow(instance).to receive(:unique_visits_for).with(targets: target).and_return(123) @@ -1110,7 +1162,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.search_unique_visits_data } before do - described_class.clear_memoization(:unique_visit_service) events = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('search') events.each do |event| allow(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:unique_events).with(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current).and_return(123) @@ -1136,9 +1187,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { ['source_code'] } + let(:ineligible_total_categories) { %w[source_code testing] } - it 'has all know_events' do + it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) expect(subject[:redis_hll_counters].keys).to match_array(categories) @@ -1146,11 +1197,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do categories.each do |category| keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category) + metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" } + if ineligible_total_categories.exclude?(category) - keys.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly") + metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly") end - expect(subject[:redis_hll_counters][category].keys).to match_array(keys) + expect(subject[:redis_hll_counters][category].keys).to match_array(metrics) end end end @@ -1169,6 +1222,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end describe '.snowplow_event_counts' do + let_it_be(:time_period) { { collector_tstamp: 8.days.ago..1.day.ago } } + context 'when self-monitoring project exists' do let_it_be(:project) { create(:project) } @@ -1181,14 +1236,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do stub_feature_flags(product_analytics: project) create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote') - create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 28.days.ago) + create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 2.days.ago) + create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 9.days.ago) + + create(:product_analytics_event, project: project, se_category: 'foo', se_action: 'bar', collector_tstamp: 2.days.ago) end it 'returns promoted_issues for the time period' do - expect(described_class.snowplow_event_counts[:promoted_issues]).to eq(2) - expect(described_class.snowplow_event_counts( - time_period: described_class.last_28_days_time_period(column: :collector_tstamp) - )[:promoted_issues]).to eq(1) + expect(described_class.snowplow_event_counts(time_period)[:promoted_issues]).to eq(1) end end @@ -1198,14 +1253,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end it 'returns an empty hash' do - expect(described_class.snowplow_event_counts).to eq({}) + expect(described_class.snowplow_event_counts(time_period)).to eq({}) end end end context 'when self-monitoring project does not exist' do it 'returns an empty hash' do - expect(described_class.snowplow_event_counts).to eq({}) + expect(described_class.snowplow_event_counts(time_period)).to eq({}) end end end diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 362cbaa78e9..9c0dc69ccd1 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -212,33 +212,26 @@ RSpec.describe Gitlab::Utils::UsageData do describe '#track_usage_event' do let(:value) { '9f302fea-f828-4ca9-aef4-e10bd723c0b3' } - let(:event_name) { 'my_event' } + let(:event_name) { 'incident_management_alert_status_changed' } let(:unknown_event) { 'unknown' } let(:feature) { "usage_data_#{event_name}" } + before do + skip_feature_flags_yaml_validation + end + context 'with feature enabled' do before do stub_feature_flags(feature => true) end it 'tracks redis hll event' do - stub_application_setting(usage_ping_enabled: true) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(value, event_name) described_class.track_usage_event(event_name, value) end - it 'does not track event when usage ping is not enabled' do - stub_application_setting(usage_ping_enabled: false) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - - described_class.track_usage_event(event_name, value) - end - it 'raise an error for unknown event' do - stub_application_setting(usage_ping_enabled: true) - expect { described_class.track_usage_event(unknown_event, value) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent) end end diff --git a/spec/lib/gitlab/visibility_level_checker_spec.rb b/spec/lib/gitlab/visibility_level_checker_spec.rb index 833021a22ca..38a7d967c33 100644 --- a/spec/lib/gitlab/visibility_level_checker_spec.rb +++ b/spec/lib/gitlab/visibility_level_checker_spec.rb @@ -5,16 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::VisibilityLevelChecker do let(:user) { create(:user) } let(:project) { create(:project) } - let(:visibility_level_checker) { } let(:override_params) { {} } - subject { described_class.new(user, project, project_params: override_params) } - describe '#level_restricted?' do + subject(:result) { described_class.new(user, project, project_params: override_params).level_restricted? } + context 'when visibility level is allowed' do it 'returns false with nil for visibility level' do - result = subject.level_restricted? - expect(result.restricted?).to eq(false) expect(result.visibility_level).to be_nil end @@ -25,12 +22,26 @@ RSpec.describe Gitlab::VisibilityLevelChecker do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end - it 'returns true and visibility name' do - project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - result = subject.level_restricted? + context 'for public project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + context 'for non-admin user' do + it 'returns true and visibility name' do + expect(result.restricted?).to eq(true) + expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context 'for admin user' do + let(:user) { create(:user, :admin) } - expect(result.restricted?).to eq(true) - expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + it 'returns false and a nil visibility level' do + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end end context 'overridden visibility' do @@ -50,8 +61,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do let(:override_visibility) { 'public' } it 'returns true and visibility name' do - result = subject.level_restricted? - expect(result.restricted?).to eq(true) expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end @@ -61,8 +70,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do let(:override_visibility) { 'publik' } it 'returns false with nil for visibility level' do - result = subject.level_restricted? - expect(result.restricted?).to eq(false) expect(result.visibility_level).to be_nil end @@ -72,8 +79,6 @@ RSpec.describe Gitlab::VisibilityLevelChecker do let(:override_params) { {} } it 'returns false with nil for visibility level' do - result = subject.level_restricted? - expect(result.restricted?).to eq(false) expect(result.visibility_level).to be_nil end diff --git a/spec/lib/gitlab/webpack/manifest_spec.rb b/spec/lib/gitlab/webpack/manifest_spec.rb new file mode 100644 index 00000000000..1427bdd7d4f --- /dev/null +++ b/spec/lib/gitlab/webpack/manifest_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'json' + +RSpec.describe Gitlab::Webpack::Manifest do + let(:manifest) do + <<-EOF + { + "errors": [], + "assetsByChunkName": { + "entry1": [ "entry1.js", "entry1-a.js" ], + "entry2": "entry2.js" + } + } + EOF + end + + around do |example| + Gitlab::Webpack::Manifest.clear_manifest! + + example.run + + Gitlab::Webpack::Manifest.clear_manifest! + end + + shared_examples_for "a valid manifest" do + it "returns single entry asset paths from the manifest" do + expect(Gitlab::Webpack::Manifest.asset_paths("entry2")).to eq(["/public_path/entry2.js"]) + end + + it "returns multiple entry asset paths from the manifest" do + expect(Gitlab::Webpack::Manifest.asset_paths("entry1")).to eq(["/public_path/entry1.js", "/public_path/entry1-a.js"]) + end + + it "errors on a missing entry point" do + expect { Gitlab::Webpack::Manifest.asset_paths("herp") }.to raise_error(Gitlab::Webpack::Manifest::AssetMissingError) + end + end + + before do + # Test that config variables work while we're here + allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false) + allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('my_manifest.json') + allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path') + allow(Gitlab.config.webpack).to receive(:output_dir).and_return('manifest_output') + end + + context "with dev server enabled" do + before do + allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true) + + stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: manifest, status: 200) + end + + describe ".asset_paths" do + it_behaves_like "a valid manifest" + + it "errors if we can't find the manifest" do + allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json') + stub_request(:get, "http://hostname:2000/public_path/broken.json").to_raise(SocketError) + + expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError) + end + + describe "webpack errors" do + context "when webpack has 'Module build failed' errors in its manifest" do + it "errors" do + error_manifest = Gitlab::Json.parse(manifest).merge("errors" => [ + "somethingModule build failed something", + "I am an error" + ]).to_json + stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200) + + expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::WebpackError) + end + end + + context "when webpack does not have 'Module build failed' errors in its manifest" do + it "does not error" do + error_manifest = Gitlab::Json.parse(manifest).merge("errors" => ["something went wrong"]).to_json + stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200) + + expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error + end + end + + it "does not error if errors is present but empty" do + error_manifest = Gitlab::Json.parse(manifest).merge("errors" => []).to_json + stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200) + expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error + end + end + end + end + + context "with dev server disabled" do + before do + allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false) + allow(File).to receive(:read).with(::Rails.root.join("manifest_output/my_manifest.json")).and_return(manifest) + end + + describe ".asset_paths" do + it_behaves_like "a valid manifest" + + it "errors if we can't find the manifest" do + allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json') + allow(File).to receive(:read).with(::Rails.root.join("manifest_output/broken.json")).and_raise(Errno::ENOENT) + expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError) + end + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index e9733851590..9662ad13631 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -54,12 +54,44 @@ RSpec.describe Gitlab::Workhorse do commit_id: metadata['CommitId'], prefix: metadata['ArchivePrefix'], format: Gitaly::GetArchiveRequest::Format::ZIP, - path: path + path: path, + include_lfs_blobs: true ).to_proto ) }.deep_stringify_keys) end + context 'when include_lfs_blobs_in_archive is disabled' do + before do + stub_feature_flags(include_lfs_blobs_in_archive: false) + end + + it 'sets include_lfs_blobs to false' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to eq({ + 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'ArchivePath' => metadata['ArchivePath'], + 'GetArchiveRequest' => Base64.encode64( + Gitaly::GetArchiveRequest.new( + repository: repository.gitaly_repository, + commit_id: metadata['CommitId'], + prefix: metadata['ArchivePrefix'], + format: Gitaly::GetArchiveRequest::Format::ZIP, + path: path, + include_lfs_blobs: false + ).to_proto + ) + }.deep_stringify_keys) + end + end + context 'when archive caching is disabled' do let(:cache_disabled) { true } diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb index b534823a888..e332647cf8a 100644 --- a/spec/lib/gitlab_danger_spec.rb +++ b/spec/lib/gitlab_danger_spec.rb @@ -9,7 +9,7 @@ RSpec.describe GitlabDanger do describe '.local_warning_message' do it 'returns an informational message with rules that can run' do - expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, telemetry, utility_css, pajamas') + expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, product_analytics, utility_css, pajamas') end end diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb index eeb99bfbb6c..92cb9e494ac 100644 --- a/spec/lib/google_api/auth_spec.rb +++ b/spec/lib/google_api/auth_spec.rb @@ -12,12 +12,12 @@ RSpec.describe GoogleApi::Auth do end describe '#authorize_url' do - subject { client.authorize_url } + subject { Addressable::URI.parse(client.authorize_url) } it 'returns authorize_url' do - is_expected.to start_with('https://accounts.google.com/o/oauth2') - is_expected.to include(URI.encode(redirect_uri, URI::PATTERN::RESERVED)) - is_expected.to include(URI.encode(redirect_to, URI::PATTERN::RESERVED)) + expect(subject.to_s).to start_with('https://accounts.google.com/o/oauth2') + expect(subject.query_values['state']).to eq(redirect_to) + expect(subject.query_values['redirect_uri']).to eq(redirect_uri) end end diff --git a/spec/lib/grafana/time_window_spec.rb b/spec/lib/grafana/time_window_spec.rb index 9ee65c6cf20..0657bed7b28 100644 --- a/spec/lib/grafana/time_window_spec.rb +++ b/spec/lib/grafana/time_window_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Grafana::TimeWindow do let(:to) { '1552828200000' } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#formatted' do @@ -37,7 +37,7 @@ RSpec.describe Grafana::RangeWithDefaults do let(:to) { Grafana::Timestamp.from_ms_since_epoch('1552828200000') } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#to_hash' do @@ -82,7 +82,7 @@ RSpec.describe Grafana::Timestamp do let(:timestamp) { Time.at(1552799400) } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#formatted' do diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb index a920f598c24..fa0cd214c7e 100644 --- a/spec/lib/marginalia_spec.rb +++ b/spec/lib/marginalia_spec.rb @@ -24,18 +24,6 @@ RSpec.describe 'Marginalia spec' do end end - def stub_feature(value) - allow(Gitlab::Marginalia).to receive(:cached_feature_enabled?).and_return(value) - end - - def make_request(correlation_id) - request_env = Rack::MockRequest.env_for('/') - - ::Labkit::Correlation::CorrelationId.use_id(correlation_id) do - MarginaliaTestController.action(:first_user).call(request_env) - end - end - describe 'For rails web requests' do let(:correlation_id) { SecureRandom.uuid } let(:recorded) { ActiveRecord::QueryRecorder.new { make_request(correlation_id) } } @@ -149,4 +137,17 @@ RSpec.describe 'Marginalia spec' do end end end + + def stub_feature(value) + stub_feature_flags(marginalia: value) + Gitlab::Marginalia.set_enabled_from_feature_flag + end + + def make_request(correlation_id) + request_env = Rack::MockRequest.env_for('/') + + ::Labkit::Correlation::CorrelationId.use_id(correlation_id) do + MarginaliaTestController.action(:first_user).call(request_env) + end + end end diff --git a/spec/lib/pager_duty/webhook_payload_parser_spec.rb b/spec/lib/pager_duty/webhook_payload_parser_spec.rb index 0010165318d..54c61b9121c 100644 --- a/spec/lib/pager_duty/webhook_payload_parser_spec.rb +++ b/spec/lib/pager_duty/webhook_payload_parser_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'json_schemer' RSpec.describe PagerDuty::WebhookPayloadParser do describe '.call' do @@ -8,36 +9,36 @@ RSpec.describe PagerDuty::WebhookPayloadParser do File.read(File.join(File.dirname(__FILE__), '../../fixtures/pager_duty/webhook_incident_trigger.json')) end + let(:triggered_event) do + { + 'event' => 'incident.trigger', + 'incident' => { + 'url' => 'https://webdemo.pagerduty.com/incidents/PRORDTY', + 'incident_number' => 33, + 'title' => 'My new incident', + 'status' => 'triggered', + 'created_at' => '2017-09-26T15:14:36Z', + 'urgency' => 'high', + 'incident_key' => nil, + 'assignees' => [{ + 'summary' => 'Laura Haley', + 'url' => 'https://webdemo.pagerduty.com/users/P553OPV' + }], + 'impacted_services' => [{ + 'summary' => 'Production XDB Cluster', + 'url' => 'https://webdemo.pagerduty.com/services/PN49J75' + }] + } + } + end + subject(:parse) { described_class.call(payload) } context 'when payload is a correct PagerDuty payload' do let(:payload) { Gitlab::Json.parse(fixture_file) } it 'returns parsed payload' do - is_expected.to eq( - [ - { - 'event' => 'incident.trigger', - 'incident' => { - 'url' => 'https://webdemo.pagerduty.com/incidents/PRORDTY', - 'incident_number' => 33, - 'title' => 'My new incident', - 'status' => 'triggered', - 'created_at' => '2017-09-26T15:14:36Z', - 'urgency' => 'high', - 'incident_key' => nil, - 'assignees' => [{ - 'summary' => 'Laura Haley', - 'url' => 'https://webdemo.pagerduty.com/users/P553OPV' - }], - 'impacted_services' => [{ - 'summary' => 'Production XDB Cluster', - 'url' => 'https://webdemo.pagerduty.com/services/PN49J75' - }] - } - } - ] - ) + is_expected.to eq([triggered_event]) end context 'when assignments summary and html_url are blank' do @@ -69,11 +70,42 @@ RSpec.describe PagerDuty::WebhookPayloadParser do end end - context 'when payload has no incidents' do + context 'when payload schema is invalid' do let(:payload) { { 'messages' => [{ 'event' => 'incident.trigger' }] } } it 'returns payload with blank incidents' do - is_expected.to eq([{ 'event' => 'incident.trigger', 'incident' => {} }]) + is_expected.to eq([]) + end + end + + context 'when payload consists of two messages' do + context 'when one of the messages has no incident data' do + let(:payload) do + valid_payload = Gitlab::Json.parse(fixture_file) + event = { 'event' => 'incident.trigger' } + valid_payload['messages'] = valid_payload['messages'].append(event) + valid_payload + end + + it 'returns parsed payload with valid events only' do + is_expected.to eq([triggered_event]) + end + end + + context 'when one of the messages has unknown event' do + let(:payload) do + valid_payload = Gitlab::Json.parse(fixture_file) + event = { 'event' => 'incident.unknown', 'incident' => valid_payload['messages'].first['incident'] } + valid_payload['messages'] = valid_payload['messages'].append(event) + valid_payload + end + + it 'returns parsed payload' do + unknown_event = triggered_event.dup + unknown_event['event'] = 'incident.unknown' + + is_expected.to contain_exactly(triggered_event, unknown_event) + end end end end diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb index 30b7e1cdd2c..443430b267d 100644 --- a/spec/lib/safe_zip/extract_spec.rb +++ b/spec/lib/safe_zip/extract_spec.rb @@ -15,11 +15,7 @@ RSpec.describe SafeZip::Extract do describe '#extract' do subject { object.extract(directories: directories, to: target_path) } - shared_examples 'extracts archive' do |param| - before do - stub_feature_flags(safezip_use_rubyzip: param) - end - + shared_examples 'extracts archive' do it 'does extract archive' do subject @@ -28,11 +24,7 @@ RSpec.describe SafeZip::Extract do end end - shared_examples 'fails to extract archive' do |param| - before do - stub_feature_flags(safezip_use_rubyzip: param) - end - + shared_examples 'fails to extract archive' do it 'does not extract archive' do expect { subject }.to raise_error(SafeZip::Extract::Error) end @@ -42,13 +34,7 @@ RSpec.describe SafeZip::Extract do context "when using #{name} archive" do let(:archive_name) { name } - context 'for RubyZip' do - it_behaves_like 'extracts archive', true - end - - context 'for UnZip' do - it_behaves_like 'extracts archive', false - end + it_behaves_like 'extracts archive' end end @@ -56,13 +42,7 @@ RSpec.describe SafeZip::Extract do context "when using #{name} archive" do let(:archive_name) { name } - context 'for RubyZip' do - it_behaves_like 'fails to extract archive', true - end - - context 'for UnZip (UNSAFE)' do - it_behaves_like 'extracts archive', false - end + it_behaves_like 'fails to extract archive' end end @@ -70,13 +50,7 @@ RSpec.describe SafeZip::Extract do let(:archive_name) { 'valid-simple.zip' } let(:directories) { %w(non/existing) } - context 'for RubyZip' do - it_behaves_like 'fails to extract archive', true - end - - context 'for UnZip' do - it_behaves_like 'fails to extract archive', false - end + it_behaves_like 'fails to extract archive' end end end |