diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 07:33:21 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 07:33:21 +0000 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /spec/lib | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) | |
download | gitlab-ce-36a59d088eca61b834191dacea009677a96c052f.tar.gz |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/lib')
224 files changed, 6093 insertions, 2585 deletions
diff --git a/spec/lib/api/ci/helpers/runner_helpers_spec.rb b/spec/lib/api/ci/helpers/runner_helpers_spec.rb index c4d740f0adc..c6cdc1732f5 100644 --- a/spec/lib/api/ci/helpers/runner_helpers_spec.rb +++ b/spec/lib/api/ci/helpers/runner_helpers_spec.rb @@ -70,5 +70,17 @@ RSpec.describe API::Ci::Helpers::Runner do expect(details['ip_address']).to eq(ip_address) end end + + describe '#log_artifact_size' do + subject { runner_helper.log_artifact_size(artifact) } + + let(:runner_params) { {} } + let(:artifact) { create(:ci_job_artifact, size: 42) } + let(:expected_params) { { artifact_size: artifact.size } } + let(:subject_proc) { proc { subject } } + + it_behaves_like 'storing arguments in the application context' + it_behaves_like 'not executing any extra queries for the application context' + end end end diff --git a/spec/lib/api/entities/ci/job_request/dependency_spec.rb b/spec/lib/api/entities/ci/job_request/dependency_spec.rb index fa5f3da554c..bbeb864c2ee 100644 --- a/spec/lib/api/entities/ci/job_request/dependency_spec.rb +++ b/spec/lib/api/entities/ci/job_request/dependency_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe API::Entities::Ci::JobRequest::Dependency do + let(:running_job) { create(:ci_build, :artifacts) } let(:job) { create(:ci_build, :artifacts) } - let(:entity) { described_class.new(job) } + let(:entity) { described_class.new(job, { running_job: running_job }) } subject { entity.as_json } @@ -16,8 +17,8 @@ RSpec.describe API::Entities::Ci::JobRequest::Dependency do expect(subject[:name]).to eq(job.name) end - it 'returns the dependency token' do - expect(subject[:token]).to eq(job.token) + it 'returns the token belonging to the running job' do + expect(subject[:token]).to eq(running_job.token) end it 'returns the dependency artifacts_file', :aggregate_failures do diff --git a/spec/lib/api/entities/plan_limit_spec.rb b/spec/lib/api/entities/plan_limit_spec.rb index 1b8b21d47f3..a88ea3f4cad 100644 --- a/spec/lib/api/entities/plan_limit_spec.rb +++ b/spec/lib/api/entities/plan_limit_spec.rb @@ -9,6 +9,14 @@ RSpec.describe API::Entities::PlanLimit do it 'exposes correct attributes' do expect(subject).to include( + :ci_pipeline_size, + :ci_active_jobs, + :ci_active_pipelines, + :ci_project_subscriptions, + :ci_pipeline_schedules, + :ci_needs_size_limit, + :ci_registered_group_runners, + :ci_registered_project_runners, :conan_max_file_size, :generic_packages_max_file_size, :helm_max_file_size, @@ -16,7 +24,8 @@ RSpec.describe API::Entities::PlanLimit do :npm_max_file_size, :nuget_max_file_size, :pypi_max_file_size, - :terraform_module_max_file_size + :terraform_module_max_file_size, + :storage_size_limit ) end diff --git a/spec/lib/api/entities/projects/topic_spec.rb b/spec/lib/api/entities/projects/topic_spec.rb index cdf142dbb7d..1ea0e724fed 100644 --- a/spec/lib/api/entities/projects/topic_spec.rb +++ b/spec/lib/api/entities/projects/topic_spec.rb @@ -11,6 +11,7 @@ RSpec.describe API::Entities::Projects::Topic do expect(subject).to include( :id, :name, + :title, :description, :total_projects_count, :avatar_url diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb index be5e8e8e8c2..407f2894f01 100644 --- a/spec/lib/api/entities/user_spec.rb +++ b/spec/lib/api/entities/user_spec.rb @@ -12,7 +12,40 @@ RSpec.describe API::Entities::User do subject { entity.as_json } it 'exposes correct attributes' do - expect(subject).to include(:name, :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information, :pronouns) + expect(subject.keys).to contain_exactly( + # UserSafe + :id, :username, :name, + # UserBasic + :state, :avatar_url, :web_url, + # User + :created_at, :bio, :location, :public_email, :skype, :linkedin, :twitter, + :website_url, :organization, :job_title, :pronouns, :bot, :work_information, + :followers, :following, :is_followed, :local_time + ) + end + + context 'exposing follow relationships' do + before do + allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, user).and_return(can_read_user_profile) + end + + %i(followers following is_followed).each do |relationship| + context 'when current user cannot read user profile' do + let(:can_read_user_profile) { false } + + it "does not expose #{relationship}" do + expect(subject).not_to include(relationship) + end + end + + context 'when current user can read user profile' do + let(:can_read_user_profile) { true } + + it "exposes #{relationship}" do + expect(subject).to include(relationship) + end + end + end end it 'exposes created_at if the current user can read the user profile' do @@ -135,6 +168,16 @@ RSpec.describe API::Entities::User do end end + context 'with logged-out user' do + let(:current_user) { nil } + + it 'exposes is_followed as nil' do + allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, user).and_return(true) + + expect(subject.keys).not_to include(:is_followed) + end + end + it 'exposes local_time' do local_time = '2:30 PM' expect(entity).to receive(:local_time).with(timezone).and_return(local_time) diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 2afe5a1a9d7..78ce9642392 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -150,8 +150,8 @@ RSpec.describe API::Helpers do context 'when user is authenticated' do before do - subject.instance_variable_set(:@current_user, user) - subject.instance_variable_set(:@initial_current_user, user) + allow(subject).to receive(:current_user).and_return(user) + allow(subject).to receive(:initial_current_user).and_return(user) end context 'public project' do @@ -167,8 +167,8 @@ RSpec.describe API::Helpers do context 'when user is not authenticated' do before do - subject.instance_variable_set(:@current_user, nil) - subject.instance_variable_set(:@initial_current_user, nil) + allow(subject).to receive(:current_user).and_return(nil) + allow(subject).to receive(:initial_current_user).and_return(nil) end context 'public project' do @@ -181,59 +181,214 @@ RSpec.describe API::Helpers do it_behaves_like 'private project without access' end end + + context 'support for IDs and paths as argument' do + let_it_be(:project) { create(:project) } + + let(:user) { project.first_owner} + + before do + allow(subject).to receive(:current_user).and_return(user) + allow(subject).to receive(:authorized_project_scope?).and_return(true) + allow(subject).to receive(:job_token_authentication?).and_return(false) + allow(subject).to receive(:authenticate_non_public?).and_return(false) + end + + shared_examples 'project finder' do + context 'when project exists' do + it 'returns requested project' do + expect(subject.find_project!(existing_id)).to eq(project) + end + + it 'returns nil' do + expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404) + expect(subject.find_project!(non_existing_id)).to be_nil + end + end + end + + context 'when ID is used as an argument' do + let(:existing_id) { project.id } + let(:non_existing_id) { non_existing_record_id } + + it_behaves_like 'project finder' + end + + context 'when PATH is used as an argument' do + let(:existing_id) { project.full_path } + let(:non_existing_id) { 'something/else' } + + it_behaves_like 'project finder' + + context 'with an invalid PATH' do + let(:non_existing_id) { 'undefined' } # path without slash + + it_behaves_like 'project finder' + + it 'does not hit the database' do + expect(Project).not_to receive(:find_by_full_path) + expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404) + + subject.find_project!(non_existing_id) + end + end + end + end end - describe '#find_project!' do - let_it_be(:project) { create(:project) } + describe '#find_group!' do + let_it_be(:group) { create(:group, :public) } + let_it_be(:user) { create(:user) } - let(:user) { project.first_owner} + shared_examples 'private group without access' do + before do + group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value('private')) + allow(subject).to receive(:authenticate_non_public?).and_return(false) + end - before do - allow(subject).to receive(:current_user).and_return(user) - allow(subject).to receive(:authorized_project_scope?).and_return(true) - allow(subject).to receive(:job_token_authentication?).and_return(false) - allow(subject).to receive(:authenticate_non_public?).and_return(false) + it 'returns not found' do + expect(subject).to receive(:not_found!) + + subject.find_group!(group.id) + end end - shared_examples 'project finder' do - context 'when project exists' do - it 'returns requested project' do - expect(subject.find_project!(existing_id)).to eq(project) + context 'when user is authenticated' do + before do + allow(subject).to receive(:current_user).and_return(user) + allow(subject).to receive(:initial_current_user).and_return(user) + end + + context 'public group' do + it 'returns requested group' do + expect(subject.find_group!(group.id)).to eq(group) end + end - it 'returns nil' do - expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404) - expect(subject.find_project!(non_existing_id)).to be_nil + context 'private group' do + it_behaves_like 'private group without access' + end + end + + context 'when user is not authenticated' do + before do + allow(subject).to receive(:current_user).and_return(nil) + allow(subject).to receive(:initial_current_user).and_return(nil) + end + + context 'public group' do + it 'returns requested group' do + expect(subject.find_group!(group.id)).to eq(group) end end + + context 'private group' do + it_behaves_like 'private group without access' + end end - context 'when ID is used as an argument' do - let(:existing_id) { project.id } - let(:non_existing_id) { non_existing_record_id } + context 'support for IDs and paths as arguments' do + let_it_be(:group) { create(:group) } - it_behaves_like 'project finder' + let(:user) { group.first_owner } + + before do + allow(subject).to receive(:current_user).and_return(user) + allow(subject).to receive(:authorized_project_scope?).and_return(true) + allow(subject).to receive(:job_token_authentication?).and_return(false) + allow(subject).to receive(:authenticate_non_public?).and_return(false) + end + + shared_examples 'group finder' do + context 'when group exists' do + it 'returns requested group' do + expect(subject.find_group!(existing_id)).to eq(group) + end + + it 'returns nil' do + expect(subject).to receive(:render_api_error!).with('404 Group Not Found', 404) + expect(subject.find_group!(non_existing_id)).to be_nil + end + end + end + + context 'when ID is used as an argument' do + let(:existing_id) { group.id } + let(:non_existing_id) { non_existing_record_id } + + it_behaves_like 'group finder' + end + + context 'when PATH is used as an argument' do + let(:existing_id) { group.full_path } + let(:non_existing_id) { 'something/else' } + + it_behaves_like 'group finder' + end end + end - context 'when PATH is used as an argument' do - let(:existing_id) { project.full_path } - let(:non_existing_id) { 'something/else' } + describe '#find_group_by_full_path!' do + let_it_be(:group) { create(:group, :public) } + let_it_be(:user) { create(:user) } - it_behaves_like 'project finder' + shared_examples 'private group without access' do + before do + group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value('private')) + allow(subject).to receive(:authenticate_non_public?).and_return(false) + end - context 'with an invalid PATH' do - let(:non_existing_id) { 'undefined' } # path without slash + it 'returns not found' do + expect(subject).to receive(:not_found!) - it_behaves_like 'project finder' + subject.find_group_by_full_path!(group.full_path) + end + end - it 'does not hit the database' do - expect(Project).not_to receive(:find_by_full_path) - expect(subject).to receive(:render_api_error!).with('404 Project Not Found', 404) + context 'when user is authenticated' do + before do + allow(subject).to receive(:current_user).and_return(user) + allow(subject).to receive(:initial_current_user).and_return(user) + end - subject.find_project!(non_existing_id) + context 'public group' do + it 'returns requested group' do + expect(subject.find_group_by_full_path!(group.full_path)).to eq(group) + end + end + + context 'private group' do + it_behaves_like 'private group without access' + + context 'with access' do + before do + group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value('private')) + group.add_developer(user) + end + + it 'returns requested group with access' do + expect(subject.find_group_by_full_path!(group.full_path)).to eq(group) + end end end end + + context 'when user is not authenticated' do + before do + allow(subject).to receive(:current_user).and_return(nil) + allow(subject).to receive(:initial_current_user).and_return(nil) + end + + context 'public group' do + it 'returns requested group' do + expect(subject.find_group_by_full_path!(group.full_path)).to eq(group) + end + end + + context 'private group' do + it_behaves_like 'private group without access' + end + end end describe '#find_namespace' do @@ -433,7 +588,7 @@ RSpec.describe API::Helpers do end end - describe '#order_options_with_tie_breaker' do + shared_examples '#order_options_with_tie_breaker' do subject { Class.new.include(described_class).new.order_options_with_tie_breaker } before do @@ -475,6 +630,30 @@ RSpec.describe API::Helpers do end end + describe '#order_options_with_tie_breaker' do + include_examples '#order_options_with_tie_breaker' + + context 'with created_at order given' do + let(:params) { { order_by: 'created_at', sort: 'asc' } } + + it 'converts to id' do + is_expected.to eq({ 'id' => 'asc' }) + end + + context 'when replace_order_by_created_at_with_id feature flag is disabled' do + before do + stub_feature_flags(replace_order_by_created_at_with_id: false) + end + + include_examples '#order_options_with_tie_breaker' + + it 'maintains created_at order' do + is_expected.to eq({ 'created_at' => 'asc', 'id' => 'asc' }) + end + end + end + end + describe "#destroy_conditionally!" do let!(:project) { create(:project) } diff --git a/spec/lib/atlassian/jira_connect/asymmetric_jwt_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb index c57d8ece86b..12ed47a1025 100644 --- a/spec/lib/atlassian/jira_connect/asymmetric_jwt_spec.rb +++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Atlassian::JiraConnect::AsymmetricJwt do +RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do describe '#valid?' do subject(:asymmetric_jwt) { described_class.new(jwt, verification_claims) } @@ -10,15 +10,19 @@ RSpec.describe Atlassian::JiraConnect::AsymmetricJwt do let(:jwt_claims) { { aud: aud, iss: client_key, qsh: qsh } } let(:aud) { 'https://test.host/-/jira_connect' } let(:client_key) { '1234' } - let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test') } let(:public_key_id) { '123e4567-e89b-12d3-a456-426614174000' } let(:jwt_headers) { { kid: public_key_id } } let(:private_key) { OpenSSL::PKey::RSA.generate 2048 } let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) } let(:public_key) { private_key.public_key } + let(:install_keys_url) { "https://connect-install-keys.atlassian.com/#{public_key_id}" } + let(:qsh) do + Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test') + end before do - stub_request(:get, "https://connect-install-keys.atlassian.com/#{public_key_id}").to_return(body: public_key.to_s, status: 200) + stub_request(:get, install_keys_url) + .to_return(body: public_key.to_s, status: 200) end it 'returns true when verified with public key from CDN' do @@ -26,7 +30,7 @@ RSpec.describe Atlassian::JiraConnect::AsymmetricJwt do expect(asymmetric_jwt).to be_valid - expect(WebMock).to have_requested(:get, "https://connect-install-keys.atlassian.com/#{public_key_id}") + expect(WebMock).to have_requested(:get, install_keys_url) end context 'JWT does not contain a key ID' do @@ -43,7 +47,7 @@ RSpec.describe Atlassian::JiraConnect::AsymmetricJwt do context 'public key can not be retrieved' do before do - stub_request(:get, "https://connect-install-keys.atlassian.com/#{public_key_id}").to_return(body: '', status: 404) + stub_request(:get, install_keys_url).to_return(body: '', status: 404) end it { is_expected.not_to be_valid } @@ -61,7 +65,8 @@ RSpec.describe Atlassian::JiraConnect::AsymmetricJwt do before do allow(JWT).to receive(:decode).and_call_original allow(JWT).to receive(:decode).with( - jwt, anything, true, aud: anything, verify_aud: true, iss: client_key, verify_iss: true, algorithm: 'RS256' + jwt, anything, true, + { aud: anything, verify_aud: true, iss: client_key, verify_iss: true, algorithm: 'RS256' } ).and_raise(JWT::DecodeError) end diff --git a/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb new file mode 100644 index 00000000000..61adff7e221 --- /dev/null +++ b/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::JiraConnect::Jwt::Symmetric do + let(:shared_secret) { 'secret' } + + describe '#iss_claim' do + let(:jwt) { Atlassian::Jwt.encode({ iss: '123' }, shared_secret) } + + subject { described_class.new(jwt).iss_claim } + + it { is_expected.to eq('123') } + + context 'invalid JWT' do + let(:jwt) { '123' } + + it { is_expected.to eq(nil) } + end + end + + describe '#sub_claim' do + let(:jwt) { Atlassian::Jwt.encode({ sub: '123' }, shared_secret) } + + subject { described_class.new(jwt).sub_claim } + + it { is_expected.to eq('123') } + + context 'invalid JWT' do + let(:jwt) { '123' } + + it { is_expected.to eq(nil) } + end + end + + describe '#valid?' do + subject { described_class.new(jwt).valid?(shared_secret) } + + context 'invalid JWT' do + let(:jwt) { '123' } + + it { is_expected.to eq(false) } + end + + context 'valid JWT' do + let(:jwt) { Atlassian::Jwt.encode({}, shared_secret) } + + it { is_expected.to eq(true) } + end + end + + describe '#verify_qsh_claim' do + let(:jwt) { Atlassian::Jwt.encode({ qsh: qsh_claim }, shared_secret) } + let(:qsh_claim) do + Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test') + end + + subject(:verify_qsh_claim) do + described_class.new(jwt).verify_qsh_claim('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test') + end + + it { is_expected.to eq(true) } + + context 'qsh does not match' do + let(:qsh_claim) do + Atlassian::Jwt.create_query_string_hash('https://example.com/foo', 'POST', 'https://example.com') + end + + it { is_expected.to eq(false) } + end + + context 'creating query string hash raises an error' do + let(:qsh_claim) { '123' } + + specify do + expect(Atlassian::Jwt).to receive(:create_query_string_hash).and_raise(StandardError) + + expect(verify_qsh_claim).to eq(false) + end + end + end + + describe '#verify_context_qsh_claim' do + let(:jwt) { Atlassian::Jwt.encode({ qsh: qsh_claim }, shared_secret) } + let(:qsh_claim) { 'context-qsh' } + + subject(:verify_context_qsh_claim) { described_class.new(jwt).verify_context_qsh_claim } + + it { is_expected.to eq(true) } + + context 'jwt does not contain a context qsh' do + let(:qsh_claim) { '123' } + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 192739d05a7..a2477834dde 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -145,16 +145,12 @@ RSpec.describe Backup::Manager do describe '#create' do let(:incremental_env) { 'false' } let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz task2.tar.gz} } - let(:backup_id) { '1546300800_2019_01_01_12.3' } - let(:tar_file) { "#{backup_id}_gitlab_backup.tar" } - let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } } - let(:tar_cmdline) { ['tar', '-cf', '-', *expected_backup_contents, tar_system_options] } - let(:backup_information) do - { - backup_created_at: Time.zone.parse('2019-01-01'), - gitlab_version: '12.3' - } - end + let(:backup_time) { Time.utc(2019, 1, 1) } + let(:backup_id) { "1546300800_2019_01_01_#{Gitlab::VERSION}" } + let(:full_backup_id) { backup_id } + let(:pack_tar_file) { "#{backup_id}_gitlab_backup.tar" } + let(:pack_tar_system_options) { { out: [pack_tar_file, 'w', Gitlab.config.backup.archive_permissions] } } + let(:pack_tar_cmdline) { ['tar', '-cf', '-', *expected_backup_contents, pack_tar_system_options] } let(:task1) { instance_double(Backup::Task) } let(:task2) { instance_double(Backup::Task) } @@ -170,427 +166,437 @@ RSpec.describe Backup::Manager do allow(ActiveRecord::Base.connection).to receive(:reconnect!) allow(Gitlab::BackupLogger).to receive(:info) allow(Kernel).to receive(:system).and_return(true) - allow(YAML).to receive(:load_file).and_call_original - allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) - .and_return(backup_information) - allow(subject).to receive(:backup_information).and_return(backup_information) - allow(task1).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task1.tar.gz'), backup_id) - allow(task2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'), backup_id) + allow(task1).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task1.tar.gz'), full_backup_id) + allow(task2).to receive(:dump).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'), full_backup_id) end it 'executes tar' do - subject.create # rubocop:disable Rails/SaveBang - - expect(Kernel).to have_received(:system).with(*tar_cmdline) - end - - context 'tar fails' do - before do - expect(Kernel).to receive(:system).with(*tar_cmdline).and_return(false) - end - - it 'logs a failure' do - expect do - subject.create # rubocop:disable Rails/SaveBang - end.to raise_error(Backup::Error, 'Backup failed') + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang - expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{tar_file} failed") + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) end end context 'when BACKUP is set' do let(:backup_id) { 'custom' } - it 'uses the given value as tar file name' do + before do stub_env('BACKUP', '/ignored/path/custom') - subject.create # rubocop:disable Rails/SaveBang - - expect(Kernel).to have_received(:system).with(*tar_cmdline) - end - end - - context 'when skipped is set in backup_information.yml' do - let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} } - let(:backup_information) do - { - backup_created_at: Time.zone.parse('2019-01-01'), - gitlab_version: '12.3', - skipped: ['task2'] - } end - it 'executes tar' do + it 'uses the given value as tar file name' do subject.create # rubocop:disable Rails/SaveBang - expect(Kernel).to have_received(:system).with(*tar_cmdline) - end - end - - context 'when SKIP env is set' do - let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} } - - before do - stub_env('SKIP', 'task2') + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) end - it 'executes tar' do - subject.create # rubocop:disable Rails/SaveBang + context 'tar fails' do + before do + expect(Kernel).to receive(:system).with(*pack_tar_cmdline).and_return(false) + end - expect(Kernel).to have_received(:system).with(*tar_cmdline) - end - end + it 'logs a failure' do + expect do + subject.create # rubocop:disable Rails/SaveBang + end.to raise_error(Backup::Error, 'Backup failed') - context 'when the destination is optional' do - let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} } - let(:definitions) do - { - 'task1' => Backup::Manager::TaskDefinition.new(task: task1, destination_path: 'task1.tar.gz'), - 'task2' => Backup::Manager::TaskDefinition.new(task: task2, destination_path: 'task2.tar.gz', destination_optional: true) - } + expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") + end end - it 'executes tar' do - expect(File).to receive(:exist?).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz')).and_return(false) + context 'when SKIP env is set' do + let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} } - subject.create # rubocop:disable Rails/SaveBang + before do + stub_env('SKIP', 'task2') + end - expect(Kernel).to have_received(:system).with(*tar_cmdline) - end - end + it 'executes tar' do + subject.create # rubocop:disable Rails/SaveBang - context 'many backup files' do - let(:files) do - [ - '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar', - '1451510000_2015_12_30_gitlab_backup.tar', - '1450742400_2015_12_22_gitlab_backup.tar', - '1449878400_gitlab_backup.tar', - '1449014400_gitlab_backup.tar', - 'manual_gitlab_backup.tar' - ] + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) + end end - before do - allow(Gitlab::BackupLogger).to receive(:info) - allow(Dir).to receive(:chdir).and_yield - allow(Dir).to receive(:glob).and_return(files) - allow(FileUtils).to receive(:rm) - allow(Time).to receive(:now).and_return(Time.utc(2016)) - end + context 'when the destination is optional' do + let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} } + let(:definitions) do + { + 'task1' => Backup::Manager::TaskDefinition.new(task: task1, destination_path: 'task1.tar.gz'), + 'task2' => Backup::Manager::TaskDefinition.new(task: task2, destination_path: 'task2.tar.gz', destination_optional: true) + } + end - context 'when keep_time is zero' do - before do - allow(Gitlab.config.backup).to receive(:keep_time).and_return(0) + it 'executes tar' do + expect(File).to receive(:exist?).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz')).and_return(false) subject.create # rubocop:disable Rails/SaveBang - end - it 'removes no files' do - expect(FileUtils).not_to have_received(:rm) - end - - it 'prints a skipped message' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... [SKIPPED]') + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) end end - context 'when no valid file is found' do + context 'many backup files' do let(:files) do [ - '14516064000_2016_01_01_1.2.3_gitlab_backup.tar', - 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar', - '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar' + '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar', + '1451510000_2015_12_30_gitlab_backup.tar', + '1450742400_2015_12_22_gitlab_backup.tar', + '1449878400_gitlab_backup.tar', + '1449014400_gitlab_backup.tar', + 'manual_gitlab_backup.tar' ] end before do - allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) - - subject.create # rubocop:disable Rails/SaveBang - end - - it 'removes no files' do - expect(FileUtils).not_to have_received(:rm) - end - - it 'prints a done message' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (0 removed)') + allow(Gitlab::BackupLogger).to receive(:info) + allow(Dir).to receive(:chdir).and_yield + allow(Dir).to receive(:glob).and_return(files) + allow(FileUtils).to receive(:rm) + allow(Time).to receive(:now).and_return(Time.utc(2016)) end - end - context 'when there are no files older than keep_time' do - before do - # Set to 30 days - allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000) + context 'when keep_time is zero' do + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(0) - subject.create # rubocop:disable Rails/SaveBang - end + subject.create # rubocop:disable Rails/SaveBang + end - it 'removes no files' do - expect(FileUtils).not_to have_received(:rm) - end + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end - it 'prints a done message' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (0 removed)') + it 'prints a skipped message' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... [SKIPPED]') + end end - end - context 'when keep_time is set to remove files' do - before do - # Set to 1 second - allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + context 'when no valid file is found' do + let(:files) do + [ + '14516064000_2016_01_01_1.2.3_gitlab_backup.tar', + 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar' + ] + end - subject.create # rubocop:disable Rails/SaveBang - end + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) - it 'removes matching files with a human-readable versioned timestamp' do - expect(FileUtils).to have_received(:rm).with(files[1]) - expect(FileUtils).to have_received(:rm).with(files[2]) - expect(FileUtils).to have_received(:rm).with(files[3]) - end + subject.create # rubocop:disable Rails/SaveBang + end - it 'removes matching files with a human-readable versioned timestamp with tagged EE' do - expect(FileUtils).to have_received(:rm).with(files[4]) - end + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end - it 'removes matching files with a human-readable non-versioned timestamp' do - expect(FileUtils).to have_received(:rm).with(files[5]) - expect(FileUtils).to have_received(:rm).with(files[6]) + it 'prints a done message' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (0 removed)') + end end - it 'removes matching files without a human-readable timestamp' do - expect(FileUtils).to have_received(:rm).with(files[7]) - expect(FileUtils).to have_received(:rm).with(files[8]) - end + context 'when there are no files older than keep_time' do + before do + # Set to 30 days + allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000) - it 'does not remove files that are not old enough' do - expect(FileUtils).not_to have_received(:rm).with(files[0]) - end + subject.create # rubocop:disable Rails/SaveBang + end - it 'does not remove non-matching files' do - expect(FileUtils).not_to have_received(:rm).with(files[9]) - end + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end - it 'prints a done message' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (8 removed)') + it 'prints a done message' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (0 removed)') + end end - end - - context 'when removing a file fails' do - let(:file) { files[1] } - let(:message) { "Permission denied @ unlink_internal - #{file}" } - before do - allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) - allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message) - - subject.create # rubocop:disable Rails/SaveBang - end + context 'when keep_time is set to remove files' do + before do + # Set to 1 second + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) - it 'removes the remaining expected files' do - expect(FileUtils).to have_received(:rm).with(files[4]) - expect(FileUtils).to have_received(:rm).with(files[5]) - expect(FileUtils).to have_received(:rm).with(files[6]) - expect(FileUtils).to have_received(:rm).with(files[7]) - expect(FileUtils).to have_received(:rm).with(files[8]) - end + subject.create # rubocop:disable Rails/SaveBang + end - it 'sets the correct removed count' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (7 removed)') - end + it 'removes matching files with a human-readable versioned timestamp' do + expect(FileUtils).to have_received(:rm).with(files[1]) + expect(FileUtils).to have_received(:rm).with(files[2]) + expect(FileUtils).to have_received(:rm).with(files[3]) + end - it 'prints the error from file that could not be removed' do - expect(Gitlab::BackupLogger).to have_received(:info).with(message: a_string_matching(message)) - end - end - end + it 'removes matching files with a human-readable versioned timestamp with tagged EE' do + expect(FileUtils).to have_received(:rm).with(files[4]) + end - describe 'cloud storage' do - let(:backup_file) { Tempfile.new('backup', Gitlab.config.backup.path) } - let(:backup_filename) { File.basename(backup_file.path) } + it 'removes matching files with a human-readable non-versioned timestamp' do + expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + end - before do - allow(Gitlab::BackupLogger).to receive(:info) - allow(subject).to receive(:tar_file).and_return(backup_filename) - - stub_backup_setting( - upload: { - connection: { - provider: 'AWS', - aws_access_key_id: 'id', - aws_secret_access_key: 'secret' - }, - remote_directory: 'directory', - multipart_chunk_size: 104857600, - encryption: nil, - encryption_key: nil, - storage_class: nil - } - ) + it 'removes matching files without a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) + end - Fog.mock! + it 'does not remove files that are not old enough' do + expect(FileUtils).not_to have_received(:rm).with(files[0]) + end - # the Fog mock only knows about directories we create explicitly - connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) - connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang - end + it 'does not remove non-matching files' do + expect(FileUtils).not_to have_received(:rm).with(files[9]) + end - context 'skipped upload' do - let(:backup_information) do - { - backup_created_at: Time.zone.parse('2019-01-01'), - gitlab_version: '12.3', - skipped: ['remote'] - } + it 'prints a done message' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (8 removed)') + end end - it 'informs the user' do - stub_env('SKIP', 'remote') - subject.create # rubocop:disable Rails/SaveBang - - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... [SKIPPED]') - end - end + context 'when removing a file fails' do + let(:file) { files[1] } + let(:message) { "Permission denied @ unlink_internal - #{file}" } - context 'target path' do - it 'uses the tar filename by default' do - expect_any_instance_of(Fog::Collection).to receive(:create) - .with(hash_including(key: backup_filename, public: false)) - .and_call_original + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message) - subject.create # rubocop:disable Rails/SaveBang - end + subject.create # rubocop:disable Rails/SaveBang + end - it 'adds the DIRECTORY environment variable if present' do - stub_env('DIRECTORY', 'daily') + it 'removes the remaining expected files' do + expect(FileUtils).to have_received(:rm).with(files[4]) + expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) + end - expect_any_instance_of(Fog::Collection).to receive(:create) - .with(hash_including(key: "daily/#{backup_filename}", public: false)) - .and_call_original + it 'sets the correct removed count' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Deleting old backups ... done. (7 removed)') + end - subject.create # rubocop:disable Rails/SaveBang + it 'prints the error from file that could not be removed' do + expect(Gitlab::BackupLogger).to have_received(:info).with(message: a_string_matching(message)) + end end end - context 'with AWS with server side encryption' do - let(:connection) { ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) } - let(:encryption_key) { nil } - let(:encryption) { nil } - let(:storage_options) { nil } + describe 'cloud storage' do + let(:backup_file) { Tempfile.new('backup', Gitlab.config.backup.path) } + let(:backup_filename) { File.basename(backup_file.path) } before do + allow(Gitlab::BackupLogger).to receive(:info) + allow(subject).to receive(:tar_file).and_return(backup_filename) + stub_backup_setting( upload: { connection: { provider: 'AWS', - aws_access_key_id: 'AWS_ACCESS_KEY_ID', - aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY' + aws_access_key_id: 'id', + aws_secret_access_key: 'secret' }, remote_directory: 'directory', - multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, - encryption: encryption, - encryption_key: encryption_key, - storage_options: storage_options, + multipart_chunk_size: 104857600, + encryption: nil, + encryption_key: nil, storage_class: nil } ) + Fog.mock! + + # the Fog mock only knows about directories we create explicitly + connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang end - context 'with SSE-S3 without using storage_options' do - let(:encryption) { 'AES256' } + context 'skipped upload' do + let(:backup_information) do + { + backup_created_at: Time.zone.parse('2019-01-01'), + gitlab_version: '12.3', + skipped: ['remote'] + } + end - it 'sets encryption attributes' do + it 'informs the user' do + stub_env('SKIP', 'remote') subject.create # rubocop:disable Rails/SaveBang - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with AES256)') + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... [SKIPPED]') end end - context 'with SSE-C (customer-provided keys) options' do - let(:encryption) { 'AES256' } - let(:encryption_key) { SecureRandom.hex } + context 'target path' do + it 'uses the tar filename by default' do + expect_any_instance_of(Fog::Collection).to receive(:create) + .with(hash_including(key: backup_filename, public: false)) + .and_call_original - it 'sets encryption attributes' do subject.create # rubocop:disable Rails/SaveBang + end + + it 'adds the DIRECTORY environment variable if present' do + stub_env('DIRECTORY', 'daily') + + expect_any_instance_of(Fog::Collection).to receive(:create) + .with(hash_including(key: "daily/#{backup_filename}", public: false)) + .and_call_original - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with AES256)') + subject.create # rubocop:disable Rails/SaveBang end end - context 'with SSE-KMS options' do - let(:storage_options) do - { - server_side_encryption: 'aws:kms', - server_side_encryption_kms_key_id: 'arn:aws:kms:12345' - } + context 'with AWS with server side encryption' do + let(:connection) { ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) } + let(:encryption_key) { nil } + let(:encryption) { nil } + let(:storage_options) { nil } + + before do + stub_backup_setting( + upload: { + connection: { + provider: 'AWS', + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY' + }, + remote_directory: 'directory', + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, + encryption: encryption, + encryption_key: encryption_key, + storage_options: storage_options, + storage_class: nil + } + ) + + connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang end - it 'sets encryption attributes' do - subject.create # rubocop:disable Rails/SaveBang + context 'with SSE-S3 without using storage_options' do + let(:encryption) { 'AES256' } - expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with aws:kms)') + it 'sets encryption attributes' do + subject.create # rubocop:disable Rails/SaveBang + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with AES256)') + end + end + + context 'with SSE-C (customer-provided keys) options' do + let(:encryption) { 'AES256' } + let(:encryption_key) { SecureRandom.hex } + + it 'sets encryption attributes' do + subject.create # rubocop:disable Rails/SaveBang + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with AES256)') + end + end + + context 'with SSE-KMS options' do + let(:storage_options) do + { + server_side_encryption: 'aws:kms', + server_side_encryption_kms_key_id: 'arn:aws:kms:12345' + } + end + + it 'sets encryption attributes' do + subject.create # rubocop:disable Rails/SaveBang + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Uploading backup archive to remote storage directory ... done (encrypted with aws:kms)') + end end end - end - context 'with Google provider' do - before do - stub_backup_setting( - upload: { - connection: { - provider: 'Google', - google_storage_access_key_id: 'test-access-id', - google_storage_secret_access_key: 'secret' - }, - remote_directory: 'directory', - multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, - encryption: nil, - encryption_key: nil, - storage_class: nil - } - ) + context 'with Google provider' do + before do + stub_backup_setting( + upload: { + connection: { + provider: 'Google', + google_storage_access_key_id: 'test-access-id', + google_storage_secret_access_key: 'secret' + }, + remote_directory: 'directory', + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, + encryption: nil, + encryption_key: nil, + storage_class: nil + } + ) + + connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) + connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang + end - connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) - connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) # rubocop:disable Rails/SaveBang + it 'does not attempt to set ACL' do + expect_any_instance_of(Fog::Collection).to receive(:create) + .with(hash_excluding(public: false)) + .and_call_original + + subject.create # rubocop:disable Rails/SaveBang + end end - it 'does not attempt to set ACL' do - expect_any_instance_of(Fog::Collection).to receive(:create) - .with(hash_excluding(public: false)) - .and_call_original + context 'with AzureRM provider' do + before do + stub_backup_setting( + upload: { + connection: { + provider: 'AzureRM', + azure_storage_account_name: 'test-access-id', + azure_storage_access_key: 'secret' + }, + remote_directory: 'directory', + multipart_chunk_size: nil, + encryption: nil, + encryption_key: nil, + storage_class: nil + } + ) + end - subject.create # rubocop:disable Rails/SaveBang + it 'loads the provider' do + expect { subject.create }.not_to raise_error # rubocop:disable Rails/SaveBang + end end end + end - context 'with AzureRM provider' do - before do - stub_backup_setting( - upload: { - connection: { - provider: 'AzureRM', - azure_storage_account_name: 'test-access-id', - azure_storage_access_key: 'secret' - }, - remote_directory: 'directory', - multipart_chunk_size: nil, - encryption: nil, - encryption_key: nil, - storage_class: nil - } - ) - end + context 'tar skipped' do + before do + stub_env('SKIP', 'tar') + end - it 'loads the provider' do - expect { subject.create }.not_to raise_error # rubocop:disable Rails/SaveBang + after do + FileUtils.rm_rf(Dir.glob(File.join(Gitlab.config.backup.path, '*')), secure: true) + end + + it 'creates a non-tarred backup' do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang end + + expect(Kernel).not_to have_received(:system).with(*pack_tar_cmdline) + expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include( + backup_created_at: backup_time.localtime, + db_version: be_a(String), + gitlab_version: Gitlab::VERSION, + installation_type: Gitlab::INSTALLATION_TYPE, + skipped: 'tar', + tar_version: be_a(String) + ) end end @@ -598,14 +604,21 @@ RSpec.describe Backup::Manager do let(:incremental_env) { 'true' } let(:gitlab_version) { Gitlab::VERSION } let(:backup_id) { "1546300800_2019_01_01_#{gitlab_version}" } - let(:tar_file) { "#{backup_id}_gitlab_backup.tar" } + let(:unpack_tar_file) { "#{full_backup_id}_gitlab_backup.tar" } + let(:unpack_tar_cmdline) { ['tar', '-xf', unpack_tar_file] } let(:backup_information) do { - backup_created_at: Time.zone.parse('2019-01-01'), + backup_created_at: Time.zone.parse('2018-01-01'), gitlab_version: gitlab_version } end + before do + allow(YAML).to receive(:load_file).and_call_original + allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')) + .and_return(backup_information) + end + context 'when there are no backup files in the directory' do before do allow(Dir).to receive(:glob).and_return([]) @@ -663,7 +676,6 @@ RSpec.describe Backup::Manager do context 'when BACKUP variable is set to a correct file' do let(:backup_id) { '1451606400_2016_01_01_1.2.3' } - let(:tar_cmdline) { %w{tar -xf 1451606400_2016_01_01_1.2.3_gitlab_backup.tar} } before do allow(Gitlab::BackupLogger).to receive(:info) @@ -678,15 +690,18 @@ RSpec.describe Backup::Manager do stub_env('BACKUP', '/ignored/path/1451606400_2016_01_01_1.2.3') end - it 'unpacks the file' do - subject.create # rubocop:disable Rails/SaveBang + it 'unpacks and packs the backup' do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang + end - expect(Kernel).to have_received(:system).with(*tar_cmdline) + expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) end - context 'tar fails' do + context 'untar fails' do before do - expect(Kernel).to receive(:system).with(*tar_cmdline).and_return(false) + expect(Kernel).to receive(:system).with(*unpack_tar_cmdline).and_return(false) end it 'logs a failure' do @@ -698,6 +713,20 @@ RSpec.describe Backup::Manager do end end + context 'tar fails' do + before do + expect(Kernel).to receive(:system).with(*pack_tar_cmdline).and_return(false) + end + + it 'logs a failure' do + expect do + subject.create # rubocop:disable Rails/SaveBang + end.to raise_error(Backup::Error, 'Backup failed') + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") + end + end + context 'on version mismatch' do let(:backup_information) do { @@ -714,21 +743,138 @@ RSpec.describe Backup::Manager do end end + context 'when PREVIOUS_BACKUP variable is set to a non-existing file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(false) + + stub_env('PREVIOUS_BACKUP', 'wrong') + end + + it 'fails the operation and prints an error' do + expect { subject.create }.to raise_error SystemExit # rubocop:disable Rails/SaveBang + expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar') + expect(progress).to have_received(:puts) + .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist')) + end + end + + context 'when PREVIOUS_BACKUP variable is set to a correct file' do + let(:full_backup_id) { 'some_previous_backup' } + + before do + allow(Gitlab::BackupLogger).to receive(:info) + allow(Dir).to receive(:glob).and_return( + [ + 'some_previous_backup_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).with('some_previous_backup_gitlab_backup.tar').and_return(true) + allow(Kernel).to receive(:system).and_return(true) + + stub_env('PREVIOUS_BACKUP', '/ignored/path/some_previous_backup') + end + + it 'unpacks and packs the backup' do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang + end + + expect(Kernel).to have_received(:system).with(*unpack_tar_cmdline) + expect(Kernel).to have_received(:system).with(*pack_tar_cmdline) + end + + context 'untar fails' do + before do + expect(Kernel).to receive(:system).with(*unpack_tar_cmdline).and_return(false) + end + + it 'logs a failure' do + expect do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang + end + end.to raise_error(SystemExit) + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: 'Unpacking backup failed') + end + end + + context 'tar fails' do + before do + expect(Kernel).to receive(:system).with(*pack_tar_cmdline).and_return(false) + end + + it 'logs a failure' do + expect do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang + end + end.to raise_error(Backup::Error, 'Backup failed') + + expect(Gitlab::BackupLogger).to have_received(:info).with(message: "Creating archive #{pack_tar_file} failed") + end + end + + context 'on version mismatch' do + let(:backup_information) do + { + backup_created_at: Time.zone.parse('2018-01-01'), + gitlab_version: "not #{gitlab_version}" + } + end + + it 'stops the process' do + expect { subject.create }.to raise_error SystemExit # rubocop:disable Rails/SaveBang + expect(progress).to have_received(:puts) + .with(a_string_matching('GitLab version mismatch')) + end + end + end + context 'when there is a non-tarred backup in the directory' do + let(:full_backup_id) { "1514764800_2018_01_01_#{Gitlab::VERSION}" } + let(:backup_information) do + { + backup_created_at: Time.zone.parse('2018-01-01'), + gitlab_version: gitlab_version, + skipped: 'tar' + } + end + before do allow(Dir).to receive(:glob).and_return( [ 'backup_information.yml' ] ) - allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:exist?).with(File.join(Gitlab.config.backup.path, 'backup_information.yml')).and_return(true) + stub_env('SKIP', 'something') end - it 'selects the non-tarred backup to restore from' do - subject.create # rubocop:disable Rails/SaveBang + after do + FileUtils.rm(File.join(Gitlab.config.backup.path, 'backup_information.yml'), force: true) + end + + it 'updates the non-tarred backup' do + travel_to(backup_time) do + subject.create # rubocop:disable Rails/SaveBang + end expect(progress).to have_received(:puts) .with(a_string_matching('Non tarred backup found ')) + expect(progress).to have_received(:puts) + .with(a_string_matching("Backup #{backup_id} is done")) + expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include( + backup_created_at: backup_time, + full_backup_id: full_backup_id, + gitlab_version: Gitlab::VERSION, + skipped: 'something,tar' + ) end context 'on version mismatch' do diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index c6f611e727c..1581e4793e3 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -5,13 +5,15 @@ require 'spec_helper' RSpec.describe Backup::Repositories do let(:progress) { spy(:stdout) } let(:strategy) { spy(:strategy) } + let(:storages) { [] } let(:destination) { 'repositories' } let(:backup_id) { 'backup_id' } subject do described_class.new( progress, - strategy: strategy + strategy: strategy, + storages: storages ) end @@ -67,17 +69,50 @@ RSpec.describe Backup::Repositories do end.count create_list(:project, 2, :repository) + create_list(:snippet, 2, :repository) expect do subject.dump(destination, backup_id) end.not_to exceed_query_limit(control_count) end + + describe 'storages' do + let(:storages) { %w{default} } + + let_it_be(:project) { create(:project, :repository) } + + before do + stub_storage_settings('test_second_storage' => { + 'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address, + 'path' => TestEnv::SECOND_STORAGE_PATH + }) + end + + it 'calls enqueue for all repositories on the specified storage', :aggregate_failures do + excluded_project = create(:project, :repository, repository_storage: 'test_second_storage') + excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) + excluded_project_snippet.track_snippet_repository('test_second_storage') + excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) + excluded_personal_snippet.track_snippet_repository('test_second_storage') + + subject.dump(destination, backup_id) + + expect(strategy).to have_received(:start).with(:create, destination, backup_id: backup_id) + expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) + expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:finish!) + end + end end describe '#restore' do - let_it_be(:project) { create(:project) } - let_it_be(:personal_snippet) { create(:personal_snippet, author: project.first_owner) } - let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.first_owner) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: project.first_owner) } + let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project, author: project.first_owner) } it 'calls enqueue for each repository type', :aggregate_failures do subject.restore(destination) @@ -116,9 +151,6 @@ RSpec.describe Backup::Repositories do context 'cleanup snippets' do before do - create(:snippet_repository, snippet: personal_snippet) - create(:snippet_repository, snippet: project_snippet) - 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 @@ -146,5 +178,35 @@ RSpec.describe Backup::Repositories do expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false end end + + context 'storages' do + let(:storages) { %w{default} } + + before do + stub_storage_settings('test_second_storage' => { + 'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address, + 'path' => TestEnv::SECOND_STORAGE_PATH + }) + end + + it 'calls enqueue for all repositories on the specified storage', :aggregate_failures do + excluded_project = create(:project, :repository, repository_storage: 'test_second_storage') + excluded_project_snippet = create(:project_snippet, :repository, project: excluded_project) + excluded_project_snippet.track_snippet_repository('test_second_storage') + excluded_personal_snippet = create(:personal_snippet, :repository, author: excluded_project.first_owner) + excluded_personal_snippet.track_snippet_repository('test_second_storage') + + subject.restore(destination) + + expect(strategy).to have_received(:start).with(:restore, destination) + expect(strategy).not_to have_received(:enqueue).with(excluded_project, Gitlab::GlRepository::PROJECT) + expect(strategy).not_to have_received(:enqueue).with(excluded_project_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).not_to have_received(:enqueue).with(excluded_personal_snippet, Gitlab::GlRepository::SNIPPET) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI) + expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN) + expect(strategy).to have_received(:finish!) + end + end end end diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb index 9f5aa558f24..5b32be0ea62 100644 --- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb +++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb @@ -23,6 +23,11 @@ RSpec.describe Banzai::Filter::ImageLazyLoadFilter do expect(doc.at_css('img')['class']).to eq 'test lazy' end + it 'adds a async decoding attribute' do + doc = filter(image_with_class('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg', 'test')) + expect(doc.at_css('img')['decoding']).to eq 'async' + end + it 'transforms the image src to a data-src' do doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb index c493cb77c98..c6f0e592cdf 100644 --- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb @@ -15,6 +15,14 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do let(:issue_path) { "/#{issue.project.namespace.path}/#{issue.project.path}/-/issues/#{issue.iid}" } let(:issue_url) { "http://#{Gitlab.config.gitlab.host}#{issue_path}" } + shared_examples 'a reference with issue type information' do + it 'contains issue-type as a data attribute' do + doc = reference_filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('data-issue-type')).to eq('issue') + end + end + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end @@ -44,6 +52,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -158,6 +168,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -208,6 +220,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -258,6 +272,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) .with(project2, issue.iid) @@ -307,6 +323,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -342,6 +360,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") @@ -371,6 +391,8 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do it_behaves_like 'a reference containing an element node' + it_behaves_like 'a reference with issue type information' + it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") diff --git a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb new file mode 100644 index 00000000000..09d2919c6c4 --- /dev/null +++ b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do + let_it_be(:project) { create(:project) } + + describe '.filters' do + it 'contains required filters' do + expect(described_class.filters).to eq( + [ + *Banzai::Pipeline::PlainMarkdownPipeline.filters, + *Banzai::Pipeline::GfmPipeline.reference_filters, + Banzai::Filter::EmojiFilter, + Banzai::Filter::SanitizationFilter, + Banzai::Filter::ExternalLinkFilter, + Banzai::Filter::ImageLinkFilter + ] + ) + end + end + + describe '.to_html' do + subject(:output) { described_class.to_html(markdown, project: project) } + + context 'when markdown contains font style transformations' do + let(:markdown) { '**bold** _italic_ `code`' } + + it { is_expected.to eq('<p><strong>bold</strong> <em>italic</em> <code>code</code></p>') } + end + + context 'when markdown contains banned HTML tags' do + let(:markdown) { '<div>div</div><h1>h1</h1>' } + + it 'filters out banned tags' do + is_expected.to eq(' div h1 ') + end + end + + context 'when markdown contains links' do + let(:markdown) { '[GitLab](https://gitlab.com)' } + + it do + is_expected.to eq( + %q(<p><a href="https://gitlab.com" rel="nofollow noreferrer noopener" target="_blank">GitLab</a></p>) + ) + end + end + + context 'when markdown contains images' do + let(:markdown) { '![Name](/path/to/image.png)' } + + it 'replaces image with a link to the image' do + # rubocop:disable Layout/LineLength + is_expected.to eq( + '<p><a class="with-attachment-icon" href="/path/to/image.png" target="_blank" rel="noopener noreferrer">Name</a></p>' + ) + # rubocop:enable Layout/LineLength + end + end + + context 'when markdown contains emojis' do + let(:markdown) { ':+1:👍' } + + it { is_expected.to eq('<p>👍👍</p>') } + end + + context 'when markdown contains a reference to an issue' do + let!(:issue) { create(:issue, project: project) } + let(:markdown) { "issue ##{issue.iid}" } + + it 'contains a link to the issue' do + is_expected.to match(%r(<p>issue <a href="[\w/]+-/issues/#{issue.iid}".*>##{issue.iid}</a></p>)) + end + end + + context 'when markdown contains a reference to a merge request' do + let!(:mr) { create(:merge_request, source_project: project, target_project: project) } + let(:markdown) { "MR !#{mr.iid}" } + + it 'contains a link to the merge request' do + is_expected.to match(%r(<p>MR <a href="[\w/]+-/merge_requests/#{mr.iid}".*>!#{mr.iid}</a></p>)) + end + end + end +end diff --git a/spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb new file mode 100644 index 00000000000..2c167cc485c --- /dev/null +++ b/spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'zlib' + +RSpec.describe BulkImports::Common::Extractors::JsonExtractor do + subject { described_class.new(relation: 'self') } + + let_it_be(:tmpdir) { Dir.mktmpdir } + let_it_be(:import) { create(:bulk_import) } + let_it_be(:config) { create(:bulk_import_configuration, bulk_import: import) } + let_it_be(:entity) { create(:bulk_import_entity, bulk_import: import) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + before do + allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original + + subject.instance_variable_set(:@tmpdir, tmpdir) + end + + after(:all) do + FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) + end + + describe '#extract' do + before do + Zlib::GzipWriter.open(File.join(tmpdir, 'self.json.gz')) do |gz| + gz.write '{"name": "Name","description": "Description","avatar":{"url":null}}' + end + + expect(BulkImports::FileDownloadService).to receive(:new) + .with( + configuration: context.configuration, + relative_url: entity.relation_download_url_path('self'), + tmpdir: tmpdir, + filename: 'self.json.gz') + .and_return(instance_double(BulkImports::FileDownloadService, execute: nil)) + end + + it 'returns ExtractedData', :aggregate_failures do + extracted_data = subject.extract(context) + + expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData) + expect(extracted_data.data).to contain_exactly( + { 'name' => 'Name', 'description' => 'Description', 'avatar' => { 'url' => nil } } + ) + end + end + + describe '#remove_tmpdir' do + it 'removes tmp dir' do + expect(FileUtils).to receive(:remove_entry).with(tmpdir).once + + subject.remove_tmpdir + end + end +end diff --git a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb index d6e19a5fc85..8b63234ba50 100644 --- a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb +++ b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true require 'spec_helper' +require 'zlib' RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do let_it_be(:tmpdir) { Dir.mktmpdir } - let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/labels.ndjson.gz' } let_it_be(:import) { create(:bulk_import) } let_it_be(:config) { create(:bulk_import_configuration, bulk_import: import) } let_it_be(:entity) { create(:bulk_import_entity, bulk_import: import) } @@ -25,21 +25,30 @@ RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do describe '#extract' do before do - FileUtils.copy_file(filepath, File.join(tmpdir, 'labels.ndjson.gz')) - - allow_next_instance_of(BulkImports::FileDownloadService) do |service| - allow(service).to receive(:execute) + Zlib::GzipWriter.open(File.join(tmpdir, 'labels.ndjson.gz')) do |gz| + gz.write [ + '{"title": "Title 1","description": "Description 1","type":"GroupLabel"}', + '{"title": "Title 2","description": "Description 2","type":"GroupLabel"}' + ].join("\n") end + + expect(BulkImports::FileDownloadService).to receive(:new) + .with( + configuration: context.configuration, + relative_url: entity.relation_download_url_path('labels'), + tmpdir: tmpdir, + filename: 'labels.ndjson.gz') + .and_return(instance_double(BulkImports::FileDownloadService, execute: nil)) end - it 'returns ExtractedData' do + it 'returns ExtractedData', :aggregate_failures do extracted_data = subject.extract(context) - label = extracted_data.data.first.first expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData) - expect(label['title']).to include('Label') - expect(label['description']).to include('Label') - expect(label['type']).to eq('GroupLabel') + expect(extracted_data.data.to_a).to contain_exactly( + [{ "title" => "Title 1", "description" => "Description 1", "type" => "GroupLabel" }, 0], + [{ "title" => "Title 2", "description" => "Description 2", "type" => "GroupLabel" }, 1] + ) end end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_attributes_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_attributes_pipeline_spec.rb new file mode 100644 index 00000000000..7ac417afa0b --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/group_attributes_pipeline_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::GroupAttributesPipeline do + subject(:pipeline) { described_class.new(context) } + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) { create(:bulk_import_entity, :group_entity, group: group, bulk_import: bulk_import) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + let(:group_attributes) do + { + 'id' => 1, + 'name' => 'Group name', + 'path' => 'group-path', + 'description' => 'description', + 'avatar' => { + 'url' => nil + }, + 'membership_lock' => true, + 'traversal_ids' => [ + 2 + ] + } + end + + describe '#run' do + before do + allow_next_instance_of(BulkImports::Common::Extractors::JsonExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return( + BulkImports::Pipeline::ExtractedData.new(data: group_attributes) + ) + end + end + + it 'imports allowed group attributes' do + expect(Groups::UpdateService).to receive(:new).with(group, user, { membership_lock: true }).and_call_original + + pipeline.run + + expect(group).to have_attributes(membership_lock: true) + end + end + + describe '#transform' do + it 'fetches only allowed attributes and symbolize keys' do + transformed_data = pipeline.transform(context, group_attributes) + + expect(transformed_data).to eq({ membership_lock: true }) + end + + context 'when there is no data to transform' do + let(:group_attributes) { nil } + + it do + transformed_data = pipeline.transform(context, group_attributes) + + expect(transformed_data).to eq(nil) + end + end + end + + describe '#after_run' do + it 'calls extractor#remove_tmpdir' do + expect_next_instance_of(BulkImports::Common::Extractors::JsonExtractor) do |extractor| + expect(extractor).to receive(:remove_tmpdir) + end + + pipeline.after_run(nil) + end + end + + describe '.relation' do + it { expect(described_class.relation).to eq('self') } + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb index 39e782dc093..441a34b0c74 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -23,7 +23,7 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do let(:group_data) do { - 'name' => 'source_name', + 'name' => 'Source Group Name', 'full_path' => 'source/full/path', 'visibility' => 'private', 'project_creation_level' => 'developer', diff --git a/spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb new file mode 100644 index 00000000000..90b63453b88 --- /dev/null +++ b/spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Groups::Pipelines::NamespaceSettingsPipeline do + subject(:pipeline) { described_class.new(context) } + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, namespace_settings: create(:namespace_settings) ) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) { create(:bulk_import_entity, :group_entity, group: group, bulk_import: bulk_import) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + before do + group.add_owner(user) + end + + describe '#run' do + before do + allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| + namespace_settings_attributes = { + 'namespace_id' => 22, + 'prevent_forking_outside_group' => true, + 'prevent_sharing_groups_outside_hierarchy' => true + } + allow(extractor).to receive(:extract).and_return( + BulkImports::Pipeline::ExtractedData.new(data: [[namespace_settings_attributes, 0]]) + ) + end + end + + it 'imports allowed namespace settings attributes' do + expect(Groups::UpdateService).to receive(:new).with( + group, user, { prevent_sharing_groups_outside_hierarchy: true } + ).and_call_original + + pipeline.run + + expect(group.namespace_settings).to have_attributes(prevent_sharing_groups_outside_hierarchy: true) + end + end + + describe '#transform' do + it 'fetches only allowed attributes and symbolize keys' do + all_model_attributes = NamespaceSetting.new.attributes + + transformed_data = pipeline.transform(context, [all_model_attributes, 0]) + + expect(transformed_data.keys).to match_array([:prevent_sharing_groups_outside_hierarchy]) + end + + context 'when there is no data to transform' do + it do + namespace_settings_attributes = nil + + transformed_data = pipeline.transform(context, namespace_settings_attributes) + + expect(transformed_data).to eq(nil) + end + end + end + + describe '#after_run' do + it 'calls extractor#remove_tmpdir' do + expect_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| + expect(extractor).to receive(:remove_tmpdir) + end + + context = instance_double(BulkImports::Pipeline::Context) + + pipeline.after_run(context) + end + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index e4a41428dd2..6949ac59948 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group, path: 'group') } - let_it_be(:parent) { create(:group, name: 'imported-group', path: 'imported-group') } + let_it_be(:parent) { create(:group, name: 'Imported Group', path: 'imported-group') } let_it_be(:parent_entity) { create(:bulk_import_entity, destination_namespace: parent.full_path, group: parent) } let_it_be(:tracker) { create(:bulk_import_tracker, entity: parent_entity) } let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } @@ -14,8 +14,8 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do let(:extracted_data) do BulkImports::Pipeline::ExtractedData.new(data: { - 'name' => 'subgroup', - 'full_path' => 'parent/subgroup' + 'path' => 'sub-group', + 'full_path' => 'parent/sub-group' }) end @@ -33,9 +33,9 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do subgroup_entity = BulkImports::Entity.last - expect(subgroup_entity.source_full_path).to eq 'parent/subgroup' + expect(subgroup_entity.source_full_path).to eq 'parent/sub-group' expect(subgroup_entity.destination_namespace).to eq 'imported-group' - expect(subgroup_entity.destination_name).to eq 'subgroup' + expect(subgroup_entity.destination_name).to eq 'sub-group' expect(subgroup_entity.parent_id).to eq parent_entity.id end end @@ -51,9 +51,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do destination_namespace: parent_entity.group.full_path, parent_id: parent_entity.id } - expect { subject.load(context, data) }.to change(BulkImports::Entity, :count).by(1) - subgroup_entity = BulkImports::Entity.last expect(subgroup_entity.source_full_path).to eq 'parent/subgroup' diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb index 645dee4a6f1..8ce25ff87d7 100644 --- a/spec/lib/bulk_imports/groups/stage_spec.rb +++ b/spec/lib/bulk_imports/groups/stage_spec.rb @@ -11,7 +11,9 @@ RSpec.describe BulkImports::Groups::Stage do let(:pipelines) do [ [0, BulkImports::Groups::Pipelines::GroupPipeline], + [1, BulkImports::Groups::Pipelines::GroupAttributesPipeline], [1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline], + [1, BulkImports::Groups::Pipelines::NamespaceSettingsPipeline], [1, BulkImports::Common::Pipelines::MembersPipeline], [1, BulkImports::Common::Pipelines::LabelsPipeline], [1, BulkImports::Common::Pipelines::MilestonesPipeline], diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb index 75d8c15088a..c42ca9bef3b 100644 --- a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb +++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb @@ -6,7 +6,6 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do describe '#transform' do let_it_be(:user) { create(:user) } let_it_be(:parent) { create(:group) } - let_it_be(:group) { create(:group, name: 'My Source Group', parent: parent) } let_it_be(:bulk_import) { create(:bulk_import, user: user) } let_it_be(:entity) do @@ -14,7 +13,7 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do :bulk_import_entity, bulk_import: bulk_import, source_full_path: 'source/full/path', - destination_name: group.name, + destination_name: 'destination-name-path', destination_namespace: parent.full_path ) end @@ -24,7 +23,8 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do let(:data) do { - 'name' => 'source_name', + 'name' => 'Source Group Name', + 'path' => 'source-group-path', 'full_path' => 'source/full/path', 'visibility' => 'private', 'project_creation_level' => 'developer', @@ -34,23 +34,27 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do subject { described_class.new } - it 'transforms name to destination name' do - transformed_data = subject.transform(context, data) + it 'returns original data with some keys transformed' do + transformed_data = subject.transform(context, { 'name' => 'Name', 'description' => 'Description' }) - expect(transformed_data['name']).not_to eq('source_name') - expect(transformed_data['name']).to eq(group.name) + expect(transformed_data).to eq({ + 'name' => 'Name', + 'description' => 'Description', + 'parent_id' => parent.id, + 'path' => 'destination-name-path' + }) end - it 'removes full path' do + it 'transforms path from destination_name' do transformed_data = subject.transform(context, data) - expect(transformed_data).not_to have_key('full_path') + expect(transformed_data['path']).to eq(entity.destination_name) end - it 'transforms path to parameterized name' do + it 'removes full path' do transformed_data = subject.transform(context, data) - expect(transformed_data['path']).to eq(group.name.parameterize) + expect(transformed_data).not_to have_key('full_path') end it 'transforms visibility level' do diff --git a/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb index 2f97a5721e7..6450d90ec0f 100644 --- a/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb +++ b/spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb @@ -9,14 +9,14 @@ RSpec.describe BulkImports::Groups::Transformers::SubgroupToEntityTransformer do parent_entity = instance_double(BulkImports::Entity, group: parent, id: 1) context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity) subgroup_data = { - "name" => "subgroup", - "full_path" => "parent/subgroup" + "path" => "sub-group", + "full_path" => "parent/sub-group" } expect(subject.transform(context, subgroup_data)).to eq( source_type: :group_entity, - source_full_path: "parent/subgroup", - destination_name: "subgroup", + source_full_path: "parent/sub-group", + destination_name: "sub-group", destination_namespace: parent.full_path, parent_id: 1 ) diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb index 8ea6ceb7619..25edc9feea8 100644 --- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb +++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb @@ -29,7 +29,7 @@ RSpec.describe BulkImports::NdjsonPipeline do subject { NdjsonPipelineClass.new(group, user) } it 'marks pipeline as ndjson' do - expect(NdjsonPipelineClass.ndjson_pipeline?).to eq(true) + expect(NdjsonPipelineClass.file_extraction_pipeline?).to eq(true) end describe '#deep_transform_relation!' do diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb index 48c265d6118..e4ecf99dab0 100644 --- a/spec/lib/bulk_imports/pipeline_spec.rb +++ b/spec/lib/bulk_imports/pipeline_spec.rb @@ -63,7 +63,7 @@ RSpec.describe BulkImports::Pipeline do BulkImports::MyPipeline.transformer(klass, options) BulkImports::MyPipeline.loader(klass, options) BulkImports::MyPipeline.abort_on_failure! - BulkImports::MyPipeline.ndjson_pipeline! + BulkImports::MyPipeline.file_extraction_pipeline! expect(BulkImports::MyPipeline.get_extractor).to eq({ klass: klass, options: options }) @@ -75,7 +75,7 @@ RSpec.describe BulkImports::Pipeline do expect(BulkImports::MyPipeline.get_loader).to eq({ klass: klass, options: options }) expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true) - expect(BulkImports::MyPipeline.ndjson_pipeline?).to eq(true) + expect(BulkImports::MyPipeline.file_extraction_pipeline?).to eq(true) end end end diff --git a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb index df7ff5b8062..aa9c7486c27 100644 --- a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb @@ -23,7 +23,6 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do 'merge_requests_ff_only_enabled' => true, 'issues_template' => 'test', 'shared_runners_enabled' => true, - 'build_coverage_regex' => 'build_coverage_regex', 'build_allow_git_fetch' => true, 'build_timeout' => 3600, 'pending_delete' => false, @@ -177,4 +176,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do end end end + + describe '.relation' do + it { expect(described_class.relation).to eq('self') } + end end diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb new file mode 100644 index 00000000000..2279e66720e --- /dev/null +++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) do + create( + :bulk_import_entity, + :project_entity, + project: project, + bulk_import: bulk_import, + source_full_path: 'source/full/path', + destination_name: 'My Destination Project', + destination_namespace: group.full_path + ) + end + + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + let(:attributes) { {} } + let(:release) do + { + 'tag' => '1.1', + 'name' => 'release 1.1', + 'description' => 'Release notes', + 'created_at' => '2019-12-26T10:17:14.621Z', + 'updated_at' => '2019-12-26T10:17:14.621Z', + 'released_at' => '2019-12-26T10:17:14.615Z', + 'sha' => '901de3a8bd5573f4a049b1457d28bc1592ba6bf9' + }.merge(attributes) + end + + subject(:pipeline) { described_class.new(context) } + + describe '#run' do + before do + group.add_owner(user) + with_index = [release, 0] + + allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [with_index])) + end + + pipeline.run + end + + it 'imports release into destination project' do + expect(project.releases.count).to eq(1) + + imported_release = project.releases.last + + aggregate_failures do + expect(imported_release.tag).to eq(release['tag']) + expect(imported_release.name).to eq(release['name']) + expect(imported_release.description).to eq(release['description']) + expect(imported_release.created_at.to_s).to eq('2019-12-26 10:17:14 UTC') + expect(imported_release.updated_at.to_s).to eq('2019-12-26 10:17:14 UTC') + expect(imported_release.released_at.to_s).to eq('2019-12-26 10:17:14 UTC') + expect(imported_release.sha).to eq(release['sha']) + end + end + + context 'links' do + let(:link) do + { + 'url' => 'http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download', + 'name' => 'release-1.1.dmg', + 'created_at' => '2019-12-26T10:17:14.621Z', + 'updated_at' => '2019-12-26T10:17:14.621Z' + } + end + + let(:attributes) {{ 'links' => [link] }} + + it 'restores release links' do + release_link = project.releases.last.links.first + + aggregate_failures do + expect(release_link.url).to eq(link['url']) + expect(release_link.name).to eq(link['name']) + expect(release_link.created_at.to_s).to eq('2019-12-26 10:17:14 UTC') + expect(release_link.updated_at.to_s).to eq('2019-12-26 10:17:14 UTC') + end + end + end + + context 'milestones' do + let(:milestone) do + { + 'iid' => 1, + 'state' => 'closed', + 'title' => 'test milestone', + 'description' => 'test milestone', + 'due_date' => '2016-06-14', + 'created_at' => '2016-06-14T15:02:04.415Z', + 'updated_at' => '2016-06-14T15:02:04.415Z' + } + end + + let(:attributes) {{ 'milestone_releases' => [{ 'milestone' => milestone }] }} + + it 'restores release milestone' do + release_milestone = project.releases.last.milestone_releases.first.milestone + + aggregate_failures do + expect(release_milestone.iid).to eq(milestone['iid']) + expect(release_milestone.state).to eq(milestone['state']) + expect(release_milestone.title).to eq(milestone['title']) + expect(release_milestone.description).to eq(milestone['description']) + expect(release_milestone.due_date.to_s).to eq('2016-06-14') + expect(release_milestone.created_at.to_s).to eq('2016-06-14 15:02:04 UTC') + expect(release_milestone.updated_at.to_s).to eq('2016-06-14 15:02:04 UTC') + end + end + end + end +end diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb index 9fce30f3a81..e81d9cc5fb4 100644 --- a/spec/lib/bulk_imports/projects/stage_spec.rb +++ b/spec/lib/bulk_imports/projects/stage_spec.rb @@ -20,10 +20,11 @@ RSpec.describe BulkImports::Projects::Stage do [4, BulkImports::Projects::Pipelines::MergeRequestsPipeline], [4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline], [4, BulkImports::Projects::Pipelines::ProtectedBranchesPipeline], - [4, BulkImports::Projects::Pipelines::CiPipelinesPipeline], [4, BulkImports::Projects::Pipelines::ProjectFeaturePipeline], [4, BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeline], [4, BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline], + [4, BulkImports::Projects::Pipelines::ReleasesPipeline], + [5, BulkImports::Projects::Pipelines::CiPipelinesPipeline], [5, BulkImports::Common::Pipelines::WikiPipeline], [5, BulkImports::Common::Pipelines::UploadsPipeline], [5, BulkImports::Common::Pipelines::LfsObjectsPipeline], diff --git a/spec/lib/constraints/feature_constrainer_spec.rb b/spec/lib/constraints/feature_constrainer_spec.rb deleted file mode 100644 index c98dc694186..00000000000 --- a/spec/lib/constraints/feature_constrainer_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Constraints::FeatureConstrainer do - describe '#matches' do - it 'calls Feature.enabled? with the correct arguments' do - gate = stub_feature_flag_gate("an object") - - expect(Feature).to receive(:enabled?) - .with(:feature_name, gate, default_enabled: true) - - described_class.new(:feature_name, gate, default_enabled: true).matches?(double('request')) - end - end -end diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index 39a594eba5c..f9e08df3399 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -199,69 +199,16 @@ RSpec.describe ContainerRegistry::Client do let(:redirect_location) { 'http://redirect?foo=bar&test=signature=' } it_behaves_like 'handling redirects' - - context 'with container_registry_follow_redirects_middleware disabled' do - before do - stub_feature_flags(container_registry_follow_redirects_middleware: false) - end - - it 'follows the redirect' do - expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original - expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original - expect(Faraday::Utils).to receive(:escape).with('test').and_call_original - expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original - - expect_new_faraday(times: 2) - expect(subject).to eq('Successfully redirected') - end - end end context 'with a redirect location with params ending with %3D' do let(:redirect_location) { 'http://redirect?foo=bar&test=signature%3D' } it_behaves_like 'handling redirects' - - context 'with container_registry_follow_redirects_middleware disabled' do - before do - stub_feature_flags(container_registry_follow_redirects_middleware: false) - end - - it 'follows the redirect' do - expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original - expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original - expect(Faraday::Utils).to receive(:escape).with('test').and_call_original - expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original - - expect_new_faraday(times: 2) - expect(subject).to eq('Successfully redirected') - end - end end end it_behaves_like 'handling timeouts' - - # TODO Remove this context along with the - # container_registry_follow_redirects_middleware feature flag - # See https://gitlab.com/gitlab-org/gitlab/-/issues/353291 - context 'faraday blob' do - subject { client.send(:faraday_blob) } - - it 'has a follow redirects middleware' do - expect(subject.builder.handlers).to include(::FaradayMiddleware::FollowRedirects) - end - - context 'with container_registry_follow_redirects_middleware is disabled' do - before do - stub_feature_flags(container_registry_follow_redirects_middleware: false) - end - - it 'has not a follow redirects middleware' do - expect(subject.builder.handlers).not_to include(::FaradayMiddleware::FollowRedirects) - end - end - end end describe '#upload_blob' do diff --git a/spec/lib/container_registry/migration_spec.rb b/spec/lib/container_registry/migration_spec.rb index 6c0fc94e27f..81dac354b8b 100644 --- a/spec/lib/container_registry/migration_spec.rb +++ b/spec/lib/container_registry/migration_spec.rb @@ -58,21 +58,25 @@ RSpec.describe ContainerRegistry::Migration do describe '.capacity' do subject { described_class.capacity } - where(:ff_1_enabled, :ff_10_enabled, :ff_25_enabled, :expected_result) do - false | false | false | 0 - true | false | false | 1 - true | true | false | 10 - true | true | true | 25 - false | true | false | 10 - false | true | true | 25 - false | false | true | 25 - true | false | true | 25 + where(:ff_1_enabled, :ff_2_enabled, :ff_5_enabled, :ff_10_enabled, :ff_25_enabled, :expected_result) do + false | false | false | false | false | 0 + true | false | false | false | false | 1 + false | true | false | false | false | 2 + true | true | false | false | false | 2 + false | false | true | false | false | 5 + true | true | true | false | false | 5 + false | false | false | true | false | 10 + true | true | true | true | false | 10 + false | false | false | false | true | 25 + true | true | true | true | true | 25 end with_them do before do stub_feature_flags( container_registry_migration_phase2_capacity_1: ff_1_enabled, + container_registry_migration_phase2_capacity_2: ff_2_enabled, + container_registry_migration_phase2_capacity_5: ff_5_enabled, container_registry_migration_phase2_capacity_10: ff_10_enabled, container_registry_migration_phase2_capacity_25: ff_25_enabled ) @@ -154,6 +158,30 @@ RSpec.describe ContainerRegistry::Migration do end end + describe '.pre_import_timeout' do + let(:value) { 10.minutes } + + before do + stub_application_setting(container_registry_pre_import_timeout: value) + end + + it 'returns the matching application_setting' do + expect(described_class.pre_import_timeout).to eq(value) + end + end + + describe '.import_timeout' do + let(:value) { 10.minutes } + + before do + stub_application_setting(container_registry_import_timeout: value) + end + + it 'returns the matching application_setting' do + expect(described_class.import_timeout).to eq(value) + end + end + describe '.target_plans' do subject { described_class.target_plans } @@ -185,4 +213,32 @@ RSpec.describe ContainerRegistry::Migration do it { is_expected.to eq(false) } end end + + describe '.enqueue_twice?' do + subject { described_class.enqueue_twice? } + + it { is_expected.to eq(true) } + + context 'feature flag disabled' do + before do + stub_feature_flags(container_registry_migration_phase2_enqueue_twice: false) + end + + it { is_expected.to eq(false) } + end + end + + describe '.enqueue_loop?' do + subject { described_class.enqueuer_loop? } + + it { is_expected.to eq(true) } + + context 'feature flag disabled' do + before do + stub_feature_flags(container_registry_migration_phase2_enqueuer_loop: false) + end + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/lib/error_tracking/collector/dsn_spec.rb b/spec/lib/error_tracking/collector/dsn_spec.rb index af55e6f20ec..3aa8719fe38 100644 --- a/spec/lib/error_tracking/collector/dsn_spec.rb +++ b/spec/lib/error_tracking/collector/dsn_spec.rb @@ -3,24 +3,32 @@ require 'spec_helper' RSpec.describe ErrorTracking::Collector::Dsn do - describe '.build__url' do - let(:gitlab) do - double( + describe '.build_url' do + let(:setting) do + { protocol: 'https', https: true, + port: 443, host: 'gitlab.example.com', - port: '4567', relative_url_root: nil - ) + } end subject { described_class.build_url('abcdef1234567890', 778) } - it 'returns a valid URL' do - allow(Settings).to receive(:gitlab).and_return(gitlab) - allow(Settings).to receive(:gitlab_on_standard_port?).and_return(false) + it 'returns a valid URL without explicit port' do + stub_config_setting(setting) - is_expected.to eq('https://abcdef1234567890@gitlab.example.com:4567/api/v4/error_tracking/collector/778') + is_expected.to eq('https://abcdef1234567890@gitlab.example.com/api/v4/error_tracking/collector/778') + end + + context 'with non-standard port' do + it 'returns a valid URL with custom port' do + setting[:port] = 4567 + stub_config_setting(setting) + + is_expected.to eq('https://abcdef1234567890@gitlab.example.com:4567/api/v4/error_tracking/collector/778') + end end end end diff --git a/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb b/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb index 4f00b1ec654..0e4bba04baa 100644 --- a/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb +++ b/spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb @@ -5,11 +5,11 @@ require 'spec_helper' RSpec.describe ErrorTracking::Collector::SentryAuthParser do describe '.parse' do let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=glet_1fedb514e17f4b958435093deb02048c" } } - let(:request) { double('request', headers: headers) } + let(:request) { instance_double('ActionDispatch::Request', headers: headers) } subject { described_class.parse(request) } - context 'empty headers' do + context 'with empty headers' do let(:headers) { {} } it 'fails with exception' do @@ -17,7 +17,7 @@ RSpec.describe ErrorTracking::Collector::SentryAuthParser do end end - context 'missing sentry_key' do + context 'with missing sentry_key' do let(:headers) { { 'X-Sentry-Auth' => "Sentry foo=bar" } } it 'returns empty value for public_key' do diff --git a/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb index 06f4b64ce93..e86ee67c129 100644 --- a/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb +++ b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb @@ -9,7 +9,7 @@ RSpec.describe ErrorTracking::Collector::SentryRequestParser do let(:body) { raw_event } let(:headers) { { 'Content-Encoding' => '' } } - let(:request) { double('request', headers: headers, body: StringIO.new(body)) } + let(:request) { instance_double('ActionDispatch::Request', headers: headers, body: StringIO.new(body)) } subject { described_class.parse(request) } @@ -22,7 +22,7 @@ RSpec.describe ErrorTracking::Collector::SentryRequestParser do end end - context 'empty body content' do + context 'with empty body content' do let(:body) { '' } it 'fails with exception' do @@ -30,7 +30,7 @@ RSpec.describe ErrorTracking::Collector::SentryRequestParser do end end - context 'plain text sentry request' do + context 'with plain text sentry request' do it_behaves_like 'valid parser' end end diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb index 2f95f8eeab7..3d11ad4c0d8 100644 --- a/spec/lib/feature/definition_spec.rb +++ b/spec/lib/feature/definition_spec.rb @@ -54,22 +54,10 @@ RSpec.describe Feature::Definition do describe '#valid_usage!' do context 'validates type' do it 'raises exception for invalid type' do - expect { definition.valid_usage!(type_in_code: :invalid, default_enabled_in_code: false) } + expect { definition.valid_usage!(type_in_code: :invalid) } .to raise_error(/The `type:` of `feature_flag` is not equal to config/) end end - - context 'validates default enabled' do - it 'raises exception for different value' do - expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) } - .to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/) - end - - it 'allows passing `default_enabled: :yaml`' do - expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: :yaml) } - .not_to raise_error - end - end end describe '.paths' do @@ -165,18 +153,14 @@ RSpec.describe Feature::Definition do using RSpec::Parameterized::TableSyntax let(:definition) do - Feature::Definition.new("development/enabled_feature_flag.yml", - name: :enabled_feature_flag, - type: 'development', - milestone: milestone, - default_enabled: false) + described_class.new("development/enabled_feature_flag.yml", + name: :enabled_feature_flag, + type: 'development', + milestone: milestone, + default_enabled: false) end before do - allow(Feature::Definition).to receive(:definitions) do - { definition.key => definition } - end - allow(Gitlab).to receive(:version_info).and_return(Gitlab::VersionInfo.parse(current_milestone)) end @@ -192,7 +176,7 @@ RSpec.describe Feature::Definition do end with_them do - it {is_expected.to be(expected)} + it { is_expected.to be(expected) } end end @@ -207,7 +191,7 @@ RSpec.describe Feature::Definition do it 'validates it usage' do expect(definition).to receive(:valid_usage!) - described_class.valid_usage!(:feature_flag, type: :development, default_enabled: false) + described_class.valid_usage!(:feature_flag, type: :development) end end @@ -221,7 +205,7 @@ RSpec.describe Feature::Definition do it 'raises exception' do expect do - described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false) + described_class.valid_usage!(:unknown_feature_flag, type: :development) end.to raise_error(/Missing feature definition for `unknown_feature_flag`/) end end @@ -235,7 +219,7 @@ RSpec.describe Feature::Definition do it 'does not raise exception' do expect do - described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false) + described_class.valid_usage!(:unknown_feature_flag, type: :development) end.not_to raise_error end end @@ -243,7 +227,7 @@ RSpec.describe Feature::Definition do context 'for an unknown type' do it 'raises exception' do expect do - described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type, default_enabled: false) + described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type) end.to raise_error(/Unknown feature flag type used: `unknown_type`/) end end @@ -254,23 +238,23 @@ RSpec.describe Feature::Definition do using RSpec::Parameterized::TableSyntax let(:definition) do - Feature::Definition.new("development/enabled_feature_flag.yml", - name: :enabled_feature_flag, - type: 'development', - milestone: milestone, - log_state_changes: log_state_change, - default_enabled: false) + described_class.new("development/enabled_feature_flag.yml", + name: :enabled_feature_flag, + type: 'development', + milestone: milestone, + log_state_changes: log_state_change, + default_enabled: false) end before do - allow(Feature::Definition).to receive(:definitions) do - { definition.key => definition } - end + stub_feature_flag_definition(:enabled_feature_flag, + milestone: milestone, + log_state_changes: log_state_change) allow(Gitlab).to receive(:version_info).and_return(Gitlab::VersionInfo.new(10, 0, 0)) end - subject { Feature::Definition.log_states?(key) } + subject { described_class.log_states?(key) } where(:ctx, :key, :milestone, :log_state_change, :expected) do 'When flag does not exist' | :no_flag | "0.0" | true | false @@ -286,10 +270,11 @@ RSpec.describe Feature::Definition do end describe '.default_enabled?' do - subject { described_class.default_enabled?(key) } + subject { described_class.default_enabled?(key, default_enabled_if_undefined: default_value) } context 'when feature flag exist' do let(:key) { definition.key } + let(:default_value) { nil } before do allow(described_class).to receive(:definitions) do @@ -319,21 +304,33 @@ RSpec.describe Feature::Definition do context 'when feature flag does not exist' do let(:key) { :unknown_feature_flag } - context 'when on dev or test environment' do - it 'raises an error' do - expect { subject }.to raise_error( - Feature::InvalidFeatureFlagError, - "The feature flag YAML definition for 'unknown_feature_flag' does not exist") + context 'when passing default value' do + let(:default_value) { false } + + it 'returns default value' do + expect(subject).to eq(default_value) end end - context 'when on production environment' do - before do - allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + context 'when default value is undefined' do + let(:default_value) { nil } + + context 'when on dev or test environment' do + it 'raises an error' do + expect { subject }.to raise_error( + Feature::InvalidFeatureFlagError, + "The feature flag YAML definition for 'unknown_feature_flag' does not exist") + end end - it 'returns false' do - expect(subject).to eq(false) + context 'when on production environment' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + it 'returns false' do + expect(subject).to eq(false) + end end end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 90c0684f8b7..6e32db09426 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Feature, stub_feature_flags: false do expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key) .and_return(feature) - expect(described_class.get(key)).to be(feature) + expect(described_class.get(key)).to eq(feature) end end @@ -67,7 +67,7 @@ RSpec.describe Feature, stub_feature_flags: false do expect(Gitlab::ProcessMemoryCache.cache_backend) .to receive(:fetch) .once - .with('flipper/v1/features', expires_in: 1.minute) + .with('flipper/v1/features', { expires_in: 1.minute }) .and_call_original 2.times do @@ -157,14 +157,65 @@ RSpec.describe Feature, stub_feature_flags: false do describe '.enabled?' do before do allow(Feature).to receive(:log_feature_flag_states?).and_return(false) + + stub_feature_flag_definition(:disabled_feature_flag) + stub_feature_flag_definition(:enabled_feature_flag, default_enabled: true) + end + + context 'when self-recursive' do + before do + allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block| + original.call(name) do |ff| + Feature.enabled?(name) + block.call(ff) + end + end + end + + it 'returns the default value' do + expect(described_class.enabled?(:enabled_feature_flag)).to eq true + end + + it 'detects self recursion' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(have_attributes(message: 'self recursion'), { stack: [:enabled_feature_flag] }) + + described_class.enabled?(:enabled_feature_flag) + end end - it 'returns false for undefined feature' do + context 'when deeply recursive' do + before do + allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block| + original.call(name) do |ff| + Feature.enabled?(:"deeper_#{name}", type: :undefined, default_enabled_if_undefined: true) + block.call(ff) + end + end + end + + it 'detects deep recursion' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(have_attributes(message: 'deep recursion'), stack: have_attributes(size: be > 10)) + + described_class.enabled?(:enabled_feature_flag) + end + end + + it 'returns false (and tracks / raises exception for dev) for undefined feature' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + expect(described_class.enabled?(:some_random_feature_flag)).to be_falsey end - it 'returns true for undefined feature with default_enabled' do - expect(described_class.enabled?(:some_random_feature_flag, default_enabled: true)).to be_truthy + it 'returns false for undefined feature with default_enabled_if_undefined: false' do + expect(described_class.enabled?(:some_random_feature_flag, default_enabled_if_undefined: false)).to be_falsey + end + + it 'returns true for undefined feature with default_enabled_if_undefined: true' do + expect(described_class.enabled?(:some_random_feature_flag, default_enabled_if_undefined: true)).to be_truthy end it 'returns false for existing disabled feature in the database' do @@ -184,23 +235,23 @@ RSpec.describe Feature, stub_feature_flags: false do it 'caches the status in L1 and L2 caches', :request_store, :use_clean_rails_memory_store_caching do - described_class.enable(:enabled_feature_flag) - flipper_key = "flipper/v1/feature/enabled_feature_flag" + described_class.enable(:disabled_feature_flag) + flipper_key = "flipper/v1/feature/disabled_feature_flag" expect(described_class.send(:l2_cache_backend)) .to receive(:fetch) .once - .with(flipper_key, expires_in: 1.hour) + .with(flipper_key, { expires_in: 1.hour }) .and_call_original expect(described_class.send(:l1_cache_backend)) .to receive(:fetch) .once - .with(flipper_key, expires_in: 1.minute) + .with(flipper_key, { expires_in: 1.minute }) .and_call_original 2.times do - expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy + expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy end end @@ -208,22 +259,14 @@ RSpec.describe Feature, stub_feature_flags: false do fake_default = double('fake default') expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, "No database" } - expect(described_class.enabled?(:a_feature, default_enabled: fake_default)).to eq(fake_default) + expect(described_class.enabled?(:a_feature, default_enabled_if_undefined: fake_default)).to eq(fake_default) end context 'logging is enabled', :request_store do before do allow(Feature).to receive(:log_feature_flag_states?).and_call_original - definition = Feature::Definition.new("development/enabled_feature_flag.yml", - name: :enabled_feature_flag, - type: 'development', - log_state_changes: true, - default_enabled: false) - - allow(Feature::Definition).to receive(:definitions) do - { definition.key => definition } - end + stub_feature_flag_definition(:enabled_feature_flag, log_state_changes: true) described_class.enable(:feature_flag_state_logs) described_class.enable(:enabled_feature_flag) @@ -241,18 +284,16 @@ RSpec.describe Feature, stub_feature_flags: false do end context 'cached feature flag', :request_store do - let(:flag) { :some_feature_flag } - before do described_class.send(:flipper).memoize = false - described_class.enabled?(flag) + described_class.enabled?(:disabled_feature_flag) end it 'caches the status in L1 cache for the first minute' do expect do expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original expect(described_class.send(:l2_cache_backend)).not_to receive(:fetch) - expect(described_class.enabled?(flag)).to be_truthy + expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy end.not_to exceed_query_limit(0) end @@ -261,7 +302,7 @@ RSpec.describe Feature, stub_feature_flags: false do expect do expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original expect(described_class.send(:l2_cache_backend)).to receive(:fetch).once.and_call_original - expect(described_class.enabled?(flag)).to be_truthy + expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy end.not_to exceed_query_limit(0) end end @@ -271,7 +312,7 @@ RSpec.describe Feature, stub_feature_flags: false do expect do expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original expect(described_class.send(:l2_cache_backend)).to receive(:fetch).once.and_call_original - expect(described_class.enabled?(flag)).to be_truthy + expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy end.not_to exceed_query_limit(1) end end @@ -338,21 +379,38 @@ RSpec.describe Feature, stub_feature_flags: false do .to raise_error(/The `type:` of/) end - it 'when invalid default_enabled is used' do - expect { described_class.enabled?(:my_feature_flag, default_enabled: true) } - .to raise_error(/The `default_enabled:` of/) + context 'when default_enabled: is false in the YAML definition' do + it 'reads the default from the YAML definition' do + expect(described_class.enabled?(:my_feature_flag)).to eq(default_enabled) + end end - context 'when `default_enabled: :yaml` is used in code' do + context 'when default_enabled: is true in the YAML definition' do + let(:default_enabled) { true } + it 'reads the default from the YAML definition' do - expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(false) + expect(described_class.enabled?(:my_feature_flag)).to eq(true) end - context 'when default_enabled is true in the YAML definition' do - let(:default_enabled) { true } + context 'and feature has been disabled' do + before do + described_class.disable(:my_feature_flag) + end - it 'reads the default from the YAML definition' do - expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(true) + it 'is not enabled' do + expect(described_class.enabled?(:my_feature_flag)).to eq(false) + end + end + + context 'with a cached value and the YAML definition is changed thereafter' do + before do + described_class.enabled?(:my_feature_flag) + end + + it 'reads new default value' do + allow(definition).to receive(:default_enabled).and_return(true) + + expect(described_class.enabled?(:my_feature_flag)).to eq(true) end end @@ -361,7 +419,7 @@ RSpec.describe Feature, stub_feature_flags: false do context 'when in dev or test environment' do it 'raises an error for dev' do - expect { described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml) } + expect { described_class.enabled?(:non_existent_flag, type: optional_type) } .to raise_error( Feature::InvalidFeatureFlagError, "The feature flag YAML definition for 'non_existent_flag' does not exist") @@ -379,9 +437,9 @@ RSpec.describe Feature, stub_feature_flags: false do end it 'checks the persisted status and returns false' do - expect(described_class).to receive(:get).with(:non_existent_flag).and_call_original + expect(described_class).to receive(:with_feature).with(:non_existent_flag).and_call_original - expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false) + expect(described_class.enabled?(:non_existent_flag, type: optional_type)).to eq(false) end end @@ -393,7 +451,7 @@ RSpec.describe Feature, stub_feature_flags: false do it 'returns false without checking the status in the database' do expect(described_class).not_to receive(:get) - expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false) + expect(described_class.enabled?(:non_existent_flag, type: optional_type)).to eq(false) end end end @@ -403,21 +461,29 @@ RSpec.describe Feature, stub_feature_flags: false do end describe '.disable?' do - it 'returns true for undefined feature' do + it 'returns true (and tracks / raises exception for dev) for undefined feature' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + expect(described_class.disabled?(:some_random_feature_flag)).to be_truthy end - it 'returns false for undefined feature with default_enabled' do - expect(described_class.disabled?(:some_random_feature_flag, default_enabled: true)).to be_falsey + it 'returns true for undefined feature with default_enabled_if_undefined: false' do + expect(described_class.disabled?(:some_random_feature_flag, default_enabled_if_undefined: false)).to be_truthy + end + + it 'returns false for undefined feature with default_enabled_if_undefined: true' do + expect(described_class.disabled?(:some_random_feature_flag, default_enabled_if_undefined: true)).to be_falsey end it 'returns true for existing disabled feature in the database' do + stub_feature_flag_definition(:disabled_feature_flag) described_class.disable(:disabled_feature_flag) expect(described_class.disabled?(:disabled_feature_flag)).to be_truthy end it 'returns false for existing enabled feature in the database' do + stub_feature_flag_definition(:enabled_feature_flag) described_class.enable(:enabled_feature_flag) expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey @@ -556,14 +622,7 @@ RSpec.describe Feature, stub_feature_flags: false do let(:log_state_changes) { false } let(:milestone) { "0.0" } let(:flag_name) { :some_flag } - let(:definition) do - Feature::Definition.new("development/#{flag_name}.yml", - name: flag_name, - type: 'development', - milestone: milestone, - log_state_changes: log_state_changes, - default_enabled: false) - end + let(:flag_type) { 'development' } before do Feature.enable(:feature_flag_state_logs) @@ -573,9 +632,10 @@ RSpec.describe Feature, stub_feature_flags: false do allow(Feature).to receive(:log_feature_flag_states?).with(:feature_flag_state_logs).and_call_original allow(Feature).to receive(:log_feature_flag_states?).with(:some_flag).and_call_original - allow(Feature::Definition).to receive(:definitions) do - { definition.key => definition } - end + stub_feature_flag_definition(flag_name, + type: flag_type, + milestone: milestone, + log_state_changes: log_state_changes) end subject { described_class.log_feature_flag_states?(flag_name) } @@ -583,6 +643,7 @@ RSpec.describe Feature, stub_feature_flags: false do context 'when flag is feature_flag_state_logs' do let(:milestone) { "14.6" } let(:flag_name) { :feature_flag_state_logs } + let(:flag_type) { 'ops' } let(:log_state_changes) { true } it { is_expected.to be_falsey } @@ -593,13 +654,7 @@ RSpec.describe Feature, stub_feature_flags: false do end context 'when flag is old while log_state_changes is not present ' do - let(:definition) do - Feature::Definition.new("development/#{flag_name}.yml", - name: flag_name, - type: 'development', - milestone: milestone, - default_enabled: false) - end + let(:log_state_changes) { nil } it { is_expected.to be_falsey } end @@ -621,12 +676,7 @@ RSpec.describe Feature, stub_feature_flags: false do end context 'when milestone is nil' do - let(:definition) do - Feature::Definition.new("development/#{flag_name}.yml", - name: flag_name, - type: 'development', - default_enabled: false) - end + let(:milestone) { nil } it { is_expected.to be_falsey } end diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb index 20c89eab5f5..efe78cd3a35 100644 --- a/spec/lib/gitlab/application_rate_limiter_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter_spec.rb @@ -56,6 +56,20 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting end end + context 'when the key is valid' do + it 'records the checked key in request storage', :request_store do + subject.throttled?(:test_action, scope: [user]) + + expect(::Gitlab::Instrumentation::RateLimitingGates.payload) + .to eq(::Gitlab::Instrumentation::RateLimitingGates::GATES => [:test_action]) + + subject.throttled?(:another_action, scope: [user], peek: true) + + expect(::Gitlab::Instrumentation::RateLimitingGates.payload) + .to eq(::Gitlab::Instrumentation::RateLimitingGates::GATES => [:test_action, :another_action]) + end + end + shared_examples 'throttles based on key and scope' do let(:start_time) { Time.current.beginning_of_hour } diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 44bbbe49cd3..d86191ca0c2 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -79,7 +79,7 @@ module Gitlab }, 'image with onerror' => { input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]', - output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" + output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" } } @@ -112,13 +112,13 @@ module Gitlab context "images" do it "does lazy load and link image" do input = 'image:https://localhost.com/image.png[]' - output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" + output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" expect(render(input, context)).to include(output) end it "does not automatically link image if link is explicitly defined" do input = 'image:https://localhost.com/image.png[link=https://gitlab.com]' - output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" + output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" decoding=\"async\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>" expect(render(input, context)).to include(output) end end @@ -524,7 +524,7 @@ module Gitlab output = <<~HTML <div> <div> - <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a> + <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" decoding="async" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a> </div> </div> HTML @@ -578,7 +578,7 @@ module Gitlab output = <<~HTML <div> <div> - <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a> + <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" decoding=\"async\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a> </div> </div> HTML @@ -625,7 +625,7 @@ module Gitlab output = <<~HTML <div> <div> - <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a> + <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" decoding="async" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a> </div> </div> HTML diff --git a/spec/lib/gitlab/audit/deploy_token_author_spec.rb b/spec/lib/gitlab/audit/deploy_token_author_spec.rb new file mode 100644 index 00000000000..449b7456a80 --- /dev/null +++ b/spec/lib/gitlab/audit/deploy_token_author_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::DeployTokenAuthor do + describe '#initialize' do + it 'sets correct attributes' do + expect(described_class.new(name: 'Lorem deploy token')) + .to have_attributes(id: -2, name: 'Lorem deploy token') + end + + it 'sets default name when it is not provided' do + expect(described_class.new) + .to have_attributes(id: -2, name: 'Deploy Token') + end + end +end diff --git a/spec/lib/gitlab/audit/null_author_spec.rb b/spec/lib/gitlab/audit/null_author_spec.rb index 7203a0cd816..2045139a5f7 100644 --- a/spec/lib/gitlab/audit/null_author_spec.rb +++ b/spec/lib/gitlab/audit/null_author_spec.rb @@ -48,6 +48,15 @@ RSpec.describe Gitlab::Audit::NullAuthor do expect(subject.for(-1, audit_event)).to be_a(Gitlab::Audit::CiRunnerTokenAuthor) expect(subject.for(-1, audit_event)).to have_attributes(id: -1, name: 'Authentication token: cde456') end + + it 'returns DeployTokenAuthor when id equals -2', :aggregate_failures do + allow(audit_event).to receive(:[]).with(:author_name).and_return('Test deploy token') + allow(audit_event).to receive(:details).and_return({}) + allow(audit_event).to receive(:target_type) + + expect(subject.for(-2, audit_event)).to be_a(Gitlab::Audit::DeployTokenAuthor) + expect(subject.for(-2, audit_event)).to have_attributes(id: -2, name: 'Test deploy token') + end end describe '#current_sign_in_ip' do diff --git a/spec/lib/gitlab/auth/ldap/adapter_spec.rb b/spec/lib/gitlab/auth/ldap/adapter_spec.rb index b7b12e49a8e..3791b7a07dd 100644 --- a/spec/lib/gitlab/auth/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/auth/ldap/adapter_spec.rb @@ -26,10 +26,12 @@ RSpec.describe Gitlab::Auth::Ldap::Adapter do it 'searches with the proper options when searching by dn' do expect(adapter).to receive(:ldap_search).with( - base: 'uid=johndoe,ou=users,dc=example,dc=com', - scope: Net::LDAP::SearchScope_BaseObject, - attributes: ldap_attributes, - filter: nil + { + base: 'uid=johndoe,ou=users,dc=example,dc=com', + scope: Net::LDAP::SearchScope_BaseObject, + attributes: ldap_attributes, + filter: nil + } ).and_return({}) adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com') diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp_spec.rb index dc20df98185..f08c787382e 100644 --- a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do +RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator::ManualOtp do let_it_be(:user) { create(:user) } let(:otp_code) { 42 } diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp_spec.rb new file mode 100644 index 00000000000..231bd3f48f1 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator::PushOtp do + let_it_be(:user) { create(:user) } + + 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/pushauth/" } + let(:response_status) { 200 } + + subject(:validate) { described_class.new(user).validate } + + before do + stub_feature_flags(forti_authenticator: user) + + stub_forti_authenticator_config( + enabled: true, + host: host, + port: port, + username: api_username, + access_token: api_token + ) + + request_body = { username: user.username } + + stub_request(:post, forti_authenticator_auth_url) + .with(body: JSON(request_body), + headers: { 'Content-Type': 'application/json' }, + basic_auth: [api_username, api_token]) + .to_return(status: response_status, body: '') + end + + context 'successful validation' do + 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 + + context 'unexpected error' do + it 'returns error' do + error_message = 'boom!' + stub_request(:post, forti_authenticator_auth_url).to_raise(StandardError.new(error_message)) + + expect(validate[:status]).to eq(:error) + expect(validate[:message]).to eq(error_message) + 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/saml/config_spec.rb b/spec/lib/gitlab/auth/saml/config_spec.rb new file mode 100644 index 00000000000..12f5da48873 --- /dev/null +++ b/spec/lib/gitlab/auth/saml/config_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Saml::Config do + describe '.enabled?' do + subject { described_class.enabled? } + + it { is_expected.to eq(false) } + + context 'when SAML is enabled' do + before do + allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml]) + end + + it { is_expected.to eq(true) } + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb deleted file mode 100644 index f5d2224747a..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20210301200959 do - subject(:perform) { migration.perform(1, 99) } - - let(:migration) { described_class.new } - let(:artifact_outside_id_range) { create_artifact!(id: 100, created_at: 1.year.ago, expire_at: nil) } - let(:artifact_outside_date_range) { create_artifact!(id: 40, created_at: Time.current, expire_at: nil) } - let(:old_artifact) { create_artifact!(id: 10, created_at: 16.months.ago, expire_at: nil) } - let(:recent_artifact) { create_artifact!(id: 20, created_at: 1.year.ago, expire_at: nil) } - let(:artifact_with_expiry) { create_artifact!(id: 30, created_at: 1.year.ago, expire_at: Time.current + 1.day) } - - before do - table(:namespaces).create!(id: 1, name: 'the-namespace', path: 'the-path') - table(:projects).create!(id: 1, name: 'the-project', namespace_id: 1) - table(:ci_builds).create!(id: 1, allow_failure: false) - end - - context 'when current date is before the 22nd' do - before do - travel_to(Time.zone.local(2020, 1, 1, 0, 0, 0)) - end - - it 'backfills the expiry date for old artifacts' do - expect(old_artifact.reload.expire_at).to eq(nil) - - perform - - expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 4, 22, 0, 0, 0)) - end - - it 'backfills the expiry date for recent artifacts' do - expect(recent_artifact.reload.expire_at).to eq(nil) - - perform - - expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 1, 22, 0, 0, 0)) - end - end - - context 'when current date is after the 22nd' do - before do - travel_to(Time.zone.local(2020, 1, 23, 0, 0, 0)) - end - - it 'backfills the expiry date for old artifacts' do - expect(old_artifact.reload.expire_at).to eq(nil) - - perform - - expect(old_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2020, 5, 22, 0, 0, 0)) - end - - it 'backfills the expiry date for recent artifacts' do - expect(recent_artifact.reload.expire_at).to eq(nil) - - perform - - expect(recent_artifact.reload.expire_at).to be_within(1.minute).of(Time.zone.local(2021, 2, 22, 0, 0, 0)) - end - end - - it 'does not touch artifacts with expiry date' do - expect { perform }.not_to change { artifact_with_expiry.reload.expire_at } - end - - it 'does not touch artifacts outside id range' do - expect { perform }.not_to change { artifact_outside_id_range.reload.expire_at } - end - - it 'does not touch artifacts outside date range' do - expect { perform }.not_to change { artifact_outside_date_range.reload.expire_at } - end - - private - - def create_artifact!(**args) - table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 1) - end -end diff --git a/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb index 1158eedfe7c..84611c88806 100644 --- a/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequests, end end - it "updates all open draft merge request's draft field to true" do + it "updates all eligible draft merge request's draft field to true" do mr_count = merge_requests.all.count expect { subject.perform(mr_ids.first, mr_ids.last) } diff --git a/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb new file mode 100644 index 00000000000..e6e10977143 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequestsWithCorrectedRegex, + :migration, schema: 20220326161803 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:merge_requests) { table(:merge_requests) } + + let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') } + let(:project) { projects.create!(namespace_id: group.id) } + + let(:draft_prefixes) { ["[Draft]", "(Draft)", "Draft:", "Draft", "[WIP]", "WIP:", "WIP"] } + + def create_merge_request(params) + common_params = { + target_project_id: project.id, + target_branch: 'feature1', + source_branch: 'master' + } + + merge_requests.create!(common_params.merge(params)) + end + + context "for MRs with #draft? == true titles but draft attribute false" do + let(:mr_ids) { merge_requests.all.collect(&:id) } + + before do + draft_prefixes.each do |prefix| + (1..4).each do |n| + create_merge_request( + title: "#{prefix} This is a title", + draft: false, + state_id: n + ) + + create_merge_request( + title: "This is a title with the #{prefix} in a weird spot", + draft: false, + state_id: n + ) + end + end + end + + it "updates all eligible draft merge request's draft field to true" do + mr_count = merge_requests.all.count + + expect { subject.perform(mr_ids.first, mr_ids.last) } + .to change { MergeRequest.where(draft: false).count } + .from(mr_count).to(mr_count - draft_prefixes.length) + end + + it "marks successful slices as completed" do + expect(subject).to receive(:mark_job_as_succeeded).with(mr_ids.first, mr_ids.last) + + subject.perform(mr_ids.first, mr_ids.last) + end + + it_behaves_like 'marks background migration job records' do + let!(:non_eligible_mrs) do + Array.new(2) do + create_merge_request( + title: "Not a d-r-a-f-t 1", + draft: false, + state_id: 1 + ) + end + end + + let(:arguments) { [non_eligible_mrs.first.id, non_eligible_mrs.last.id] } + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb index 4705f0d0ab9..d84bc479554 100644 --- a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb @@ -6,7 +6,15 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, s let(:group_features) { table(:group_features) } let(:namespaces) { table(:namespaces) } - subject { described_class.new(connection: ActiveRecord::Base.connection) } + subject do + described_class.new(start_id: 1, + end_id: 4, + batch_table: :namespaces, + batch_column: :id, + sub_batch_size: 10, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + end describe '#perform' do it 'creates settings for all group namespaces in range' do @@ -19,7 +27,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, s group_features.create!(id: 1, group_id: 4) expect(group_features.count).to eq 1 - expect { subject.perform(1, 4, :namespaces, :id, 10, 0, 4) }.to change { group_features.count }.by(2) + expect { subject.perform(4) }.to change { group_features.count }.by(2) expect(group_features.count).to eq 3 expect(group_features.all.pluck(:group_id)).to contain_exactly(1, 3, 4) diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb new file mode 100644 index 00000000000..b3825a7c4ea --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsEnableSslVerification, schema: 20220425121410 do + let(:migration) { described_class.new } + let(:integrations) { described_class::Integration } + + before do + integrations.create!(id: 1, type_new: 'Integrations::Bamboo') # unaffected integration + integrations.create!(id: 2, type_new: 'Integrations::DroneCi') # no properties + integrations.create!(id: 3, type_new: 'Integrations::DroneCi', + properties: {}) # no URL + integrations.create!(id: 4, type_new: 'Integrations::DroneCi', + properties: { 'drone_url' => '' }) # blank URL + integrations.create!(id: 5, type_new: 'Integrations::DroneCi', + properties: { 'drone_url' => 'https://example.com:foo' }) # invalid URL + integrations.create!(id: 6, type_new: 'Integrations::DroneCi', + properties: { 'drone_url' => 'https://example.com' }) # unknown URL + integrations.create!(id: 7, type_new: 'Integrations::DroneCi', + properties: { 'drone_url' => 'http://cloud.drone.io' }) # no HTTPS + integrations.create!(id: 8, type_new: 'Integrations::DroneCi', + properties: { 'drone_url' => 'https://cloud.drone.io' }) # known URL + integrations.create!(id: 9, type_new: 'Integrations::Teamcity', + properties: { 'teamcity_url' => 'https://example.com' }) # unknown URL + integrations.create!(id: 10, type_new: 'Integrations::Teamcity', + properties: { 'teamcity_url' => 'https://foo.bar.teamcity.com' }) # unknown URL + integrations.create!(id: 11, type_new: 'Integrations::Teamcity', + properties: { 'teamcity_url' => 'https://teamcity.com' }) # unknown URL + integrations.create!(id: 12, type_new: 'Integrations::Teamcity', + properties: { 'teamcity_url' => 'https://customer.teamcity.com' }) # known URL + end + + def properties(id) + integrations.find(id).properties + end + + it 'enables SSL verification for known-good hostnames', :aggregate_failures do + migration.perform(1, 12) + + # Bamboo + expect(properties(1)).to be_nil + + # DroneCi + expect(properties(2)).to be_nil + expect(properties(3)).not_to include('enable_ssl_verification') + expect(properties(4)).not_to include('enable_ssl_verification') + expect(properties(5)).not_to include('enable_ssl_verification') + expect(properties(6)).not_to include('enable_ssl_verification') + expect(properties(7)).not_to include('enable_ssl_verification') + expect(properties(8)).to include('enable_ssl_verification' => true) + + # Teamcity + expect(properties(9)).not_to include('enable_ssl_verification') + expect(properties(10)).not_to include('enable_ssl_verification') + expect(properties(11)).not_to include('enable_ssl_verification') + expect(properties(12)).to include('enable_ssl_verification' => true) + end + + it 'only updates records within the given ID range', :aggregate_failures do + migration.perform(1, 8) + + expect(properties(8)).to include('enable_ssl_verification' => true) + expect(properties(12)).not_to include('enable_ssl_verification') + end + + it 'marks the job as succeeded' do + expect(Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded) + .with('BackfillIntegrationsEnableSslVerification', [1, 10]) + + migration.perform(1, 10) + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb index 8f765a7a536..d8a7ec775dd 100644 --- a/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb @@ -2,10 +2,19 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsTypeNew do +RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsTypeNew, :migration, schema: 20220212120735 do let(:migration) { described_class.new } let(:integrations) { table(:integrations) } - let(:namespaced_integrations) { Gitlab::Integrations::StiType.namespaced_integrations } + + let(:namespaced_integrations) do + Set.new(%w[ + Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog + Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Harbor Irker Jenkins Jira Mattermost + MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker + Prometheus Pushover Redmine Shimo Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack Zentao + Github GitlabSlackApplication + ]).freeze + end before do integrations.connection.execute 'ALTER TABLE integrations DISABLE TRIGGER "trigger_type_new_on_insert"' diff --git a/spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb new file mode 100644 index 00000000000..dcb4ede36af --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillNoteDiscussionId do + let(:migration) { described_class.new } + let(:notes_table) { table(:notes) } + let(:existing_discussion_id) { Digest::SHA1.hexdigest('test') } + + before do + notes_table.create!(id: 1, noteable_type: 'Issue', noteable_id: 2, discussion_id: existing_discussion_id) + notes_table.create!(id: 2, noteable_type: 'Issue', noteable_id: 1, discussion_id: nil) + notes_table.create!(id: 3, noteable_type: 'MergeRequest', noteable_id: 1, discussion_id: nil) + notes_table.create!(id: 4, noteable_type: 'Commit', commit_id: RepoHelpers.sample_commit.id, discussion_id: nil) + notes_table.create!(id: 5, noteable_type: 'Issue', noteable_id: 2, discussion_id: nil) + notes_table.create!(id: 6, noteable_type: 'MergeRequest', noteable_id: 2, discussion_id: nil) + end + + it 'updates records in the specified batch', :aggregate_failures do + migration.perform(1, 5) + + expect(notes_table.where(discussion_id: nil).count).to eq(1) + + expect(notes_table.find(1).discussion_id).to eq(existing_discussion_id) + notes_table.where(id: 2..5).each do |n| + expect(n.discussion_id).to match(/\A[0-9a-f]{40}\z/) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb new file mode 100644 index 00000000000..525c236b644 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectSettings, :migration, schema: 20220324165436 do + let(:migration) { described_class.new } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + + let(:table_name) { 'projects' } + let(:batch_column) { :id } + let(:sub_batch_size) { 2 } + let(:pause_ms) { 0 } + + subject(:perform_migration) { migration.perform(1, 30, table_name, batch_column, sub_batch_size, pause_ms) } + + before do + namespaces_table.create!(id: 1, name: 'namespace', path: 'namespace-path', type: 'Group') + projects_table.create!(id: 11, name: 'group-project-1', path: 'group-project-path-1', namespace_id: 1) + projects_table.create!(id: 12, name: 'group-project-2', path: 'group-project-path-2', namespace_id: 1) + project_settings_table.create!(project_id: 11) + + namespaces_table.create!(id: 2, name: 'namespace', path: 'namespace-path', type: 'User') + projects_table.create!(id: 21, name: 'user-project-1', path: 'user--project-path-1', namespace_id: 2) + projects_table.create!(id: 22, name: 'user-project-2', path: 'user-project-path-2', namespace_id: 2) + project_settings_table.create!(project_id: 21) + end + + it 'backfills project settings when it does not exist', :aggregate_failures do + expect(project_settings_table.count).to eq 2 + + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(5) + + expect(project_settings_table.count).to eq 4 + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb b/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb new file mode 100644 index 00000000000..3c46456eed0 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillTopicsTitle, schema: 20220331133802 do + it 'correctly backfills the title of the topics' do + topics = table(:topics) + + topic_1 = topics.create!(name: 'topic1') + topic_2 = topics.create!(name: 'topic2', title: 'Topic 2') + topic_3 = topics.create!(name: 'topic3') + topic_4 = topics.create!(name: 'topic4') + + subject.perform(topic_1.id, topic_3.id) + + expect(topic_1.reload.title).to eq('topic1') + expect(topic_2.reload.title).to eq('Topic 2') + expect(topic_3.reload.title).to eq('topic3') + expect(topic_4.reload.title).to be_nil + end +end diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb new file mode 100644 index 00000000000..f8b3a8681f0 --- /dev/null +++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do + describe '#perform' do + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + let(:job_class) { Class.new(described_class) } + + let(:job_instance) do + job_class.new(start_id: 1, end_id: 10, + batch_table: '_test_table', + batch_column: 'id', + sub_batch_size: 2, + pause_ms: 1000, + connection: connection) + end + + subject(:perform_job) { job_instance.perform } + + it 'raises an error if not overridden' do + expect { perform_job }.to raise_error(NotImplementedError, /must implement perform/) + end + + context 'when the subclass uses sub-batching' do + let(:job_class) do + Class.new(described_class) do + def perform(*job_arguments) + each_sub_batch( + operation_name: :update, + batching_arguments: { order_hint: :updated_at }, + batching_scope: -> (relation) { relation.where.not(bar: nil) } + ) do |sub_batch| + sub_batch.update_all('to_column = from_column') + end + end + end + end + + let(:test_table) { table(:_test_table) } + + before do + allow(job_instance).to receive(:sleep) + + connection.create_table :_test_table do |t| + t.timestamps_with_timezone null: false + t.integer :from_column, null: false + t.text :bar + t.integer :to_column + end + + test_table.create!(id: 1, from_column: 5, bar: 'value') + test_table.create!(id: 2, from_column: 10, bar: 'value') + test_table.create!(id: 3, from_column: 15) + test_table.create!(id: 4, from_column: 20, bar: 'value') + end + + after do + connection.drop_table(:_test_table) + end + + it 'calls the operation for each sub-batch' do + expect { perform_job }.to change { test_table.where(to_column: nil).count }.from(4).to(1) + + expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(5, 10, nil, 20) + end + + it 'instruments the batch operation' do + expect(job_instance.batch_metrics.affected_rows).to be_empty + + expect(job_instance.batch_metrics).to receive(:instrument_operation).with(:update).twice.and_call_original + + perform_job + + expect(job_instance.batch_metrics.affected_rows[:update]).to contain_exactly(2, 1) + end + + it 'pauses after each sub-batch' do + expect(job_instance).to receive(:sleep).with(1.0).twice + + perform_job + end + + context 'when batching_arguments are given' do + it 'forwards them for batching' do + expect(job_instance).to receive(:parent_batch_relation).and_return(test_table) + + expect(test_table).to receive(:each_batch).with(column: 'id', of: 2, order_hint: :updated_at) + + perform_job + end + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb index 90d9bbb42c3..78bd1afd8d2 100644 --- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb @@ -3,123 +3,134 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob do - let(:table_name) { :_test_copy_primary_key_test } - let(:test_table) { table(table_name) } - let(:sub_batch_size) { 1000 } - let(:pause_ms) { 0 } - let(:connection) { ApplicationRecord.connection } - - let(:helpers) do - ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers) - end - - before do - connection.execute(<<~SQL) - CREATE TABLE #{table_name} - ( - id integer NOT NULL, - name character varying, - fk integer NOT NULL, - #{helpers.convert_to_bigint_column(:id)} bigint DEFAULT 0 NOT NULL, - #{helpers.convert_to_bigint_column(:fk)} bigint DEFAULT 0 NOT NULL, - name_convert_to_text text DEFAULT 'no name' - ); - SQL - - # Insert some data, it doesn't make a difference - test_table.create!(id: 11, name: 'test1', fk: 1) - test_table.create!(id: 12, name: 'test2', fk: 2) - test_table.create!(id: 15, name: nil, fk: 3) - test_table.create!(id: 19, name: 'test4', fk: 4) - end + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchedMigrationJob } - after do - # Make sure that the temp table we created is dropped (it is not removed by the database_cleaner) - connection.execute(<<~SQL) - DROP TABLE IF EXISTS #{table_name}; - SQL - end + describe '#perform' do + let(:table_name) { :_test_copy_primary_key_test } + let(:test_table) { table(table_name) } + let(:sub_batch_size) { 1000 } + let(:pause_ms) { 0 } + let(:connection) { ApplicationRecord.connection } + + let(:helpers) do + ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers) + end - subject(:copy_columns) { described_class.new(connection: connection) } + let(:copy_job) do + described_class.new(start_id: 12, + end_id: 20, + batch_table: table_name, + batch_column: 'id', + sub_batch_size: sub_batch_size, + pause_ms: pause_ms, + connection: connection) + end - it { expect(described_class).to be < Gitlab::BackgroundMigration::BaseJob } + before do + connection.execute(<<~SQL) + CREATE TABLE #{table_name} + ( + id integer NOT NULL, + name character varying, + fk integer NOT NULL, + #{helpers.convert_to_bigint_column(:id)} bigint DEFAULT 0 NOT NULL, + #{helpers.convert_to_bigint_column(:fk)} bigint DEFAULT 0 NOT NULL, + name_convert_to_text text DEFAULT 'no name' + ); + SQL + + # Insert some data, it doesn't make a difference + test_table.create!(id: 11, name: 'test1', fk: 1) + test_table.create!(id: 12, name: 'test2', fk: 2) + test_table.create!(id: 15, name: nil, fk: 3) + test_table.create!(id: 19, name: 'test4', fk: 4) + end - describe '#perform' do - let(:migration_class) { described_class.name } + after do + # Make sure that the temp table we created is dropped (it is not removed by the database_cleaner) + connection.execute(<<~SQL) + DROP TABLE IF EXISTS #{table_name}; + SQL + end it 'copies all primary keys in range' do temporary_column = helpers.convert_to_bigint_column(:id) - copy_columns.perform(12, 15, table_name, 'id', sub_batch_size, pause_ms, 'id', temporary_column) - expect(test_table.where("id = #{temporary_column}").pluck(:id)).to contain_exactly(12, 15) - expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(11, 19) - expect(test_table.all.count).to eq(4) + copy_job.perform('id', temporary_column) + + expect(test_table.count).to eq(4) + expect(test_table.where("id = #{temporary_column}").pluck(:id)).to contain_exactly(12, 15, 19) + expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(11) end it 'copies all foreign keys in range' do temporary_column = helpers.convert_to_bigint_column(:fk) - copy_columns.perform(10, 14, table_name, 'id', sub_batch_size, pause_ms, 'fk', temporary_column) - expect(test_table.where("fk = #{temporary_column}").pluck(:id)).to contain_exactly(11, 12) - expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(15, 19) - expect(test_table.all.count).to eq(4) + copy_job.perform('fk', temporary_column) + + expect(test_table.count).to eq(4) + expect(test_table.where("fk = #{temporary_column}").pluck(:id)).to contain_exactly(12, 15, 19) + expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(11) end it 'copies columns with NULLs' do - expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(4) + expect { copy_job.perform('name', 'name_convert_to_text') } + .to change { test_table.where("name_convert_to_text = 'no name'").count }.from(4).to(1) - copy_columns.perform(10, 20, table_name, 'id', sub_batch_size, pause_ms, 'name', 'name_convert_to_text') - - expect(test_table.where('name = name_convert_to_text').pluck(:id)).to contain_exactly(11, 12, 19) + expect(test_table.where('name = name_convert_to_text').pluck(:id)).to contain_exactly(12, 19) expect(test_table.where('name is NULL and name_convert_to_text is NULL').pluck(:id)).to contain_exactly(15) - expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(0) end - it 'copies multiple columns when given' do - columns_to_copy_from = %w[id fk] - id_tmp_column = helpers.convert_to_bigint_column('id') - fk_tmp_column = helpers.convert_to_bigint_column('fk') - columns_to_copy_to = [id_tmp_column, fk_tmp_column] + context 'when multiple columns are given' do + let(:id_tmp_column) { helpers.convert_to_bigint_column('id') } + let(:fk_tmp_column) { helpers.convert_to_bigint_column('fk') } + let(:columns_to_copy_from) { %w[id fk] } + let(:columns_to_copy_to) { [id_tmp_column, fk_tmp_column] } - subject.perform(10, 15, table_name, 'id', sub_batch_size, pause_ms, columns_to_copy_from, columns_to_copy_to) + it 'copies all values in the range' do + copy_job.perform(columns_to_copy_from, columns_to_copy_to) - expect(test_table.where("id = #{id_tmp_column} AND fk = #{fk_tmp_column}").pluck(:id)).to contain_exactly(11, 12, 15) - expect(test_table.where(id_tmp_column => 0).where(fk_tmp_column => 0).pluck(:id)).to contain_exactly(19) - expect(test_table.all.count).to eq(4) - end + expect(test_table.count).to eq(4) + expect(test_table.where("id = #{id_tmp_column} AND fk = #{fk_tmp_column}").pluck(:id)).to contain_exactly(12, 15, 19) + expect(test_table.where(id_tmp_column => 0).where(fk_tmp_column => 0).pluck(:id)).to contain_exactly(11) + end - it 'raises error when number of source and target columns does not match' do - columns_to_copy_from = %w[id fk] - columns_to_copy_to = [helpers.convert_to_bigint_column(:id)] + context 'when the number of source and target columns does not match' do + let(:columns_to_copy_to) { [id_tmp_column] } - expect do - subject.perform(10, 15, table_name, 'id', sub_batch_size, pause_ms, columns_to_copy_from, columns_to_copy_to) - end.to raise_error(ArgumentError, 'number of source and destination columns must match') + it 'raises an error' do + expect do + copy_job.perform(columns_to_copy_from, columns_to_copy_to) + end.to raise_error(ArgumentError, 'number of source and destination columns must match') + end + end end it 'tracks timings of queries' do - expect(copy_columns.batch_metrics.timings).to be_empty + expect(copy_job.batch_metrics.timings).to be_empty - copy_columns.perform(10, 20, table_name, 'id', sub_batch_size, pause_ms, 'name', 'name_convert_to_text') + copy_job.perform('name', 'name_convert_to_text') - expect(copy_columns.batch_metrics.timings[:update_all]).not_to be_empty + expect(copy_job.batch_metrics.timings[:update_all]).not_to be_empty end context 'pause interval between sub-batches' do - it 'sleeps for the specified time between sub-batches' do - sub_batch_size = 2 + let(:pause_ms) { 5 } - expect(copy_columns).to receive(:sleep).with(0.005) + it 'sleeps for the specified time between sub-batches' do + expect(copy_job).to receive(:sleep).with(0.005) - copy_columns.perform(10, 12, table_name, 'id', sub_batch_size, 5, 'name', 'name_convert_to_text') + copy_job.perform('name', 'name_convert_to_text') end - it 'treats negative values as 0' do - sub_batch_size = 2 + context 'when pause_ms value is negative' do + let(:pause_ms) { -5 } - expect(copy_columns).to receive(:sleep).with(0) + it 'treats it as a 0' do + expect(copy_job).to receive(:sleep).with(0) - copy_columns.perform(10, 12, table_name, 'id', sub_batch_size, -5, 'name', 'name_convert_to_text') + copy_job.perform('name', 'name_convert_to_text') + end end end end diff --git a/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb new file mode 100644 index 00000000000..cffcda0a2ca --- /dev/null +++ b/spec/lib/gitlab/background_migration/expire_o_auth_tokens_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::ExpireOAuthTokens, :migration, schema: 20220428133724 do + let(:migration) { described_class.new } + let(:oauth_access_tokens_table) { table(:oauth_access_tokens) } + + let(:table_name) { 'oauth_access_tokens' } + + subject(:perform_migration) do + described_class.new(start_id: 1, + end_id: 30, + batch_table: :oauth_access_tokens, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + before do + oauth_access_tokens_table.create!(id: 1, token: 's3cr3t-1', expires_in: nil) + oauth_access_tokens_table.create!(id: 2, token: 's3cr3t-2', expires_in: 42) + oauth_access_tokens_table.create!(id: 3, token: 's3cr3t-3', expires_in: nil) + end + + it 'adds expiry to oauth tokens', :aggregate_failures do + expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(3) + + expect(oauth_access_tokens_table.find(1).expires_in).to eq(7_200) + expect(oauth_access_tokens_table.find(2).expires_in).to eq(42) + expect(oauth_access_tokens_table.find(3).expires_in).to eq(7_200) + end +end diff --git a/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb b/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb index a111007a984..65d55f85a98 100644 --- a/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb +++ b/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Gitlab::BackgroundMigration::ExtractProjectTopicsIntoSeparateTabl # Tagging records expect { tagging_1.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { tagging_2.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { other_tagging.reload }.not_to raise_error(ActiveRecord::RecordNotFound) + expect { other_tagging.reload }.not_to raise_error expect { tagging_3.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { tagging_4.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { tagging_5.reload }.to raise_error(ActiveRecord::RecordNotFound) diff --git a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb index c1351481505..95847c67d94 100644 --- a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb +++ b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::BackgroundMigration::JobCoordinator do it 'raises an error' do expect do described_class.for_tracking_database('notvalid') - end.to raise_error(ArgumentError, /tracking_database must be one of/) + end.to raise_error(ArgumentError, /must be one of/) end end end diff --git a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb index 254b4fea698..2c2c048992f 100644 --- a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb +++ b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220223124428 do +RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220331133802 do def set_avatar(topic_id, avatar) topic = ::Projects::Topic.find(topic_id) topic.avatar = avatar @@ -16,49 +16,62 @@ RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 202 topics = table(:topics) project_topics = table(:project_topics) - group = namespaces.create!(name: 'group', path: 'group') - project_1 = projects.create!(namespace_id: group.id, visibility_level: 20) - project_2 = projects.create!(namespace_id: group.id, visibility_level: 10) - project_3 = projects.create!(namespace_id: group.id, visibility_level: 0) + group_1 = namespaces.create!(name: 'space1', type: 'Group', path: 'space1') + group_2 = namespaces.create!(name: 'space2', type: 'Group', path: 'space2') + group_3 = namespaces.create!(name: 'space3', type: 'Group', path: 'space3') + proj_space_1 = namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: group_1.id) + proj_space_2 = namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: group_2.id) + proj_space_3 = namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: group_3.id) + project_1 = projects.create!(namespace_id: group_1.id, project_namespace_id: proj_space_1.id, visibility_level: 20) + project_2 = projects.create!(namespace_id: group_2.id, project_namespace_id: proj_space_2.id, visibility_level: 10) + project_3 = projects.create!(namespace_id: group_3.id, project_namespace_id: proj_space_3.id, visibility_level: 0) topic_1_keep = topics.create!( name: 'topic1', + title: 'Topic 1', description: 'description 1 to keep', total_projects_count: 2, non_private_projects_count: 2 ) topic_1_remove = topics.create!( name: 'TOPIC1', + title: 'Topic 1', description: 'description 1 to remove', total_projects_count: 2, non_private_projects_count: 1 ) topic_2_remove = topics.create!( name: 'topic2', + title: 'Topic 2', total_projects_count: 0 ) topic_2_keep = topics.create!( name: 'TOPIC2', + title: 'Topic 2', description: 'description 2 to keep', total_projects_count: 1 ) topic_3_remove_1 = topics.create!( name: 'topic3', + title: 'Topic 3', total_projects_count: 2, non_private_projects_count: 1 ) topic_3_keep = topics.create!( name: 'Topic3', + title: 'Topic 3', total_projects_count: 2, non_private_projects_count: 2 ) topic_3_remove_2 = topics.create!( name: 'TOPIC3', + title: 'Topic 3', description: 'description 3 to keep', total_projects_count: 2, non_private_projects_count: 1 ) topic_4_keep = topics.create!( - name: 'topic4' + name: 'topic4', + title: 'Topic 4' ) project_topics_1 = [] diff --git a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb index 90dd3e14606..e38edfc3643 100644 --- a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb +++ b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, :migration, schema: 20220223112304 do let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:ci_runners) { table(:ci_runners) } - let(:ci_pipelines) { table(:ci_pipelines) } - let(:ci_builds) { table(:ci_builds) } + let(:ci_runners) { table(:ci_runners, database: :ci) } + let(:ci_pipelines) { table(:ci_pipelines, database: :ci) } + let(:ci_builds) { table(:ci_builds, database: :ci) } subject { described_class.new } @@ -26,9 +26,9 @@ RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, :mi describe '#perform' do let(:namespace) { namespaces.create!(name: 'test', path: 'test', type: 'Group') } let(:project) { projects.create!(namespace_id: namespace.id, name: 'test') } - let(:pipeline) { ci_pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success') } it 'nullifies runner_id for orphan ci_builds in range' do + pipeline = ci_pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success') ci_runners.create!(id: 2, runner_type: 'project_type') ci_builds.create!(id: 5, type: 'Ci::Build', commit_id: pipeline.id, runner_id: 2) diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb index d02f7245c15..71020746fa7 100644 --- a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb @@ -6,32 +6,47 @@ RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenEncrypte let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:perform) { described_class.new.perform(1, 4) } + subject(:background_migration) { described_class.new } before do namespaces.create!(id: 123, name: 'sample', path: 'sample') projects.create!(id: 1, namespace_id: 123, runners_token_encrypted: 'duplicate') projects.create!(id: 2, namespace_id: 123, runners_token_encrypted: 'a-runners-token') - projects.create!(id: 3, namespace_id: 123, runners_token_encrypted: 'duplicate') + projects.create!(id: 3, namespace_id: 123, runners_token_encrypted: 'duplicate-2') projects.create!(id: 4, namespace_id: 123, runners_token_encrypted: nil) projects.create!(id: 5, namespace_id: 123, runners_token_encrypted: 'duplicate-2') - projects.create!(id: 6, namespace_id: 123, runners_token_encrypted: 'duplicate-2') + projects.create!(id: 6, namespace_id: 123, runners_token_encrypted: 'duplicate') + projects.create!(id: 7, namespace_id: 123, runners_token_encrypted: 'another-runners-token') + projects.create!(id: 8, namespace_id: 123, runners_token_encrypted: 'another-runners-token') end describe '#up' do - before do - stub_const("#{described_class}::SUB_BATCH_SIZE", 2) - end - it 'nullifies duplicate tokens', :aggregate_failures do - perform + background_migration.perform(1, 2) + background_migration.perform(3, 4) - expect(projects.count).to eq(6) + expect(projects.count).to eq(8) expect(projects.all.pluck(:id, :runners_token_encrypted).to_h).to eq( - { 1 => nil, 2 => 'a-runners-token', 3 => nil, 4 => nil, 5 => 'duplicate-2', 6 => 'duplicate-2' } - ) - expect(projects.pluck(:runners_token_encrypted).uniq).to match_array [nil, 'a-runners-token', 'duplicate-2'] + { + 1 => nil, + 2 => 'a-runners-token', + 3 => nil, + 4 => nil, + 5 => 'duplicate-2', + 6 => 'duplicate', + 7 => 'another-runners-token', + 8 => 'another-runners-token' + }) + expect(projects.pluck(:runners_token_encrypted).uniq).to match_array [ + nil, 'a-runners-token', 'duplicate', 'duplicate-2', 'another-runners-token' + ] + end + + it 'does not touch projects outside id range' do + expect do + background_migration.perform(1, 2) + end.not_to change { projects.where(id: [3..8]).each(&:reload).map(&:updated_at) } end end end diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb index fd61047d851..7d3df69bee2 100644 --- a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb @@ -6,32 +6,47 @@ RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenValuesOn let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:perform) { described_class.new.perform(1, 4) } + subject(:background_migration) { described_class.new } before do namespaces.create!(id: 123, name: 'sample', path: 'sample') projects.create!(id: 1, namespace_id: 123, runners_token: 'duplicate') projects.create!(id: 2, namespace_id: 123, runners_token: 'a-runners-token') - projects.create!(id: 3, namespace_id: 123, runners_token: 'duplicate') + projects.create!(id: 3, namespace_id: 123, runners_token: 'duplicate-2') projects.create!(id: 4, namespace_id: 123, runners_token: nil) projects.create!(id: 5, namespace_id: 123, runners_token: 'duplicate-2') - projects.create!(id: 6, namespace_id: 123, runners_token: 'duplicate-2') + projects.create!(id: 6, namespace_id: 123, runners_token: 'duplicate') + projects.create!(id: 7, namespace_id: 123, runners_token: 'another-runners-token') + projects.create!(id: 8, namespace_id: 123, runners_token: 'another-runners-token') end describe '#up' do - before do - stub_const("#{described_class}::SUB_BATCH_SIZE", 2) - end - it 'nullifies duplicate tokens', :aggregate_failures do - perform + background_migration.perform(1, 2) + background_migration.perform(3, 4) - expect(projects.count).to eq(6) + expect(projects.count).to eq(8) expect(projects.all.pluck(:id, :runners_token).to_h).to eq( - { 1 => nil, 2 => 'a-runners-token', 3 => nil, 4 => nil, 5 => 'duplicate-2', 6 => 'duplicate-2' } - ) - expect(projects.pluck(:runners_token).uniq).to match_array [nil, 'a-runners-token', 'duplicate-2'] + { + 1 => nil, + 2 => 'a-runners-token', + 3 => nil, + 4 => nil, + 5 => 'duplicate-2', + 6 => 'duplicate', + 7 => 'another-runners-token', + 8 => 'another-runners-token' + }) + expect(projects.pluck(:runners_token).uniq).to match_array [ + nil, 'a-runners-token', 'duplicate', 'duplicate-2', 'another-runners-token' + ] + end + + it 'does not touch projects outside id range' do + expect do + background_migration.perform(1, 2) + end.not_to change { projects.where(id: [3..8]).each(&:reload).map(&:updated_at) } end end end diff --git a/spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb b/spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb new file mode 100644 index 00000000000..3f59b0a24a3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/reset_too_many_tags_skipped_registry_imports_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::ResetTooManyTagsSkippedRegistryImports, :migration, + :aggregate_failures, + schema: 20220502173045 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:container_repositories) { table(:container_repositories) } + + subject(:background_migration) { described_class.new } + + let!(:namespace) { namespaces.create!(id: 1, path: 'foo', name: 'foo') } + let!(:project) { projects.create!(id: 1, project_namespace_id: 1, namespace_id: 1, path: 'bar', name: 'bar') } + + let!(:container_repository1) do + container_repositories.create!(id: 1, + project_id: 1, + name: 'a', + migration_state: 'import_skipped', + migration_skipped_at: Time.zone.now, + migration_skipped_reason: 2, + migration_pre_import_started_at: Time.zone.now, + migration_pre_import_done_at: Time.zone.now, + migration_import_started_at: Time.zone.now, + migration_import_done_at: Time.zone.now, + migration_aborted_at: Time.zone.now, + migration_retries_count: 2, + migration_aborted_in_state: 'importing') + end + + let!(:container_repository2) do + container_repositories.create!(id: 2, + project_id: 1, + name: 'b', + migration_state: 'import_skipped', + migration_skipped_at: Time.zone.now, + migration_skipped_reason: 2) + end + + let!(:container_repository3) do + container_repositories.create!(id: 3, + project_id: 1, + name: 'c', + migration_state: 'import_skipped', + migration_skipped_at: Time.zone.now, + migration_skipped_reason: 1) + end + + # This is an unlikely state, but included here to test the edge case + let!(:container_repository4) do + container_repositories.create!(id: 4, + project_id: 1, + name: 'd', + migration_state: 'default', + migration_skipped_reason: 2) + end + + describe '#up' do + it 'resets only qualified container repositories', :aggregate_failures do + background_migration.perform(1, 4) + + expect(container_repository1.reload.migration_state).to eq('default') + expect(container_repository1.migration_skipped_reason).to eq(nil) + expect(container_repository1.migration_pre_import_started_at).to eq(nil) + expect(container_repository1.migration_pre_import_done_at).to eq(nil) + expect(container_repository1.migration_import_started_at).to eq(nil) + expect(container_repository1.migration_import_done_at).to eq(nil) + expect(container_repository1.migration_aborted_at).to eq(nil) + expect(container_repository1.migration_skipped_at).to eq(nil) + expect(container_repository1.migration_retries_count).to eq(0) + expect(container_repository1.migration_aborted_in_state).to eq(nil) + + expect(container_repository2.reload.migration_state).to eq('default') + expect(container_repository2.migration_skipped_reason).to eq(nil) + + expect(container_repository3.reload.migration_state).to eq('import_skipped') + expect(container_repository3.migration_skipped_reason).to eq(1) + + expect(container_repository4.reload.migration_state).to eq('default') + expect(container_repository4.migration_skipped_reason).to eq(2) + end + end +end diff --git a/spec/lib/gitlab/backtrace_cleaner_spec.rb b/spec/lib/gitlab/backtrace_cleaner_spec.rb index e46a90e8606..cdde5a02d3b 100644 --- a/spec/lib/gitlab/backtrace_cleaner_spec.rb +++ b/spec/lib/gitlab/backtrace_cleaner_spec.rb @@ -25,7 +25,6 @@ RSpec.describe Gitlab::BacktraceCleaner do "app/models/repository.rb:113:in `commit'", "lib/gitlab/i18n.rb:50:in `with_locale'", "lib/gitlab/middleware/multipart.rb:95:in `call'", - "lib/gitlab/request_profiler/middleware.rb:14:in `call'", "ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'", "ee/lib/gitlab/jira/middleware.rb:15:in `call'" ] diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index c06d26d1441..d6280d3c28c 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -103,7 +103,7 @@ RSpec.describe Gitlab::Checks::BranchCheck do it 'prevents force push' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect { subject.validate! }.to raise_error + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError) end end end @@ -126,7 +126,7 @@ RSpec.describe Gitlab::Checks::BranchCheck do it 'prevents force push' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect { subject.validate! }.to raise_error + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError) end end @@ -141,7 +141,7 @@ RSpec.describe Gitlab::Checks::BranchCheck do it 'prevents force push' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect { subject.validate! }.to raise_error + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError) end end end diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb index 1cb4edd7337..41ec11c1055 100644 --- a/spec/lib/gitlab/checks/changes_access_spec.rb +++ b/spec/lib/gitlab/checks/changes_access_spec.rb @@ -49,10 +49,17 @@ RSpec.describe Gitlab::Checks::ChangesAccess do context 'when changes contain empty revisions' do let(:expected_commit) { instance_double(Commit) } + let(:expected_allow_quarantine) { allow_quarantine } shared_examples 'returns only commits with non empty revisions' do + before do + stub_feature_flags(filter_quarantined_commits: filter_quarantined_commits) + end + specify do - expect(project.repository).to receive(:new_commits).with([newrev], { allow_quarantine: allow_quarantine }) { [expected_commit] } + expect(project.repository) + .to receive(:new_commits) + .with([newrev], allow_quarantine: expected_allow_quarantine) { [expected_commit] } expect(subject.commits).to match_array([expected_commit]) end end @@ -60,13 +67,37 @@ RSpec.describe Gitlab::Checks::ChangesAccess do it_behaves_like 'returns only commits with non empty revisions' do let(:changes) { [{ oldrev: oldrev, newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] } let(:allow_quarantine) { true } + let(:filter_quarantined_commits) { true } end context 'without oldrev' do - it_behaves_like 'returns only commits with non empty revisions' do - let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] } - # The quarantine directory should not be used because we're lacking oldrev. + let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] } + + context 'with disallowed quarantine' do + # The quarantine directory should not be used because we're lacking + # oldrev, and we're not filtering commits. let(:allow_quarantine) { false } + let(:filter_quarantined_commits) { false } + + it_behaves_like 'returns only commits with non empty revisions' + end + + context 'with allowed quarantine and :filter_quarantined_commits disabled' do + # When we allow usage of the quarantine but have no oldrev and we're + # not filtering commits then results returned by the quarantine aren't + # accurate. We thus mustn't try using it. + let(:allow_quarantine) { true } + let(:filter_quarantined_commits) { false } + let(:expected_allow_quarantine) { false } + + it_behaves_like 'returns only commits with non empty revisions' + end + + context 'with allowed quarantine and :filter_quarantined_commits enabled' do + let(:allow_quarantine) { true } + let(:filter_quarantined_commits) { true } + + it_behaves_like 'returns only commits with non empty revisions' end end end diff --git a/spec/lib/gitlab/checks/single_change_access_spec.rb b/spec/lib/gitlab/checks/single_change_access_spec.rb index e81e4951539..1b34e58797e 100644 --- a/spec/lib/gitlab/checks/single_change_access_spec.rb +++ b/spec/lib/gitlab/checks/single_change_access_spec.rb @@ -96,13 +96,26 @@ RSpec.describe Gitlab::Checks::SingleChangeAccess do let(:provided_commits) { nil } before do + stub_feature_flags(filter_quarantined_commits: filter_quarantined_commits) + expect(project.repository) .to receive(:new_commits) + .with(newrev, allow_quarantine: filter_quarantined_commits) .once .and_return(expected_commits) end - it_behaves_like '#commits' + context 'with :filter_quarantined_commits disabled' do + let(:filter_quarantined_commits) { false } + + it_behaves_like '#commits' + end + + context 'with :filter_quarantined_commits enabled' do + let(:filter_quarantined_commits) { true } + + it_behaves_like '#commits' + end end end end diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb index c9c0d1a744e..f9d23ff97bc 100644 --- a/spec/lib/gitlab/ci/ansi2json_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json_spec.rb @@ -27,6 +27,17 @@ RSpec.describe Gitlab::Ci::Ansi2json do ]) end + it 'ignores empty newlines' do + expect(convert_json("Hello\n\nworld")).to eq([ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 7, content: [{ text: 'world' }] } + ]) + expect(convert_json("Hello\r\n\r\nworld")).to eq([ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 9, content: [{ text: 'world' }] } + ]) + end + it 'replace the current line when encountering \r' do expect(convert_json("Hello\rworld")).to eq([ { offset: 0, content: [{ text: 'world' }] } diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb new file mode 100644 index 00000000000..81bce989833 --- /dev/null +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' + +RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If do + include StubFeatureFlags + + subject(:if_clause) { described_class.new(expression) } + + describe '#satisfied_by?' do + let(:context_class) { Gitlab::Ci::Build::Context::Base } + let(:rules_context) { instance_double(context_class, variables_hash: {}) } + + subject(:satisfied_by?) { if_clause.satisfied_by?(nil, rules_context) } + + context 'when expression is a basic string comparison' do + context 'when comparison is true' do + let(:expression) { '"value" == "value"' } + + it { is_expected.to eq(true) } + end + + context 'when comparison is false' do + let(:expression) { '"value" == "other"' } + + it { is_expected.to eq(false) } + end + end + + context 'when expression is a regexp' do + context 'when comparison is true' do + let(:expression) { '"abcde" =~ /^ab.*/' } + + it { is_expected.to eq(true) } + end + + context 'when comparison is false' do + let(:expression) { '"abcde" =~ /^af.*/' } + + it { is_expected.to eq(false) } + end + + context 'when both side of the expression are variables' do + let(:expression) { '$teststring =~ $pattern' } + + context 'when comparison is true' do + let(:rules_context) do + instance_double(context_class, variables_hash: { 'teststring' => 'abcde', 'pattern' => '/^ab.*/' }) + end + + it { is_expected.to eq(true) } + + context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do + before do + stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when comparison is false' do + let(:rules_context) do + instance_double(context_class, variables_hash: { 'teststring' => 'abcde', 'pattern' => '/^af.*/' }) + end + + it { is_expected.to eq(false) } + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index 37bfdca4d1d..e82dcd0254d 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -188,6 +188,19 @@ RSpec.describe Gitlab::Ci::Build::Rules do it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, { MY_VAR: 'my var' })) } end end + + context 'with a regexp variable matching rule' do + let(:rule_list) { [{ if: '"abcde" =~ $pattern' }] } + + before do + allow(ci_build).to receive(:scoped_variables).and_return( + Gitlab::Ci::Variables::Collection.new + .append(key: 'pattern', value: '/^ab.*/', public: true) + ) + end + + it { is_expected.to eq(described_class::Result.new('on_success')) } + end end describe 'Gitlab::Ci::Build::Rules::Result' do diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index dd8a79f0d84..36c26c8ee4f 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -92,24 +92,18 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do end context 'when valid action is used' do - let(:config) do - { name: 'production', - action: 'start' } - end - - it 'is valid' do - expect(entry).to be_valid + where(:action) do + %w(start stop prepare verify access) end - end - context 'when prepare action is used' do - let(:config) do - { name: 'production', - action: 'prepare' } - end + with_them do + let(:config) do + { name: 'production', action: action } + end - it 'is valid' do - expect(entry).to be_valid + it 'is valid' do + expect(entry).to be_valid + end end end @@ -148,7 +142,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do describe '#errors' do it 'contains error about invalid action' do expect(entry.errors) - .to include 'environment action should be start, stop or prepare' + .to include 'environment action should be start, stop, prepare, verify, or access' end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 97691504abd..ca336c3ecaa 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do subject { described_class.nodes.keys } let(:result) do - %i[before_script script stage type after_script cache + %i[before_script script stage after_script cache image services only except rules needs variables artifacts environment coverage retry interruptible timeout release tags inherit parallel] diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 061d8f34c8d..051cccb4833 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -45,10 +45,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do :load_performance | 'load-performance.json' :lsif | 'lsif.json' :dotenv | 'build.dotenv' - :cobertura | 'cobertura-coverage.xml' :terraform | 'tfplan.json' :accessibility | 'gl-accessibility.json' - :cluster_applications | 'gl-cluster-applications.json' end with_them do @@ -90,18 +88,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do expect(entry.value).to eq({ coverage_report: coverage_report, dast: ['gl-dast-report.json'] }) end end - - context 'and a direct coverage report format is specified' do - let(:config) { { coverage_report: coverage_report, cobertura: 'cobertura-coverage.xml' } } - - it 'is not valid' do - expect(entry).not_to be_valid - end - - it 'reports error' do - expect(entry.errors).to include /please use only one the following keys: coverage_report, cobertura/ - 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 b9c32bc51be..55ad119ea21 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do # The purpose of `Root` is have only globally defined configuration. expect(described_class.nodes.keys) .to match_array(%i[before_script image services after_script - variables cache stages types include default workflow]) + variables cache stages include default workflow]) end end end @@ -55,41 +55,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do } end - context 'when deprecated types/type keywords are defined' do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - let(:hash) do - { types: %w(test deploy), - rspec: { script: 'rspec', type: 'test' } } - end - - before do - root.compose! - end - - it 'returns array of types as stages with a warning' do - expect(root.jobs_value[:rspec][:stage]).to eq 'test' - expect(root.stages_value).to eq %w[test deploy] - expect(root.warnings).to match_array([ - "root `types` is deprecated in 9.0 and will be removed in 15.0.", - "jobs:rspec `type` is deprecated in 9.0 and will be removed in 15.0." - ]) - end - - it 'logs usage of keywords' do - expect(Gitlab::AppJsonLogger).to( - receive(:info) - .with(event: 'ci_used_deprecated_keyword', - entry: root[:stages].key.to_s, - user_id: user.id, - project_id: project.id) - ) - - root.compose! - end - end - describe '#compose!' do before do root.compose! diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index c0a0b0009ce..0e78498c98e 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -199,6 +199,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local do context_sha: '12345', type: :local, location: location, + blob: "http://localhost/#{project.full_path}/-/blob/12345/lib/gitlab/ci/templates/existent-file.yml", + raw: "http://localhost/#{project.full_path}/-/raw/12345/lib/gitlab/ci/templates/existent-file.yml", extra: {} ) } diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 5d3412a148b..77e542cf933 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -207,6 +207,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do context_sha: '12345', type: :file, location: '/file.yml', + blob: "http://localhost/#{project.full_path}/-/blob/#{project.commit('master').id}/file.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{project.commit('master').id}/file.yml", extra: { project: project.full_path, ref: 'HEAD' } ) } @@ -227,6 +229,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do context_sha: '12345', type: :file, location: '/file.yml', + blob: nil, + raw: nil, extra: { project: 'xxxxxxxxxxxxxxxxxxxxxxxx', ref: 'xxxxxxxxxxxxxxxxxxxxxxxx' } ) } diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index 5c07c87fd5a..3e1c4df4e32 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -213,6 +213,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do context_sha: '12345', type: :remote, location: 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml', + raw: 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml', + blob: nil, extra: {} ) } diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb index 4da9a933a9f..074e7a1d32d 100644 --- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb @@ -124,6 +124,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Template do context_sha: '12345', type: :template, location: template, + raw: "https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/#{template}", + blob: nil, extra: {} ) } diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 56cd006717e..15a0ff40aa4 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -267,11 +267,41 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do perform expect(context.includes).to contain_exactly( - { type: :local, location: '/local/file.yml', extra: {}, context_project: project.full_path, context_sha: '12345' }, - { type: :template, location: 'Ruby.gitlab-ci.yml', extra: {}, context_project: project.full_path, context_sha: '12345' }, - { type: :remote, location: 'http://my.domain.com/config.yml', extra: {}, context_project: project.full_path, context_sha: '12345' }, - { type: :file, location: '/templates/my-workflow.yml', extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: '12345' }, - { type: :local, location: '/templates/my-build.yml', extra: {}, context_project: another_project.full_path, context_sha: another_project.commit.sha } + { type: :local, + location: '/local/file.yml', + blob: "http://localhost/#{project.full_path}/-/blob/12345/local/file.yml", + raw: "http://localhost/#{project.full_path}/-/raw/12345/local/file.yml", + extra: {}, + context_project: project.full_path, + context_sha: '12345' }, + { type: :template, + location: 'Ruby.gitlab-ci.yml', + blob: nil, + raw: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml', + extra: {}, + context_project: project.full_path, + context_sha: '12345' }, + { type: :remote, + location: 'http://my.domain.com/config.yml', + blob: nil, + raw: "http://my.domain.com/config.yml", + extra: {}, + context_project: project.full_path, + context_sha: '12345' }, + { type: :file, + location: '/templates/my-workflow.yml', + blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-workflow.yml", + raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-workflow.yml", + extra: { project: another_project.full_path, ref: 'HEAD' }, + context_project: project.full_path, + context_sha: '12345' }, + { type: :local, + location: '/templates/my-build.yml', + blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml", + raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml", + extra: {}, + context_project: another_project.full_path, + context_sha: another_project.commit.sha } ) end end @@ -394,8 +424,20 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do perform expect(context.includes).to contain_exactly( - { type: :file, location: '/templates/my-build.yml', extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: '12345' }, - { type: :file, location: '/templates/my-test.yml', extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: '12345' } + { type: :file, + location: '/templates/my-build.yml', + blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml", + raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml", + extra: { project: another_project.full_path, ref: 'HEAD' }, + context_project: project.full_path, + context_sha: '12345' }, + { type: :file, + blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-test.yml", + raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-test.yml", + location: '/templates/my-test.yml', + extra: { project: another_project.full_path, ref: 'HEAD' }, + context_project: project.full_path, + context_sha: '12345' } ) end end @@ -438,8 +480,20 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do perform expect(context.includes).to contain_exactly( - { type: :local, location: 'myfolder/file1.yml', extra: {}, context_project: project.full_path, context_sha: '12345' }, - { type: :local, location: 'myfolder/file2.yml', extra: {}, context_project: project.full_path, context_sha: '12345' } + { type: :local, + location: 'myfolder/file1.yml', + blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file1.yml", + raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file1.yml", + extra: {}, + context_project: project.full_path, + context_sha: '12345' }, + { type: :local, + blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file2.yml", + raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file2.yml", + location: 'myfolder/file2.yml', + extra: {}, + context_project: project.full_path, + context_sha: '12345' } ) end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 3ba6a9059c6..5eb04d969eb 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -109,16 +109,22 @@ RSpec.describe Gitlab::Ci::Config do expect(config.metadata[:includes]).to contain_exactly( { type: :template, location: 'Jobs/Deploy.gitlab-ci.yml', + blob: nil, + raw: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml', extra: {}, context_project: nil, context_sha: nil }, { type: :template, location: 'Jobs/Build.gitlab-ci.yml', + blob: nil, + raw: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml', extra: {}, context_project: nil, context_sha: nil }, { type: :remote, location: 'https://example.com/gitlab-ci.yml', + blob: nil, + raw: 'https://example.com/gitlab-ci.yml', extra: {}, context_project: nil, context_sha: nil } @@ -428,16 +434,22 @@ RSpec.describe Gitlab::Ci::Config do expect(config.metadata[:includes]).to contain_exactly( { type: :local, location: local_location, + blob: "http://localhost/#{project.full_path}/-/blob/12345/#{local_location}", + raw: "http://localhost/#{project.full_path}/-/raw/12345/#{local_location}", extra: {}, context_project: project.full_path, context_sha: '12345' }, { type: :remote, location: remote_location, + blob: nil, + raw: remote_location, extra: {}, context_project: project.full_path, context_sha: '12345' }, { type: :file, location: '.gitlab-ci.yml', + blob: "http://localhost/#{main_project.full_path}/-/blob/#{main_project.commit.sha}/.gitlab-ci.yml", + raw: "http://localhost/#{main_project.full_path}/-/raw/#{main_project.commit.sha}/.gitlab-ci.yml", extra: { project: main_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: '12345' } diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 747ff13c840..7e0b2b5aa8e 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Lint do end end - shared_examples 'sets merged yaml' do + shared_examples 'sets config metadata' do let(:content) do <<~YAML :include: @@ -106,6 +106,20 @@ RSpec.describe Gitlab::Ci::Lint do expect(subject.merged_yaml).to eq(expected_config.to_yaml) end + + it 'sets includes' do + expect(subject.includes).to contain_exactly( + { + type: :local, + location: 'another-gitlab-ci.yml', + blob: "http://localhost/#{project.full_path}/-/blob/#{project.commit.sha}/another-gitlab-ci.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{project.commit.sha}/another-gitlab-ci.yml", + extra: {}, + context_project: project.full_path, + context_sha: project.commit.sha + } + ) + end end shared_examples 'content with errors and warnings' do @@ -220,7 +234,7 @@ RSpec.describe Gitlab::Ci::Lint do end end - it_behaves_like 'sets merged yaml' + it_behaves_like 'sets config metadata' include_context 'advanced validations' do it 'does not catch advanced logical errors' do @@ -275,7 +289,7 @@ RSpec.describe Gitlab::Ci::Lint do end end - it_behaves_like 'sets merged yaml' + it_behaves_like 'sets config metadata' include_context 'advanced validations' do it 'runs advanced logical validations' do diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index dfc5dec1481..6495d1f654b 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -292,7 +292,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do expect(scans.map(&:status).all?('success')).to be(true) expect(scans.map(&:start_time).all?('placeholder-value')).to be(true) expect(scans.map(&:end_time).all?('placeholder-value')).to be(true) - expect(scans.size).to eq(3) + expect(scans.size).to eq(7) expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) end @@ -348,22 +348,29 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do it 'returns links object for each finding', :aggregate_failures do links = report.findings.flat_map(&:links) - expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030']) - expect(links.map(&:name)).to match_array([nil, 'CVE-1030']) - expect(links.size).to eq(2) + expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030', + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140"]) + expect(links.map(&:name)).to match_array([nil, nil, nil, nil, nil, 'CVE-1030']) + expect(links.size).to eq(6) expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link) end end describe 'parsing evidence' do - it 'returns evidence object for each finding', :aggregate_failures do - evidences = report.findings.map(&:evidence) + RSpec::Matchers.define_negated_matcher :have_values, :be_empty - expect(evidences.first.data).not_to be_empty - expect(evidences.first.data["summary"]).to match(/The Origin header was changed/) - expect(evidences.size).to eq(3) - expect(evidences.compact.size).to eq(2) - expect(evidences.first).to be_a(::Gitlab::Ci::Reports::Security::Evidence) + it 'returns evidence object for each finding', :aggregate_failures do + all_evidences = report.findings.map(&:evidence) + evidences = all_evidences.compact + data = evidences.map(&:data) + summaries = evidences.map { |e| e.data["summary"] } + + expect(all_evidences.size).to eq(7) + expect(evidences.size).to eq(2) + expect(evidences).to all( be_a(::Gitlab::Ci::Reports::Security::Evidence) ) + expect(data).to all( have_values ) + expect(summaries).to all( match(/The Origin header was changed/) ) end end diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index f6409c8b01f..d06077d69b6 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let_it_be(:project) { create(:project) } + let(:supported_dast_versions) { described_class::SUPPORTED_VERSIONS[:dast].join(', ') } + let(:scanner) do { 'id' => 'gemnasium', @@ -22,7 +24,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do expect(described_class::SUPPORTED_VERSIONS.keys).to eq(described_class::DEPRECATED_VERSIONS.keys) end - context 'files under schema path are explicitly listed' do + context 'when a schema JSON file exists for a particular report type version' do # We only care about the part that comes before report-format.json # https://rubular.com/r/N8Juz7r8hYDYgD filename_regex = /(?<report_type>[-\w]*)\-report-format.json/ @@ -36,14 +38,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do matches = filename_regex.match(file) report_type = matches[:report_type].tr("-", "_").to_sym - it "#{report_type} #{version}" do + it "#{report_type} #{version} is in the constant" do expect(described_class::SUPPORTED_VERSIONS[report_type]).to include(version) end end end end - context 'every SUPPORTED_VERSION has a corresponding JSON file' do + context 'when every SUPPORTED_VERSION has a corresponding JSON file' do described_class::SUPPORTED_VERSIONS.each_key do |report_type| # api_fuzzing is covered by DAST schema next if report_type == :api_fuzzing @@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -77,7 +79,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_truthy } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version @@ -104,9 +106,19 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do context 'when given a deprecated schema version' do let(:report_type) { :dast } + let(:deprecations_hash) do + { + dast: %w[10.0.0] + } + end + let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } - context 'and the report passes schema validation' do + before do + stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) + end + + context 'when the report passes schema validation' do let(:report_data) do { 'version' => '10.0.0', @@ -131,8 +143,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end - context 'and the report does not pass schema validation' do - context 'and enforce_security_report_validation is enabled' do + context 'when the report does not pass schema validation' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end @@ -146,7 +158,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_falsey } end - context 'and enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end @@ -166,12 +178,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'if enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -196,14 +208,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - context 'and scanner information is empty' do + context 'when scanner information is empty' do let(:scanner) { {} } it 'logs related information' do @@ -235,12 +247,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end - context 'if enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -251,7 +263,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_truthy } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version @@ -262,6 +274,30 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end end + + context 'when not given a schema version' do + let(:report_type) { :dast } + let(:report_version) { nil } + let(:report_data) do + { + 'vulnerabilities' => [] + } + end + + before do + stub_feature_flags(enforce_security_report_validation: true) + end + + it { is_expected.to be_falsey } + + context 'when enforce_security_report_validation is disabled' do + before do + stub_feature_flags(enforce_security_report_validation: false) + end + + it { is_expected.to be_truthy } + end + end end describe '#errors' do @@ -271,7 +307,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -279,19 +315,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_errors) { [] } - - it { is_expected.to match_array(expected_errors) } + it { is_expected.to be_empty } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - context 'if enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: project) end @@ -305,23 +339,31 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to match_array(expected_errors) } end - context 'if enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end - let(:expected_errors) { [] } - - it { is_expected.to match_array(expected_errors) } + it { is_expected.to be_empty } end end end context 'when given a deprecated schema version' do let(:report_type) { :dast } + let(:deprecations_hash) do + { + dast: %w[10.0.0] + } + end + let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } - context 'and the report passes schema validation' do + before do + stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) + end + + context 'when the report passes schema validation' do let(:report_data) do { 'version' => '10.0.0', @@ -329,13 +371,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_errors) { [] } - - it { is_expected.to match_array(expected_errors) } + it { is_expected.to be_empty } end - context 'and the report does not pass schema validation' do - context 'and enforce_security_report_validation is enabled' do + context 'when the report does not pass schema validation' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end @@ -356,7 +396,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to match_array(expected_errors) } end - context 'and enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end @@ -367,9 +407,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_errors) { [] } - - it { is_expected.to match_array(expected_errors) } + it { is_expected.to be_empty } end end end @@ -378,12 +416,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'if enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -393,14 +431,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1" + "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}" ] end it { is_expected.to match_array(expected_errors) } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version @@ -409,7 +447,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1", + "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}", "root is missing required keys: vulnerabilities" ] end @@ -418,12 +456,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end - context 'if enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -431,22 +469,45 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_errors) { [] } - - it { is_expected.to match_array(expected_errors) } + it { is_expected.to be_empty } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - let(:expected_errors) { [] } + it { is_expected.to be_empty } + end + end + end - it { is_expected.to match_array(expected_errors) } + context 'when not given a schema version' do + let(:report_type) { :dast } + let(:report_version) { nil } + let(:report_data) do + { + 'vulnerabilities' => [] + } + end + + let(:expected_errors) do + [ + "root is missing required keys: version", + "Report version not provided, dast report type supports versions: #{supported_dast_versions}" + ] + end + + it { is_expected.to match_array(expected_errors) } + + context 'when enforce_security_report_validation is disabled' do + before do + stub_feature_flags(enforce_security_report_validation: false) end + + it { is_expected.to be_empty } end end end @@ -458,9 +519,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - let(:expected_deprecation_warnings) { [] } - - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -468,30 +527,40 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - it { is_expected.to match_array(expected_deprecation_warnings) } + it { is_expected.to be_empty } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - it { is_expected.to match_array(expected_deprecation_warnings) } + it { is_expected.to be_empty } end end context 'when given a deprecated schema version' do let(:report_type) { :dast } + let(:deprecations_hash) do + { + dast: %w[V2.7.0] + } + end + let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } let(:expected_deprecation_warnings) do [ - "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1" + "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}" ] end - context 'and the report passes schema validation' do + before do + stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) + end + + context 'when the report passes schema validation' do let(:report_data) do { 'version' => report_version, @@ -502,7 +571,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to match_array(expected_deprecation_warnings) } end - context 'and the report does not pass schema validation' do + context 'when the report does not pass schema validation' do let(:report_data) do { 'version' => 'V2.7.0' @@ -535,7 +604,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -543,29 +612,25 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - context 'if enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: project) end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end - context 'if enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end @@ -583,38 +648,44 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do context 'when given a deprecated schema version' do let(:report_type) { :dast } + let(:deprecations_hash) do + { + dast: %w[V2.7.0] + } + end + let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } - context 'and the report passes schema validation' do + before do + stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) + end + + context 'when the report passes schema validation' do let(:report_data) do { 'vulnerabilities' => [] } end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end - context 'and the report does not pass schema validation' do + context 'when the report does not pass schema validation' do let(:report_data) do { 'version' => 'V2.7.0' } end - context 'and enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end - context 'and enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end @@ -635,12 +706,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'if enforce_security_report_validation is enabled' do + context 'when enforce_security_report_validation is enabled' do before do stub_feature_flags(enforce_security_report_validation: true) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -648,30 +719,26 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version } end - let(:expected_warnings) { [] } - - it { is_expected.to match_array(expected_warnings) } + it { is_expected.to be_empty } end end - context 'if enforce_security_report_validation is disabled' do + context 'when enforce_security_report_validation is disabled' do before do stub_feature_flags(enforce_security_report_validation: false) end - context 'and the report is valid' do + context 'when the report is valid' do let(:report_data) do { 'version' => report_version, @@ -681,14 +748,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_warnings) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1" + "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}" ] end it { is_expected.to match_array(expected_warnings) } end - context 'and the report is invalid' do + context 'when the report is invalid' do let(:report_data) do { 'version' => report_version @@ -697,7 +764,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_warnings) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1", + "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}", "root is missing required keys: vulnerabilities" ] end @@ -706,5 +773,32 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end end + + context 'when not given a schema version' do + let(:report_type) { :dast } + let(:report_version) { nil } + let(:report_data) do + { + 'vulnerabilities' => [] + } + end + + it { is_expected.to be_empty } + + context 'when enforce_security_report_validation is disabled' do + before do + stub_feature_flags(enforce_security_report_validation: false) + end + + let(:expected_warnings) do + [ + "root is missing required keys: version", + "Report version not provided, dast report type supports versions: #{supported_dast_versions}" + ] + end + + it { is_expected.to match_array(expected_warnings) } + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index 25e81f6d538..b570f2a7f75 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do create(:ci_build, :interruptible, :running, pipeline: child_pipeline) end - not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::BUILD_STARTED_RUNNING_STATUSES + not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES context 'when the jobs are cancelable' do cancelable_not_started_statuses = Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES) cancelable_not_started_statuses.each do |status| diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb index 1aa104310af..431073b5a09 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -87,7 +87,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do it 'logs the error' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( instance_of(Gitlab::Ci::Limit::LimitExceededError), - project_id: project.id, plan: namespace.actual_plan_name + { project_id: project.id, plan: namespace.actual_plan_name } ) perform diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb index 0da04d8dcf7..83742699d3d 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' require_dependency 're2' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do + include StubFeatureFlags + let(:left) { double('left') } let(:right) { double('right') } @@ -148,5 +152,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do it { is_expected.to eq(false) } end + + context 'when right value is a regexp string' do + let(:right_value) { '/^ab.*/' } + + context 'when matching' do + let(:left_value) { 'abcde' } + + it { is_expected.to eq(true) } + + context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do + before do + stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when not matching' do + let(:left_value) { 'dfg' } + + it { is_expected.to eq(false) } + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb index 9bff2355d58..aad33106647 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' require_dependency 're2' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do + include StubFeatureFlags + let(:left) { double('left') } let(:right) { double('right') } @@ -148,5 +152,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do it { is_expected.to eq(true) } end + + context 'when right value is a regexp string' do + let(:right_value) { '/^ab.*/' } + + context 'when matching' do + let(:left_value) { 'abcde' } + + it { is_expected.to eq(false) } + + context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do + before do + stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false) + end + + it { is_expected.to eq(true) } + end + end + + context 'when not matching' do + let(:left_value) { 'dfg' } + + it { is_expected.to eq(true) } + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb index fa4f8a20984..be205395b69 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -1,8 +1,32 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do + describe '#initialize' do + context 'when the value is a valid regular expression' do + it 'initializes the pattern' do + pattern = described_class.new('/foo/') + + expect(pattern.value).to eq('/foo/') + end + end + + context 'when the value is a valid regular expression with escaped slashes' do + it 'initializes the pattern' do + pattern = described_class.new('/foo\\/bar/') + + expect(pattern.value).to eq('/foo/bar/') + end + end + + context 'when the value is not a valid regular expression' do + it 'raises an error' do + expect { described_class.new('foo') }.to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError) + end + end + end + describe '.build' do it 'creates a new instance of the token' do expect(described_class.build('/.*/')) @@ -15,6 +39,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do end end + describe '.build_and_evaluate' do + context 'when the value is a valid regular expression' do + it 'returns the value as a Gitlab::UntrustedRegexp' do + expect(described_class.build_and_evaluate('/foo/')) + .to eq(Gitlab::UntrustedRegexp.new('foo')) + end + end + + context 'when the value is a Gitlab::UntrustedRegexp' do + it 'returns the value itself' do + expect(described_class.build_and_evaluate(Gitlab::UntrustedRegexp.new('foo'))) + .to eq(Gitlab::UntrustedRegexp.new('foo')) + end + end + + context 'when the value is not a valid regular expression' do + it 'returns the value itself' do + expect(described_class.build_and_evaluate('foo')) + .to eq('foo') + end + end + end + describe '.type' do it 'is a value lexeme' do expect(described_class.type).to eq :value diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 84713e2a798..bbd11a00149 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do .to_hash end - subject do + subject(:statement) do described_class.new(text, variables) end @@ -29,6 +29,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do describe '#evaluate' do using RSpec::Parameterized::TableSyntax + subject(:evaluate) { statement.evaluate } + where(:expression, :value) do '$PRESENT_VARIABLE == "my variable"' | true '"my variable" == $PRESENT_VARIABLE' | true @@ -125,7 +127,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do let(:text) { expression } it "evaluates to `#{params[:value].inspect}`" do - expect(subject.evaluate).to eq(value) + expect(evaluate).to eq(value) end end end @@ -133,6 +135,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do describe '#truthful?' do using RSpec::Parameterized::TableSyntax + subject(:truthful?) { statement.truthful? } + where(:expression, :value) do '$PRESENT_VARIABLE == "my variable"' | true "$PRESENT_VARIABLE == 'no match'" | false @@ -151,7 +155,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do let(:text) { expression } it "returns `#{params[:value].inspect}`" do - expect(subject.truthful?).to eq value + expect(truthful?).to eq value end end @@ -159,10 +163,41 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do let(:text) { '$PRESENT_VARIABLE' } it 'returns false' do - allow(subject).to receive(:evaluate) + allow(statement).to receive(:evaluate) .and_raise(described_class::StatementError) - expect(subject.truthful?).to be_falsey + expect(truthful?).to be_falsey + end + end + + context 'when variables have patterns' do + let(:variables) do + Gitlab::Ci::Variables::Collection.new + .append(key: 'teststring', value: 'abcde') + .append(key: 'pattern1', value: '/^ab.*/') + .append(key: 'pattern2', value: '/^at.*/') + .to_hash + end + + where(:expression, :ff, :result) do + '$teststring =~ "abcde"' | true | true + '$teststring =~ "abcde"' | false | true + '$teststring =~ $teststring' | true | true + '$teststring =~ $teststring' | false | true + '$teststring =~ $pattern1' | true | true + '$teststring =~ $pattern1' | false | false + '$teststring =~ $pattern2' | true | false + '$teststring =~ $pattern2' | false | false + end + + with_them do + let(:text) { expression } + + before do + stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: ff) + end + + it { is_expected.to eq(result) } end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb index 9f7281fb714..51185be3e74 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb @@ -90,29 +90,22 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do end end - context 'when job has environment attribute with stop action' do - let(:attributes) do - { - environment: 'production', - options: { environment: { name: 'production', action: 'stop' } } - } - end - - it 'returns nothing' do - is_expected.to be_nil + context 'when job does not start environment' do + where(:action) do + %w(stop prepare verify access) end - end - context 'when job has environment attribute with prepare action' do - let(:attributes) do - { - environment: 'production', - options: { environment: { name: 'production', action: 'prepare' } } - } - end + with_them do + let(:attributes) do + { + environment: 'production', + options: { environment: { name: 'production', action: action } } + } + end - it 'returns nothing' do - is_expected.to be_nil + it 'returns nothing' do + is_expected.to be_nil + end end end diff --git a/spec/lib/gitlab/ci/reports/security/scanner_spec.rb b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb index eb406e01b24..d7ac82e3b53 100644 --- a/spec/lib/gitlab/ci/reports/security/scanner_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb @@ -103,8 +103,6 @@ RSpec.describe Gitlab::Ci::Reports::Security::Scanner do context 'when the `external_id` of the scanners are different' do where(:scanner_1_attributes, :scanner_2_attributes, :expected_comparison_result) do - { external_id: 'bundler_audit', name: 'foo', vendor: 'bar' } | { external_id: 'retire.js', name: 'foo', vendor: 'bar' } | -1 - { external_id: 'retire.js', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium', name: 'foo', vendor: 'bar' } | -1 { external_id: 'gemnasium', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium-maven', name: 'foo', vendor: 'bar' } | -1 { external_id: 'gemnasium-maven', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium-python', name: 'foo', vendor: 'bar' } | -1 { external_id: 'gemnasium-python', name: 'foo', vendor: 'bar' } | { external_id: 'bandit', name: 'foo', vendor: 'bar' } | 1 diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index b430da376dd..f2b4e7573c0 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -22,8 +22,8 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with nil runner_version' do let(:runner_version) { nil } - it 'raises :unknown' do - is_expected.to eq(:unknown) + it 'returns :invalid' do + is_expected.to eq(:invalid) end end diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb index 0f97bc06a4e..85516d0bbb0 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do - subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/SAST-IaC.latest') } +RSpec.describe 'Jobs/SAST-IaC.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/SAST-IaC') } describe 'the created pipeline' do let_it_be(:project) { create(:project, :repository) } diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..0f97bc06a4e --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/SAST-IaC.latest') } + + describe 'the created pipeline' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.first_owner } + + let(:default_branch) { 'main' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| + allow(instance).to receive(:perform).and_return(true) + end + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on feature branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the kics-iac-sast job' do + expect(build_names).to contain_exactly('kics-iac-sast') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request).payload } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'SAST_DISABLED is set' do + before do + create(:ci_variable, key: 'SAST_DISABLED', value: 'true', project: project) + end + + context 'on default branch' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on feature branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb index a12d69b67a6..432040c4a14 100644 --- a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb +++ b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'MATLAB.gitlab-ci.yml' do end it 'creates all jobs' do - expect(build_names).to include('command', 'test', 'test_artifacts_job') + expect(build_names).to include('command', 'test', 'test_artifacts') end 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 index 5e9224cebd9..eca79f37779 100644 --- 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 @@ -16,7 +16,6 @@ RSpec.describe 'Terraform/Base.gitlab-ci.yml' do 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 diff --git a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb deleted file mode 100644 index 14aaf717453..00000000000 --- a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Managed-Cluster-Applications.gitlab-ci.yml' do - subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Managed-Cluster-Applications') } - - describe 'the created pipeline' do - let_it_be(:user) { create(:user) } - - let(:project) { create(:project, :custom_repo, namespace: user.namespace, files: { 'README.md' => '' }) } - let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } - let(:build_names) { pipeline.builds.pluck(:name) } - let(:default_branch) { project.default_branch_or_main } - let(:pipeline_branch) { default_branch } - - before do - stub_ci_pipeline_yaml_file(template.content) - end - - context 'for a default branch' do - it 'creates a apply job' do - expect(build_names).to match_array('apply') - end - end - - context 'outside of default branch' do - let(:pipeline_branch) { 'a_branch' } - - before do - project.repository.create_branch(pipeline_branch, default_branch) - end - - it 'has no jobs' do - expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.') - end - end - end -end diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index ca096fcecc4..36c6e805bdf 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -7,10 +7,9 @@ RSpec.describe 'CI YML Templates' do let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) } let(:excluded_templates) do - excluded = all_templates.select do |name| + all_templates.select do |name| Gitlab::Template::GitlabCiYmlTemplate.excluded_patterns.any? { |pattern| pattern.match?(name) } end - excluded + ["Terraform.gitlab-ci.yml"] end shared_examples 'require default stages to be included' do diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb index 346ab9f7af7..2fc4b509aab 100644 --- a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb @@ -20,13 +20,16 @@ RSpec.describe 'Terraform.gitlab-ci.yml' do before do stub_ci_pipeline_yaml_file(template.content) + allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| + allow(instance).to receive(:perform).and_return(true) + end allow(project).to receive(:default_branch).and_return(default_branch) end context 'on master branch' do it 'creates init, validate and build jobs', :aggregate_failures do expect(pipeline.errors).to be_empty - expect(build_names).to include('init', 'validate', 'build', 'deploy') + expect(build_names).to include('validate', 'build', 'deploy') 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 index 6c06403adff..42e56c4ab3c 100644 --- 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 @@ -20,7 +20,9 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do before do stub_ci_pipeline_yaml_file(template.content) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| + allow(instance).to receive(:perform).and_return(true) + end allow(project).to receive(:default_branch).and_return(default_branch) end diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index b9aa5f7c431..e13a0993fa8 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -246,7 +246,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder do subject { builder.kubernetes_variables(environment: nil, job: job) } before do - allow(Ci::GenerateKubeconfigService).to receive(:new).with(job).and_return(service) + allow(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token).and_return(service) end it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) } diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 25705fd4260..8416501e949 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -12,8 +12,8 @@ module Gitlab 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 } + describe '#config_metadata' do + subject(:config_metadata) { result.config_metadata } let(:config_content) do YAML.dump( @@ -33,11 +33,23 @@ module Gitlab end it 'returns expanded yaml config' do - expanded_config = YAML.safe_load(merged_yaml, [Symbol]) + expanded_config = YAML.safe_load(config_metadata[:merged_yaml], [Symbol]) included_config = YAML.safe_load(included_yml, [Symbol]) expect(expanded_config).to include(*included_config.keys) end + + it 'returns includes' do + expect(config_metadata[:includes]).to contain_exactly( + { type: :remote, + location: 'https://example.com/sample.yml', + blob: nil, + raw: 'https://example.com/sample.yml', + extra: {}, + context_project: nil, + context_sha: nil } + ) + end end describe '#yaml_variables_for' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 9b68ee2d6a2..1910057622b 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -630,7 +630,7 @@ module Gitlab describe 'only / except policies validations' do context 'when `only` has an invalid value' do - let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } + let(:config) { { rspec: { script: "rspec", stage: "test", only: only } } } subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } @@ -2606,19 +2606,19 @@ module Gitlab end context 'returns errors if job stage is not a string' do - let(:config) { YAML.dump({ rspec: { script: "test", type: 1 } }) } + let(:config) { YAML.dump({ rspec: { script: "test", stage: 1 } }) } - it_behaves_like 'returns errors', 'jobs:rspec:type config should be a string' + it_behaves_like 'returns errors', 'jobs:rspec:stage config should be a string' end context 'returns errors if job stage is not a pre-defined stage' do - let(:config) { YAML.dump({ rspec: { script: "test", type: "acceptance" } }) } + let(:config) { YAML.dump({ rspec: { script: "test", stage: "acceptance" } }) } it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post' end context 'returns errors if job stage is not a defined stage' do - let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", type: "acceptance" } }) } + let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", stage: "acceptance" } }) } it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, .post' end diff --git a/spec/lib/gitlab/color_spec.rb b/spec/lib/gitlab/color_spec.rb index 8b16e13fa4d..28719aa6199 100644 --- a/spec/lib/gitlab/color_spec.rb +++ b/spec/lib/gitlab/color_spec.rb @@ -24,6 +24,48 @@ RSpec.describe Gitlab::Color do end end + describe '.color_for' do + subject { described_class.color_for(value) } + + shared_examples 'deterministic' do + it 'is deterministoc' do + expect(subject.to_s).to eq(described_class.color_for(value).to_s) + end + end + + context 'when generating color for nil value' do + let(:value) { nil } + + specify { is_expected.to be_valid } + + it_behaves_like 'deterministic' + end + + context 'when generating color for empty string value' do + let(:value) { '' } + + specify { is_expected.to be_valid } + + it_behaves_like 'deterministic' + end + + context 'when generating color for number value' do + let(:value) { 1 } + + specify { is_expected.to be_valid } + + it_behaves_like 'deterministic' + end + + context 'when generating color for string value' do + let(:value) { "1" } + + specify { is_expected.to be_valid } + + it_behaves_like 'deterministic' + end + end + describe '#new' do it 'handles nil values' do expect(described_class.new(nil)).to eq(described_class.new(nil)) diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb index 44e2cb21677..2df85434f0e 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -183,6 +183,8 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end describe '#load' do + let(:default_directives) { described_class.default_directives } + subject { described_class.new(csp_config[:directives]) } def expected_config(directive) @@ -207,5 +209,23 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do expect(policy.directives['base-uri']).to be_nil end + + it 'returns default values for directives not defined by the user' do + # Explicitly disabling script_src and setting report_uri + csp_config[:directives] = { + script_src: false, + report_uri: 'https://example.org' + } + + subject.load(policy) + + expected_policy = ActionDispatch::ContentSecurityPolicy.new + # Creating a policy from default settings and manually overriding the custom values + described_class.new(default_directives).load(expected_policy) + expected_policy.script_src(nil) + expected_policy.report_uri('https://example.org') + + expect(policy.directives).to eq(expected_policy.directives) + end end end diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/data_builder/issuable_spec.rb index 676396697fb..c1ae65c160f 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/data_builder/issuable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::HookData::IssuableBuilder do +RSpec.describe Gitlab::DataBuilder::Issuable do let_it_be(:user) { create(:user) } # This shared example requires a `builder` and `user` variable diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb index 7a433be0e2f..a1c979bba50 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -99,6 +99,15 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '.created_after' do + let!(:migration_old) { create :batched_background_migration, created_at: 2.days.ago } + let!(:migration_new) { create :batched_background_migration, created_at: 0.days.ago } + + it 'only returns migrations created after the specified time' do + expect(described_class.created_after(1.day.ago)).to contain_exactly(migration_new) + end + end + describe '.queued' do let!(:migration1) { create(:batched_background_migration, :finished) } let!(:migration2) { create(:batched_background_migration, :paused) } diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index 6a4ac317cad..83c0275a870 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -3,23 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '#perform' do - subject { described_class.new(connection: connection, metrics: metrics_tracker).perform(job_record) } + subject(:perform) { described_class.new(connection: connection, metrics: metrics_tracker).perform(job_record) } let(:connection) { Gitlab::Database.database_base_models[:main].connection } let(:metrics_tracker) { instance_double('::Gitlab::Database::BackgroundMigration::PrometheusMetrics', track: nil) } - let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob } + let(:job_class) { Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) } let_it_be(:pause_ms) { 250 } let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) } let!(:job_record) do - create(:batched_background_migration_job, - batched_migration: active_migration, - pause_ms: pause_ms - ) + create(:batched_background_migration_job, batched_migration: active_migration, pause_ms: pause_ms) end - let(:job_instance) { double('job instance', batch_metrics: {}) } + let(:job_instance) { instance_double('Gitlab::BackgroundMigration::BatchedMigrationJob') } around do |example| Gitlab::Database::SharedModel.using_connection(connection) do @@ -28,23 +25,35 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end before do + allow(active_migration).to receive(:job_class).and_return(job_class) + allow(job_class).to receive(:new).and_return(job_instance) end it 'runs the migration job' do - expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, pause_ms, 'id', 'other_id') - - subject + expect(job_class).to receive(:new) + .with(start_id: 1, + end_id: 10, + batch_table: 'events', + batch_column: 'id', + sub_batch_size: 1, + pause_ms: pause_ms, + connection: connection) + .and_return(job_instance) + + expect(job_instance).to receive(:perform).with('id', 'other_id') + + perform end it 'updates the tracking record in the database' do - test_metrics = { 'my_metris' => 'some value' } + test_metrics = { 'my_metrics' => 'some value' } - expect(job_instance).to receive(:perform) + expect(job_instance).to receive(:perform).with('id', 'other_id') expect(job_instance).to receive(:batch_metrics).and_return(test_metrics) freeze_time do - subject + perform reloaded_job_record = job_record.reload @@ -69,11 +78,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' it 'increments attempts and updates other fields' do updated_metrics = { 'updated_metrics' => 'some_value' } - expect(job_instance).to receive(:perform) + expect(job_instance).to receive(:perform).with('id', 'other_id') expect(job_instance).to receive(:batch_metrics).and_return(updated_metrics) freeze_time do - subject + perform job_record.reload @@ -88,10 +97,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' context 'when the migration job does not raise an error' do it 'marks the tracking record as succeeded' do - expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, pause_ms, 'id', 'other_id') + expect(job_instance).to receive(:perform).with('id', 'other_id') freeze_time do - subject + perform reloaded_job_record = job_record.reload @@ -101,22 +110,20 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end it 'tracks metrics of the execution' do - expect(job_instance).to receive(:perform) + expect(job_instance).to receive(:perform).with('id', 'other_id') expect(metrics_tracker).to receive(:track).with(job_record) - subject + perform end end context 'when the migration job raises an error' do shared_examples 'an error is raised' do |error_class| it 'marks the tracking record as failed' do - expect(job_instance).to receive(:perform) - .with(1, 10, 'events', 'id', 1, pause_ms, 'id', 'other_id') - .and_raise(error_class) + expect(job_instance).to receive(:perform).with('id', 'other_id').and_raise(error_class) freeze_time do - expect { subject }.to raise_error(error_class) + expect { perform }.to raise_error(error_class) reloaded_job_record = job_record.reload @@ -126,10 +133,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end it 'tracks metrics of the execution' do - expect(job_instance).to receive(:perform).and_raise(error_class) + expect(job_instance).to receive(:perform).with('id', 'other_id').and_raise(error_class) expect(metrics_tracker).to receive(:track).with(job_record) - expect { subject }.to raise_error(error_class) + expect { perform }.to raise_error(error_class) end end @@ -138,41 +145,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' it_behaves_like 'an error is raised', ActiveRecord::StatementTimeout.new('Timeout!') end - context 'when the batched background migration does not inherit from BaseJob' do - let(:migration_class) { Class.new } - - before do - stub_const('Gitlab::BackgroundMigration::Foo', migration_class) - end + context 'when the batched background migration does not inherit from BatchedMigrationJob' do + let(:job_class) { Class.new } - let(:active_migration) { create(:batched_background_migration, :active, job_class_name: 'Foo') } - let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) } - - it 'does not pass any argument' do - expect(Gitlab::BackgroundMigration::Foo).to receive(:new).with(no_args).and_return(job_instance) - - expect(job_instance).to receive(:perform) - - subject - end - end - - context 'when the batched background migration inherits from BaseJob' do - let(:active_migration) { create(:batched_background_migration, :active, job_class_name: 'Foo') } - let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) } - - let(:migration_class) { Class.new(::Gitlab::BackgroundMigration::BaseJob) } - - before do - stub_const('Gitlab::BackgroundMigration::Foo', migration_class) - end - - it 'passes the correct connection' do - expect(Gitlab::BackgroundMigration::Foo).to receive(:new).with(connection: connection).and_return(job_instance) - - expect(job_instance).to receive(:perform) + it 'runs the job with the correct arguments' do + expect(job_class).to receive(:new).with(no_args).and_return(job_instance) + expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, pause_ms, 'id', 'other_id') - subject + perform end end end diff --git a/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb index e7b5bad8626..1009ec354c3 100644 --- a/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb @@ -401,8 +401,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a ci: :dml_not_allowed }, gitlab_schema_gitlab_shared: { - main: :dml_access_denied, - ci: :dml_access_denied + main: :runtime_error, + ci: :runtime_error }, gitlab_schema_gitlab_main: { main: :success, @@ -465,7 +465,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a "does raise exception when accessing feature flags" => { migration: ->(klass) do def up - Feature.enabled?(:redis_hll_tracking, type: :ops, default_enabled: :yaml) + Feature.enabled?(:redis_hll_tracking, type: :ops) end def down @@ -486,6 +486,37 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a ci: :skipped } } + }, + "does raise exception about cross schema access when suppressing restriction to ensure" => { + migration: ->(klass) do + # The purpose of this test is to ensure that we use ApplicationRecord + # a correct connection will be used: + # - this is a case for finalizing background migrations + def up + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + ::ApplicationRecord.connection.execute("SELECT 1 FROM ci_builds") + end + end + + def down + end + end, + query_matcher: /FROM ci_builds/, + setup: -> (_) { skip_if_multiple_databases_not_setup }, + expected: { + no_gitlab_schema: { + main: :cross_schema_error, + ci: :success + }, + gitlab_schema_gitlab_shared: { + main: :cross_schema_error, + ci: :success + }, + gitlab_schema_gitlab_main: { + main: :cross_schema_error, + ci: :skipped + } + } } } end @@ -517,6 +548,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a %i[no_gitlab_schema gitlab_schema_gitlab_main gitlab_schema_gitlab_shared].each do |restrict_gitlab_migration| context "while restrict_gitlab_migration=#{restrict_gitlab_migration}" do it "does run migrate :up and :down" do + instance_eval(&setup) if setup + expected_result = expected.fetch(restrict_gitlab_migration)[db_config_name.to_sym] skip "not configured" unless expected_result @@ -543,10 +576,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLAccessDeniedError) expect { ignore_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLAccessDeniedError) { migration_class.migrate(:down) } }.not_to raise_error + when :runtime_error + expect { migration_class.migrate(:up) }.to raise_error(RuntimeError) + expect { ignore_error(RuntimeError) { migration_class.migrate(:down) } }.not_to raise_error + when :ddl_not_allowed expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DDLNotAllowedError) expect { ignore_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DDLNotAllowedError) { migration_class.migrate(:down) } }.not_to raise_error + when :cross_schema_error + expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError) + expect { ignore_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError) { migration_class.migrate(:down) } }.not_to raise_error + when :skipped expect_next_instance_of(migration_class) do |migration_object| expect(migration_object).to receive(:migration_skipped).and_call_original diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 798eee0de3e..04fe1fad10e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1537,10 +1537,12 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:add_concurrent_index) .with(:issues, %w(gl_project_id), + { unique: false, name: 'index_on_issues_gl_project_id', length: [], - order: []) + order: [] + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1564,10 +1566,12 @@ RSpec.describe Gitlab::Database::MigrationHelpers do 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: []) + order: [] + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1591,11 +1595,13 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:add_concurrent_index) .with(:issues, %w(gl_project_id), + { unique: false, name: 'index_on_issues_gl_project_id', length: [], order: [], - where: 'foo') + where: 'foo' + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1619,11 +1625,13 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:add_concurrent_index) .with(:issues, %w(gl_project_id), + { unique: false, name: 'index_on_issues_gl_project_id', length: [], order: [], - using: 'foo') + using: 'foo' + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1647,11 +1655,13 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:add_concurrent_index) .with(:issues, %w(gl_project_id), + { unique: false, name: 'index_on_issues_gl_project_id', length: [], order: [], - opclass: { 'gl_project_id' => 'bar' }) + opclass: { 'gl_project_id' => 'bar' } + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1660,14 +1670,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do 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: []) + { + 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]) @@ -1675,12 +1687,14 @@ RSpec.describe Gitlab::Database::MigrationHelpers do 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) + using: :gin + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -1689,14 +1703,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do 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: []) + { + 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]) @@ -1704,12 +1720,14 @@ RSpec.describe Gitlab::Database::MigrationHelpers do 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) + using: :gin + }) model.copy_indexes(:issues, :project_id, :gl_project_id) end @@ -2210,12 +2228,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end describe '#ensure_batched_background_migration_is_finished' do + let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' } + let(:table) { :events } + let(:column_name) { :id } + let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] } + let(:configuration) do { - job_class_name: 'CopyColumnUsingBackgroundMigrationJob', - table_name: :events, - column_name: :id, - job_arguments: [["id"], ["id_convert_to_bigint"], nil] + job_class_name: job_class_name, + table_name: table, + column_name: column_name, + job_arguments: job_arguments } end @@ -2224,11 +2247,15 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'raises an error when migration exists and is not marked as finished' do create(:batched_background_migration, :active, configuration) + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + allow(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(false) + end + expect { ensure_batched_background_migration_is_finished } .to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':" \ "\t#{configuration}" \ "\n\n" \ - "Finalize it manualy by running" \ + "Finalize it manually by running" \ "\n\n" \ "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \ "\n\n" \ @@ -2251,6 +2278,28 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect { ensure_batched_background_migration_is_finished } .not_to raise_error end + + it 'finalizes the migration' do + migration = create(:batched_background_migration, :active, configuration) + + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(migration.finish!) + end + + ensure_batched_background_migration_is_finished + end + + context 'when the flag finalize is false' do + it 'does not finalize the migration' do + create(:batched_background_migration, :active, configuration) + + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).not_to receive(:finalize).with(job_class_name, table, column_name, job_arguments) + end + + expect { model.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError) + end + end end describe '#index_exists_by_name?' do @@ -3162,15 +3211,15 @@ RSpec.describe Gitlab::Database::MigrationHelpers do context 'without proper permissions' do before do - allow(model).to receive(:execute).with(/CREATE EXTENSION IF NOT EXISTS #{extension}/).and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied') + allow(model).to receive(:execute) + .with(/CREATE EXTENSION IF NOT EXISTS #{extension}/) + .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied') end - it 'raises the exception' do - expect { subject }.to raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/) - end - - it 'prints an error message' do - expect { subject }.to output(/user is not allowed/).to_stderr.and raise_error + it 'raises an exception and prints an error message' do + expect { subject } + .to output(/user is not allowed/).to_stderr + .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/) end end end @@ -3188,15 +3237,15 @@ RSpec.describe Gitlab::Database::MigrationHelpers do context 'without proper permissions' do before do - allow(model).to receive(:execute).with(/DROP EXTENSION IF EXISTS #{extension}/).and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied') - end - - it 'raises the exception' do - expect { subject }.to raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/) + allow(model).to receive(:execute) + .with(/DROP EXTENSION IF EXISTS #{extension}/) + .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied') end - it 'prints an error message' do - expect { subject }.to output(/user is not allowed/).to_stderr.and raise_error + it 'raises an exception and prints an error message' do + expect { subject } + .to output(/user is not allowed/).to_stderr + .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/) end end end diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb index e64f5807385..b0caa21e01a 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -3,16 +3,31 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do + let(:base_class) { ActiveRecord::Migration } + let(:model) do - ActiveRecord::Migration.new.extend(described_class) + base_class.new + .extend(described_class) + .extend(Gitlab::Database::Migrations::ReestablishedConnectionStack) end - shared_examples_for 'helpers that enqueue background migrations' do |worker_class, tracking_database| + shared_examples_for 'helpers that enqueue background migrations' do |worker_class, connection_class, tracking_database| before do allow(model).to receive(:tracking_database).and_return(tracking_database) + + # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore + # we cannot use stub_feature_flags(force_no_sharing_primary_model: true) + allow(connection_class.connection.load_balancer.configuration) + .to receive(:use_dedicated_connection?).and_return(true) + + allow(model).to receive(:connection).and_return(connection_class.connection) end describe '#queue_background_migration_jobs_by_range_at_intervals' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + context 'when the model has an ID column' do let!(:id1) { create(:user).id } let!(:id2) { create(:user).id } @@ -196,6 +211,34 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end.to raise_error(StandardError, /does not have an ID/) end end + + context 'when using Migration[2.0]' do + let(:base_class) { Class.new(Gitlab::Database::Migration[2.0]) } + + context 'when restriction is set to gitlab_shared' do + before do + base_class.restrict_gitlab_migration gitlab_schema: :gitlab_shared + end + + it 'does raise an exception' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) + end.to raise_error /use `restrict_gitlab_migration:` " with `:gitlab_shared`/ + end + end + end + + context 'when within transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(true) + end + + it 'does raise an exception' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) + end.to raise_error /The `#queue_background_migration_jobs_by_range_at_intervals` can not be run inside a transaction./ + end + end end describe '#requeue_background_migration_jobs_by_range_at_intervals' do @@ -205,6 +248,10 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do let!(:successful_job_1) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [5, 6]) } let!(:successful_job_2) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [7, 8]) } + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + around do |example| freeze_time do Sidekiq::Testing.fake! do @@ -219,6 +266,38 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do expect(subject).to eq(20.minutes) end + context 'when using Migration[2.0]' do + let(:base_class) { Class.new(Gitlab::Database::Migration[2.0]) } + + it 'does re-enqueue pending jobs' do + subject + + expect(worker_class.jobs).not_to be_empty + end + + context 'when restriction is set' do + before do + base_class.restrict_gitlab_migration gitlab_schema: :gitlab_main + end + + it 'does raise an exception' do + expect { subject } + .to raise_error /The `#requeue_background_migration_jobs_by_range_at_intervals` cannot use `restrict_gitlab_migration:`./ + end + end + end + + context 'when within transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(true) + end + + it 'does raise an exception' do + expect { subject } + .to raise_error /The `#requeue_background_migration_jobs_by_range_at_intervals` can not be run inside a transaction./ + end + end + context 'when nothing is queued' do subject { model.requeue_background_migration_jobs_by_range_at_intervals('FakeJob', 10.minutes) } @@ -290,7 +369,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end end - describe '#finalized_background_migration' do + describe '#finalize_background_migration' do let(:coordinator) { Gitlab::BackgroundMigration::JobCoordinator.new(worker_class) } let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) } @@ -309,8 +388,8 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database) .with(tracking_database).and_return(coordinator) - expect(coordinator).to receive(:migration_class_for) - .with(job_class_name).at_least(:once) { job_class } + allow(coordinator).to receive(:migration_class_for) + .with(job_class_name) { job_class } Sidekiq::Testing.disable! do worker_class.perform_async(job_class_name, [1, 2]) @@ -318,6 +397,8 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do worker_class.perform_in(10, job_class_name, [5, 6]) worker_class.perform_in(20, job_class_name, [7, 8]) end + + allow(model).to receive(:transaction_open?).and_return(false) end it_behaves_like 'finalized tracked background migration', worker_class do @@ -326,6 +407,52 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end end + context 'when within transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(true) + end + + it 'does raise an exception' do + expect { model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded]) } + .to raise_error /The `#finalize_background_migration` can not be run inside a transaction./ + end + end + + context 'when using Migration[2.0]' do + let(:base_class) { Class.new(Gitlab::Database::Migration[2.0]) } + + it_behaves_like 'finalized tracked background migration', worker_class do + before do + model.finalize_background_migration(job_class_name) + end + end + + context 'when restriction is set' do + before do + base_class.restrict_gitlab_migration gitlab_schema: :gitlab_main + end + + it 'does raise an exception' do + expect { model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded]) } + .to raise_error /The `#finalize_background_migration` cannot use `restrict_gitlab_migration:`./ + end + end + end + + context 'when running migration in reconfigured ActiveRecord::Base context' do + it_behaves_like 'reconfigures connection stack', tracking_database do + it 'does restore connection hierarchy' do + expect_next_instances_of(job_class, 1..) do |job| + expect(job).to receive(:perform) do + validate_connections! + end + end + + model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded]) + end + end + end + context 'when removing all tracked job records' do let!(:job_class) do Class.new do @@ -443,7 +570,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end context 'when the migration is running against the main database' do - it_behaves_like 'helpers that enqueue background migrations', BackgroundMigrationWorker, 'main' + it_behaves_like 'helpers that enqueue background migrations', BackgroundMigrationWorker, ActiveRecord::Base, 'main' end context 'when the migration is running against the ci database', if: Gitlab::Database.has_config?(:ci) do @@ -453,7 +580,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end end - it_behaves_like 'helpers that enqueue background migrations', BackgroundMigration::CiDatabaseWorker, 'ci' + it_behaves_like 'helpers that enqueue background migrations', BackgroundMigration::CiDatabaseWorker, Ci::ApplicationRecord, 'ci' end describe '#delete_job_tracking' do diff --git a/spec/lib/gitlab/database/migrations/base_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/base_background_runner_spec.rb new file mode 100644 index 00000000000..34c83c42056 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/base_background_runner_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::BaseBackgroundRunner, :freeze_time do + let(:result_dir) { Dir.mktmpdir } + + after do + FileUtils.rm_rf(result_dir) + end + + context 'subclassing' do + subject { described_class.new(result_dir: result_dir) } + + it 'requires that jobs_by_migration_name be implemented' do + expect { subject.jobs_by_migration_name }.to raise_error(NotImplementedError) + end + + it 'requires that run_job be implemented' do + expect { subject.run_job(nil) }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index f9347a174c4..d1a66036149 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -163,4 +163,45 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end end end + + describe '#finalize_batched_background_migration' do + let!(:batched_migration) { create(:batched_background_migration, job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: []) } + + it 'finalizes the migration' do + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with('MyClass', :projects, :id, []) + end + + migration.finalize_batched_background_migration(job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: []) + end + + context 'when the migration does not exist' do + it 'raises an exception' do + expect do + migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: []) + end.to raise_error(RuntimeError, 'Could not find batched background migration') + end + end + + context 'when uses a CI connection', :reestablished_active_record_base do + before do + skip_if_multiple_databases_not_setup + + ActiveRecord::Base.establish_connection(:ci) # rubocop:disable Database/EstablishConnection + end + + it 'raises an exception' do + ci_migration = create(:batched_background_migration, :active) + + expect do + migration.finalize_batched_background_migration( + job_class_name: ci_migration.job_class_name, + table_name: ci_migration.table_name, + column_name: ci_migration.column_name, + job_arguments: ci_migration.job_arguments + ) + end.to raise_error /is currently not supported when running in decomposed/ + end + end + end end diff --git a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb index 2515f0d4a06..66de25d65bb 100644 --- a/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb +++ b/spec/lib/gitlab/database/migrations/observers/query_statistics_spec.rb @@ -43,6 +43,7 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do <<~SQL SELECT query, calls, total_time, max_time, mean_time, rows FROM pg_stat_statements + WHERE pg_get_userbyid(userid) = current_user ORDER BY total_time DESC SQL end diff --git a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb new file mode 100644 index 00000000000..cfb308c63e4 --- /dev/null +++ b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do + let(:base_class) { ActiveRecord::Migration } + + let(:model) do + base_class.new + .extend(described_class) + end + + describe '#with_restored_connection_stack' do + Gitlab::Database.database_base_models.each do |db_config_name, _| + context db_config_name do + it_behaves_like "reconfigures connection stack", db_config_name do + it 'does restore connection hierarchy' do + model.with_restored_connection_stack do + validate_connections! + end + end + + primary_db_config = ActiveRecord::Base.configurations.primary?(db_config_name) + + it 'does reconfigure connection handler', unless: primary_db_config do + original_handler = ActiveRecord::Base.connection_handler + new_handler = nil + + model.with_restored_connection_stack do + new_handler = ActiveRecord::Base.connection_handler + + # establish connection + ApplicationRecord.connection.select_one("SELECT 1 FROM projects LIMIT 1") + Ci::ApplicationRecord.connection.select_one("SELECT 1 FROM ci_builds LIMIT 1") + end + + expect(new_handler).not_to eq(original_handler), "is reconnected" + expect(new_handler).not_to be_active_connections + expect(ActiveRecord::Base.connection_handler).to eq(original_handler), "is restored" + end + + it 'does keep original connection handler', if: primary_db_config do + original_handler = ActiveRecord::Base.connection_handler + new_handler = nil + + model.with_restored_connection_stack do + new_handler = ActiveRecord::Base.connection_handler + end + + expect(new_handler).to eq(original_handler) + expect(ActiveRecord::Base.connection_handler).to eq(original_handler) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb index 8b1ccf05eb1..e7f68e3e4a8 100644 --- a/spec/lib/gitlab/database/migrations/runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/runner_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Migrations::Runner do + include Database::MultipleDatabases + let(:result_dir) { Pathname.new(Dir.mktmpdir) } let(:migration_runs) { [] } # This list gets populated as the runner tries to run migrations @@ -136,4 +138,35 @@ RSpec.describe Gitlab::Database::Migrations::Runner do expect(runner.result_dir).to eq(described_class::BASE_RESULT_DIR.join( 'background_migrations')) end end + + describe '.batched_background_migrations' do + it 'is a TestBatchedBackgroundRunner' do + expect(described_class.batched_background_migrations(for_database: 'main')).to be_a(Gitlab::Database::Migrations::TestBatchedBackgroundRunner) + end + + context 'choosing the database to test against' do + it 'chooses the main database' do + runner = described_class.batched_background_migrations(for_database: 'main') + + chosen_connection_name = Gitlab::Database.db_config_name(runner.connection) + + expect(chosen_connection_name).to eq('main') + end + + it 'chooses the ci database' do + skip_if_multiple_databases_not_setup + + runner = described_class.batched_background_migrations(for_database: 'ci') + + chosen_connection_name = Gitlab::Database.db_config_name(runner.connection) + + expect(chosen_connection_name).to eq('ci') + end + + it 'throws an error with an invalid name' do + expect { described_class.batched_background_migrations(for_database: 'not_a_database') } + .to raise_error(/not a valid database name/) + end + end + end end diff --git a/spec/lib/gitlab/database/migrations/test_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_background_runner_spec.rb index 9407efad91f..a2fe91712c7 100644 --- a/spec/lib/gitlab/database/migrations/test_background_runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/test_background_runner_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Migrations::TestBackgroundRunner, :redis do + include Gitlab::Database::Migrations::ReestablishedConnectionStack include Gitlab::Database::Migrations::BackgroundMigrationHelpers + include Database::MigrationTestingHelpers # In order to test the interaction between queueing sidekiq jobs and seeing those jobs in queues, # we need to disable sidekiq's testing mode and actually send our jobs to redis @@ -12,6 +14,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBackgroundRunner, :redis do end let(:result_dir) { Dir.mktmpdir } + let(:connection) { ApplicationRecord.connection } after do FileUtils.rm_rf(result_dir) @@ -41,40 +44,6 @@ RSpec.describe Gitlab::Database::Migrations::TestBackgroundRunner, :redis do end context 'running migrations', :freeze_time do - def define_background_migration(name) - klass = Class.new do - # Can't simply def perform here as we won't have access to the block, - # similarly can't define_method(:perform, &block) here as it would change the block receiver - define_method(:perform) { |*args| yield(*args) } - end - stub_const("Gitlab::BackgroundMigration::#{name}", klass) - klass - end - - def expect_migration_call_counts(migrations_to_calls) - migrations_to_calls.each do |migration, calls| - expect_next_instances_of(migration, calls) do |m| - expect(m).to receive(:perform).and_call_original - end - end - end - - def expect_recorded_migration_runs(migrations_to_runs) - migrations_to_runs.each do |migration, runs| - path = File.join(result_dir, migration.name.demodulize) - num_subdirs = Pathname(path).children.count(&:directory?) - expect(num_subdirs).to eq(runs) - end - end - - def expect_migration_runs(migrations_to_run_counts) - expect_migration_call_counts(migrations_to_run_counts) - - yield - - expect_recorded_migration_runs(migrations_to_run_counts) - end - it 'runs the migration class correctly' do calls = [] define_background_migration(migration_name) do |i| diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb new file mode 100644 index 00000000000..fbfff1268cc --- /dev/null +++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freeze_time do + include Gitlab::Database::MigrationHelpers + include Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers + include Database::MigrationTestingHelpers + + let(:result_dir) { Dir.mktmpdir } + + after do + FileUtils.rm_rf(result_dir) + end + + let(:connection) { ApplicationRecord.connection } + + let(:table_name) { "_test_column_copying"} + + before do + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id bigint primary key not null, + data bigint + ); + + insert into #{table_name} (id) select i from generate_series(1, 1000) g(i); + SQL + end + + context 'running a real background migration' do + it 'runs sampled jobs from the batched background migration' do + queue_batched_background_migration('CopyColumnUsingBackgroundMigrationJob', + table_name, :id, + :id, :data, + batch_size: 100, + job_interval: 5.minutes) # job_interval is skipped when testing + described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 1.minute) + unmigrated_row_count = define_batchable_model(table_name).where('id != data').count + + expect(unmigrated_row_count).to eq(0) + end + end + + context 'with jobs to run' do + let(:migration_name) { 'TestBackgroundMigration' } + + before do + queue_batched_background_migration(migration_name, table_name, :id, job_interval: 5.minutes, batch_size: 100) + end + + it 'samples jobs' do + calls = [] + define_background_migration(migration_name) do |*args| + calls << args + end + + described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 3.minutes) + + expect(calls.count).to eq(10) # 1000 rows / batch size 100 = 10 + end + + context 'with multiple jobs to run' do + it 'runs all jobs created within the last 48 hours' do + old_migration = define_background_migration(migration_name) + + travel 3.days + + new_migration = define_background_migration('NewMigration') { travel 1.second } + queue_batched_background_migration('NewMigration', table_name, :id, + job_interval: 5.minutes, + batch_size: 10, + sub_batch_size: 5) + + other_new_migration = define_background_migration('NewMigration2') { travel 2.seconds } + queue_batched_background_migration('NewMigration2', table_name, :id, + job_interval: 5.minutes, + batch_size: 10, + sub_batch_size: 5) + + expect_migration_runs(new_migration => 3, other_new_migration => 2, old_migration => 0) do + described_class.new(result_dir: result_dir, connection: connection).run_jobs(for_duration: 5.seconds) + end + end + end + end +end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb index 8ab3816529b..edb8ae36c45 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -54,7 +54,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do expect_add_concurrent_index_and_call_original(partition2_identifier, column_name, partition2_index) expect(migration).to receive(:with_lock_retries).ordered.and_yield - expect(migration).to receive(:add_index).with(table_name, column_name, name: index_name).ordered.and_call_original + expect(migration).to receive(:add_index).with(table_name, column_name, { name: index_name }).ordered.and_call_original migration.add_concurrent_partitioned_index(table_name, column_name, name: index_name) @@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do end def expect_add_concurrent_index_and_call_original(table, column, index) - expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, name: index) + expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, { name: index }) .and_wrap_original { |_, table, column, options| connection.add_index(table, column, **options) } end end @@ -90,13 +90,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do it 'forwards them to the index helper methods', :aggregate_failures do expect(migration).to receive(:add_concurrent_index) - .with(partition1_identifier, column_name, name: partition1_index, where: 'x > 0', unique: true) + .with(partition1_identifier, column_name, { name: partition1_index, where: 'x > 0', unique: true }) expect(migration).to receive(:add_index) - .with(table_name, column_name, name: index_name, where: 'x > 0', unique: true) + .with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true }) migration.add_concurrent_partitioned_index(table_name, column_name, - name: index_name, where: 'x > 0', unique: true) + { name: index_name, where: 'x > 0', unique: true }) end end diff --git a/spec/lib/gitlab/database/query_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzer_spec.rb index 3b4cbc79de2..0b849063562 100644 --- a/spec/lib/gitlab/database/query_analyzer_spec.rb +++ b/spec/lib/gitlab/database/query_analyzer_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do let(:analyzer) { double(:query_analyzer) } - let(:user_analyzer) { double(:query_analyzer) } + let(:user_analyzer) { double(:user_query_analyzer) } let(:disabled_analyzer) { double(:disabled_query_analyzer) } before do @@ -49,14 +49,36 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do end end - it 'does not evaluate enabled? again do yield block' do - expect(analyzer).not_to receive(:enabled?) + it 'does initialize analyzer only once' do + expect(analyzer).to receive(:enabled?).once + expect(analyzer).to receive(:begin!).once + expect(analyzer).to receive(:end!).once expect { |b| described_class.instance.within(&b) }.to yield_control end - it 'raises exception when trying to re-define analyzers' do - expect { |b| described_class.instance.within([user_analyzer], &b) }.to raise_error /Query analyzers are already defined, cannot re-define them/ + it 'does initialize user analyzer when enabled' do + expect(user_analyzer).to receive(:enabled?).and_return(true) + expect(user_analyzer).to receive(:begin!) + expect(user_analyzer).to receive(:end!) + + expect { |b| described_class.instance.within([user_analyzer], &b) }.to yield_control + end + + it 'does initialize user analyzer only once' do + expect(user_analyzer).to receive(:enabled?).and_return(false, true) + expect(user_analyzer).to receive(:begin!).once + expect(user_analyzer).to receive(:end!).once + + expect { |b| described_class.instance.within([user_analyzer, user_analyzer, user_analyzer], &b) }.to yield_control + end + + it 'does not initializer user analyzer when disabled' do + expect(user_analyzer).to receive(:enabled?).and_return(false) + expect(user_analyzer).not_to receive(:begin!) + expect(user_analyzer).not_to receive(:end!) + + expect { |b| described_class.instance.within([user_analyzer], &b) }.to yield_control end end @@ -162,7 +184,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do def process_sql(sql) described_class.instance.within do ApplicationRecord.load_balancer.read_write do |connection| - described_class.instance.process_sql(sql, connection) + described_class.instance.send(:process_sql, sql, connection) end end end diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb index b8c1ecd9089..0d687db0f96 100644 --- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb +++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb @@ -140,7 +140,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana def process_sql(model, sql) Gitlab::Database::QueryAnalyzer.instance.within do # Skip load balancer and retrieve connection assigned to model - Gitlab::Database::QueryAnalyzer.instance.process_sql(sql, model.retrieve_connection) + Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection) end end end diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb new file mode 100644 index 00000000000..5e8afc0102e --- /dev/null +++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection, query_analyzers: false do + let(:analyzer) { described_class } + + context 'properly observes all queries', :request_store do + using RSpec::Parameterized::TableSyntax + + where do + { + "for simple query observes schema correctly" => { + model: ApplicationRecord, + sql: "SELECT 1 FROM projects", + expect_error: nil, + setup: nil + }, + "for query accessing gitlab_ci and gitlab_main" => { + model: ApplicationRecord, + sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id", + expect_error: /The query tried to access \["projects", "ci_builds"\]/, + setup: -> (_) { skip_if_multiple_databases_not_setup } + }, + "for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => { + model: ApplicationRecord, + sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id", + expect_error: /The query tried to access \["ci_builds", "projects"\]/, + setup: -> (_) { skip_if_multiple_databases_not_setup } + }, + "for query accessing main table from CI database" => { + model: Ci::ApplicationRecord, + sql: "SELECT 1 FROM projects", + expect_error: /The query tried to access \["projects"\]/, + setup: -> (_) { skip_if_multiple_databases_not_setup } + }, + "for query accessing CI database" => { + model: Ci::ApplicationRecord, + sql: "SELECT 1 FROM ci_builds", + expect_error: nil + }, + "for query accessing CI table from main database" => { + model: ::ApplicationRecord, + sql: "SELECT 1 FROM ci_builds", + expect_error: /The query tried to access \["ci_builds"\]/, + setup: -> (_) { skip_if_multiple_databases_not_setup } + } + } + end + + with_them do + it do + instance_eval(&setup) if setup + + if expect_error + expect { process_sql(model, sql) }.to raise_error(expect_error) + else + expect { process_sql(model, sql) }.not_to raise_error + end + end + end + end + + def process_sql(model, sql) + Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do + # Skip load balancer and retrieve connection assigned to model + Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection) + end + end +end diff --git a/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb b/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb index a2c7916fa01..261bef58bb6 100644 --- a/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb +++ b/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb @@ -155,7 +155,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas, query_a yield if block_given? # Skip load balancer and retrieve connection assigned to model - Gitlab::Database::QueryAnalyzer.instance.process_sql(sql, model.retrieve_connection) + Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection) end end end diff --git a/spec/lib/gitlab/database/shared_model_spec.rb b/spec/lib/gitlab/database/shared_model_spec.rb index 54af4a0c4dc..574111f4c01 100644 --- a/spec/lib/gitlab/database/shared_model_spec.rb +++ b/spec/lib/gitlab/database/shared_model_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Gitlab::Database::SharedModel do expect do described_class.using_connection(second_connection) {} - end.to raise_error(/cannot nest connection overrides/) + end.to raise_error(/Cannot change connection for Gitlab::Database::SharedModel/) expect(described_class.connection).to be(new_connection) end diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb index 8c3d372cc55..d044170dc75 100644 --- a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb +++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do - subject { described_class.import } + subject { described_class.upsert_types } it_behaves_like 'work item base types importer' end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index ac8616f84a7..23f4f0e7089 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -70,40 +70,6 @@ RSpec.describe Gitlab::Database do end end - describe '.main_database?' do - using RSpec::Parameterized::TableSyntax - - where(:database_name, :result) do - :main | true - 'main' | true - :ci | false - 'ci' | false - :archive | false - 'archive' | false - end - - with_them do - it { expect(described_class.main_database?(database_name)).to eq(result) } - end - end - - describe '.ci_database?' do - using RSpec::Parameterized::TableSyntax - - where(:database_name, :result) do - :main | false - 'main' | false - :ci | true - 'ci' | true - :archive | false - 'archive' | false - end - - with_them do - it { expect(described_class.ci_database?(database_name)).to eq(result) } - end - end - describe '.check_for_non_superuser' do subject { described_class.check_for_non_superuser } diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 0d7a183bb11..b7262629e0a 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -99,6 +99,22 @@ RSpec.describe Gitlab::Diff::File do end end + describe '#ipynb?' do + context 'is ipynb' do + let(:commit) { project.commit("532c837") } + + it 'is true' do + expect(diff_file.ipynb?).to be_truthy + end + end + + context 'is not ipynb' do + it 'is false' do + expect(diff_file.ipynb?).to be_falsey + end + end + end + describe '#has_renderable?' do context 'file is ipynb' do let(:commit) { project.commit("532c837") } diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb index 89b284feee0..1b74e24bf81 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb @@ -72,7 +72,7 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do end it 'falls back to nil on timeout' do - allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + expect(Gitlab::ErrorTracking).to receive(:log_exception) expect(Timeout).to receive(:timeout).and_raise(Timeout::Error) expect(nb_file.diff).to be_nil @@ -101,6 +101,22 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do expect(nb_file.has_renderable?).to be_truthy end end + + context 'when old blob file is truncated' do + it 'is false' do + allow(source.old_blob).to receive(:truncated?).and_return(true) + + expect(nb_file.has_renderable?).to be_falsey + end + end + + context 'when new blob file is truncated' do + it 'is false' do + allow(source.new_blob).to receive(:truncated?).and_return(true) + + expect(nb_file.has_renderable?).to be_falsey + end + end end describe '#highlighted_diff_lines?' do @@ -125,5 +141,9 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do expect(nb_file.highlighted_diff_lines[12].old_pos).to eq(18) end end + + it 'computes de first line where the remove would appear' do + expect(nb_file.highlighted_diff_lines.map(&:text).join('')).to include('[Hidden Image Output]') + end end end diff --git a/spec/lib/gitlab/doctor/secrets_spec.rb b/spec/lib/gitlab/doctor/secrets_spec.rb index f95a7eb1492..efdd6cc1199 100644 --- a/spec/lib/gitlab/doctor/secrets_spec.rb +++ b/spec/lib/gitlab/doctor/secrets_spec.rb @@ -7,10 +7,25 @@ RSpec.describe Gitlab::Doctor::Secrets do let!(:group) { create(:group, runners_token: "test") } let!(:project) { create(:project) } let!(:grafana_integration) { create(:grafana_integration, project: project, token: "test") } + let!(:integration) { create(:integration, project: project, properties: { test_key: "test_value" }) } let(:logger) { double(:logger).as_null_object } subject { described_class.new(logger).run! } + before do + allow(Gitlab::Runtime).to receive(:rake?).and_return(true) + end + + context 'when not ran in a Rake runtime' do + before do + allow(Gitlab::Runtime).to receive(:rake?).and_return(false) + end + + it 'raises an error' do + expect { subject }.to raise_error(StandardError, 'can only be used in a Rake environment') + end + end + context 'when encrypted attributes are properly set' do it 'detects decryptable secrets' do expect(logger).to receive(:info).with(/User failures: 0/) @@ -42,6 +57,25 @@ RSpec.describe Gitlab::Doctor::Secrets do end end + context 'when initializers attempt to use encrypted data' do + it 'skips the initializers and detects bad data' do + integration.encrypted_properties = "invalid" + integration.save! + + expect(logger).to receive(:info).with(/Integration failures: 1/) + + subject + end + + it 'resets the initializers after the task runs' do + subject + + expect(integration).to receive(:initialize_properties) + + integration.run_callbacks(:initialize) + end + end + context 'when GrafanaIntegration token is set via private method' do it 'can access GrafanaIntegration token value' do expect(logger).to receive(:info).with(/GrafanaIntegration failures: 0/) 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 59b87c5d8e7..9ff395070ea 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end it 'does not raise a UserNotFoundError' do - expect { receiver.execute }.not_to raise_error(Gitlab::Email::UserNotFoundError) + expect { receiver.execute }.not_to raise_error end end end @@ -71,7 +71,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do let(:original_recipient) { User.support_bot } it 'does not raise a UserNotFoundError' do - expect { receiver.execute }.not_to raise_error(Gitlab::Email::UserNotFoundError) + expect { receiver.execute }.not_to raise_error end end end diff --git a/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb b/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb new file mode 100644 index 00000000000..3089f955252 --- /dev/null +++ b/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Email::Message::BuildIosAppGuide do + subject(:message) { described_class.new } + + before do + allow(Gitlab).to receive(:com?) { true } + end + + it 'contains the correct message', :aggregate_failures do + expect(message.subject_line).to eq 'Get set up to build for iOS' + expect(message.title).to eq "Building for iOS? We've got you covered." + expect(message.body_line1).to eq "Want to get your iOS app up and running, including " \ + "publishing all the way to TestFlight? Follow our guide to set up GitLab and fastlane to publish iOS apps to " \ + "the App Store." + expect(message.cta_text).to eq 'Learn how to build for iOS' + expect(message.cta2_text).to eq 'Watch iOS building in action.' + expect(message.logo_path).to eq 'mailers/in_product_marketing/create-0.png' + expect(message.unsubscribe).to include('%tag_unsubscribe_url%') + end +end diff --git a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb index 8bd873cf008..dfa18c27d5e 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do let(:series) { 0 } it 'does not raise error' do - expect { subject }.not_to raise_error(ArgumentError) + expect { subject }.not_to raise_error end end end diff --git a/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb new file mode 100644 index 00000000000..3c0d83d0f9e --- /dev/null +++ b/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Email::Message::InProductMarketing::Helper do + describe 'unsubscribe_message' do + include Gitlab::Routing + + let(:dummy_class_with_helper) do + Class.new do + include Gitlab::Email::Message::InProductMarketing::Helper + include Gitlab::Routing + + def initialize(format = :html) + @format = format + end + + def default_url_options + {} + end + + attr_accessor :format + end + end + + let(:format) { :html } + + subject(:class_with_helper) { dummy_class_with_helper.new(format) } + + context 'gitlab.com' do + before do + allow(Gitlab).to receive(:com?) { true } + end + + context 'format is HTML' do + it 'returns the correct HTML' do + message = "If you no longer wish to receive marketing emails from us, " \ + "you may <a href=\"%tag_unsubscribe_url%\">unsubscribe</a> at any time." + expect(class_with_helper.unsubscribe_message).to match message + end + end + + context 'format is text' do + let(:format) { :text } + + it 'returns the correct string' do + message = "If you no longer wish to receive marketing emails from us, " \ + "you may unsubscribe (%tag_unsubscribe_url%) at any time." + expect(class_with_helper.unsubscribe_message.squish).to match message + end + end + end + + context 'self-managed' do + context 'format is HTML' do + it 'returns the correct HTML' do + preferences_link = "http://example.com/preferences" + message = "To opt out of these onboarding emails, " \ + "<a href=\"#{profile_notifications_url}\">unsubscribe</a>. " \ + "If you don't want to receive marketing emails directly from GitLab, #{preferences_link}." + expect(class_with_helper.unsubscribe_message(preferences_link)) + .to match message + end + end + + context 'format is text' do + let(:format) { :text } + + it 'returns the correct string' do + preferences_link = "http://example.com/preferences" + message = "To opt out of these onboarding emails, " \ + "unsubscribe (#{profile_notifications_url}). " \ + "If you don't want to receive marketing emails directly from GitLab, #{preferences_link}." + expect(class_with_helper.unsubscribe_message(preferences_link).squish).to match message + end + end + end + end +end diff --git a/spec/lib/gitlab/experiment/rollout/feature_spec.rb b/spec/lib/gitlab/experiment/rollout/feature_spec.rb index 82603e6fe0f..a66f4fea207 100644 --- a/spec/lib/gitlab/experiment/rollout/feature_spec.rb +++ b/spec/lib/gitlab/experiment/rollout/feature_spec.rb @@ -53,8 +53,7 @@ RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment do expect(Feature).to receive(:enabled?).with( 'namespaced_stub', subject, - type: :experiment, - default_enabled: :yaml + type: :experiment ).and_return(false) expect(subject.execute_assignment).to be_nil diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 435a0d56301..799884d7a74 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -274,7 +274,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do action: 'start', property: 'control_group', value: 1, - label: Digest::MD5.hexdigest('abc'), + label: Digest::SHA256.hexdigest('abc'), user: user ) end @@ -289,7 +289,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do action: 'start', property: 'control_group', value: 1, - label: Digest::MD5.hexdigest('somestring'), + label: Digest::SHA256.hexdigest('somestring'), user: user ) end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 46f544797bb..2c931a999f1 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -165,17 +165,21 @@ EOT context 'when diff contains invalid characters' do let(:bad_string) { [0xae].pack("C*") } let(:bad_string_two) { [0x89].pack("C*") } + let(:bad_string_three) { "@@ -1,5 +1,6 @@\n \xFF\xFE#\x00l\x00a\x00n\x00g\x00u\x00" } let(:diff) { described_class.new(@raw_diff_hash.merge({ diff: bad_string })) } let(:diff_two) { described_class.new(@raw_diff_hash.merge({ diff: bad_string_two })) } + let(:diff_three) { described_class.new(@raw_diff_hash.merge({ diff: bad_string_three })) } context 'when replace_invalid_utf8_chars is true' do it 'will convert invalid characters and not cause an encoding error' do expect(diff.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER) expect(diff_two.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER) + expect(diff_three.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER) - expect { Oj.dump(diff) }.not_to raise_error(EncodingError) - expect { Oj.dump(diff_two) }.not_to raise_error(EncodingError) + expect { Oj.dump(diff) }.not_to raise_error + expect { Oj.dump(diff_two) }.not_to raise_error + expect { Oj.dump(diff_three) }.not_to raise_error end context 'when the diff is binary' do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index d6ef1836ad9..e628a06a542 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -228,6 +228,15 @@ RSpec.describe Gitlab::GitAccess do project.add_maintainer(user) end + context 'key is expired' do + let(:actor) { create(:rsa_key_2048, :expired) } + + it 'does not allow expired keys', :aggregate_failures do + expect { pull_access_check }.to raise_forbidden('Your SSH key has expired.') + expect { push_access_check }.to raise_forbidden('Your SSH key has expired.') + end + end + context 'key is too small' do before do stub_application_setting(rsa_key_restriction: 4096) diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 50a0f20e775..92860c9232f 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -339,11 +339,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do describe '#list_new_commits' do let(:revisions) { [revision] } let(:gitaly_commits) { create_list(:gitaly_commit, 3) } - let(:commits) { gitaly_commits.map { |c| Gitlab::Git::Commit.new(repository, c) }} + let(:expected_commits) { gitaly_commits.map { |c| Gitlab::Git::Commit.new(repository, c) }} + let(:filter_quarantined_commits) { false } - subject { client.list_new_commits(revisions, allow_quarantine: allow_quarantine) } + subject do + client.list_new_commits(revisions, allow_quarantine: allow_quarantine) + end shared_examples 'a #list_all_commits message' do + before do + stub_feature_flags(filter_quarantined_commits: filter_quarantined_commits) + end + it 'sends a list_all_commits message' do expected_repository = repository.gitaly_repository.dup expected_repository.git_alternate_object_directories = Google::Protobuf::RepeatedField.new(:string) @@ -352,9 +359,33 @@ RSpec.describe Gitlab::GitalyClient::CommitService do expect(service).to receive(:list_all_commits) .with(gitaly_request_with_params(repository: expected_repository), kind_of(Hash)) .and_return([Gitaly::ListAllCommitsResponse.new(commits: gitaly_commits)]) + + if filter_quarantined_commits + # The object directory of the repository must not be set so that we + # don't use the quarantine directory. + objects_exist_repo = repository.gitaly_repository.dup + objects_exist_repo.git_object_directory = "" + + # The first request contains the repository, the second request the + # commit IDs we want to check for existence. + objects_exist_request = [ + gitaly_request_with_params(repository: objects_exist_repo), + gitaly_request_with_params(revisions: gitaly_commits.map(&:id)) + ] + + objects_exist_response = Gitaly::CheckObjectsExistResponse.new(revisions: revision_existence.map do + |rev, exists| Gitaly::CheckObjectsExistResponse::RevisionExistence.new(name: rev, exists: exists) + end) + + expect(service).to receive(:check_objects_exist) + .with(objects_exist_request, kind_of(Hash)) + .and_return([objects_exist_response]) + else + expect(service).not_to receive(:check_objects_exist) + end end - expect(subject).to eq(commits) + expect(subject).to eq(expected_commits) end end @@ -366,7 +397,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do .and_return([Gitaly::ListCommitsResponse.new(commits: gitaly_commits)]) end - expect(subject).to eq(commits) + expect(subject).to eq(expected_commits) end end @@ -390,7 +421,40 @@ RSpec.describe Gitlab::GitalyClient::CommitService do context 'with allowed quarantine' do let(:allow_quarantine) { true } - it_behaves_like 'a #list_all_commits message' + context 'without commit filtering' do + it_behaves_like 'a #list_all_commits message' + end + + context 'with commit filtering' do + let(:filter_quarantined_commits) { true } + + context 'reject commits which exist in target repository' do + let(:revision_existence) { gitaly_commits.to_h { |c| [c.id, true] } } + let(:expected_commits) { [] } + + it_behaves_like 'a #list_all_commits message' + end + + context 'keep commits which do not exist in target repository' do + let(:revision_existence) { gitaly_commits.to_h { |c| [c.id, false] } } + + it_behaves_like 'a #list_all_commits message' + end + + context 'mixed existing and nonexisting commits' do + let(:revision_existence) do + { + gitaly_commits[0].id => true, + gitaly_commits[1].id => false, + gitaly_commits[2].id => true + } + end + + let(:expected_commits) { [Gitlab::Git::Commit.new(repository, gitaly_commits[1])] } + + it_behaves_like 'a #list_all_commits message' + end + end end context 'with disallowed quarantine' do @@ -493,6 +557,61 @@ RSpec.describe Gitlab::GitalyClient::CommitService do end end + describe '#object_existence_map' do + shared_examples 'a CheckObjectsExistRequest' do + before do + ::Gitlab::GitalyClient.clear_stubs! + end + + it 'returns expected results' do + expect_next_instance_of(Gitaly::CommitService::Stub) do |service| + expect(service) + .to receive(:check_objects_exist) + .and_call_original + end + + expect(client.object_existence_map(revisions.keys)).to eq(revisions) + end + end + + context 'with empty request' do + let(:revisions) { {} } + + it_behaves_like 'a CheckObjectsExistRequest' + end + + context 'when revision exists' do + let(:revisions) { { 'refs/heads/master' => true } } + + it_behaves_like 'a CheckObjectsExistRequest' + end + + context 'when revision does not exist' do + let(:revisions) { { 'refs/does/not/exist' => false } } + + it_behaves_like 'a CheckObjectsExistRequest' + end + + context 'when request contains mixed revisions' do + let(:revisions) do + { + "refs/heads/master" => true, + "refs/does/not/exist" => false + } + end + + it_behaves_like 'a CheckObjectsExistRequest' + end + + context 'when requesting many revisions' do + let(:revisions) do + Array(1..1234).to_h { |i| ["refs/heads/#{i}", false] } + end + + it_behaves_like 'a CheckObjectsExistRequest' + end + end + describe '#commits_by_message' do shared_examples 'a CommitsByMessageRequest' do let(:commits) { create_list(:gitaly_commit, 2) } diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb index 321ad7d3238..8eeb2332131 100644 --- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb @@ -181,8 +181,10 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail expect(Gitlab::GithubImport::Logger) .to receive(:warn) .with( - message: "Validation failed: Line code can't be blank, Line code must be a valid line code, Position is incomplete", - 'error.class': 'Gitlab::GithubImport::Importer::DiffNoteImporter::DiffNoteCreationError' + { + message: "Validation failed: Line code can't be blank, Line code must be a valid line code, Position is incomplete", + 'error.class': 'Gitlab::GithubImport::Importer::DiffNoteImporter::DiffNoteCreationError' + } ) expect { subject.execute } @@ -204,8 +206,10 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail expect(Gitlab::GithubImport::Logger) .to receive(:warn) .with( - message: 'Failed to create diff note file', - 'error.class': 'DiffNote::NoteDiffFileCreationError' + { + message: 'Failed to create diff note file', + 'error.class': 'DiffNote::NoteDiffFileCreationError' + } ) expect { subject.execute } diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb index c5fa67e50aa..0eb86feb040 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 1) + .with(:pull_request_reviews, 'github/repo', merge_request.iid, { page: 1 }) .and_yield(page) expect { |b| subject.each_object_to_import(&b) } @@ -67,7 +67,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 2) + .with(:pull_request_reviews, 'github/repo', merge_request.iid, { page: 2 }) subject.each_object_to_import {} end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb index 8c71d7d0ed7..471302cb31b 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_diff_notes_importer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointDiffNotesImporter d expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:pull_request_comments, 'github/repo', merge_request.iid, page: 1) + .with(:pull_request_comments, 'github/repo', merge_request.iid, { page: 1 }) .and_yield(page) expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(note) @@ -56,7 +56,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointDiffNotesImporter d expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:pull_request_comments, 'github/repo', merge_request.iid, page: 2) + .with(:pull_request_comments, 'github/repo', merge_request.iid, { page: 2 }) subject.each_object_to_import {} end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb index 8d8f2730880..d769f4fdcf5 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_notes_importer_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueNotesImporter expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:issue_comments, 'github/repo', issue.iid, page: 1) + .with(:issue_comments, 'github/repo', issue.iid, { page: 1 }) .and_yield(page) expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(note) @@ -55,7 +55,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueNotesImporter expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:issue_comments, 'github/repo', issue.iid, page: 2) + .with(:issue_comments, 'github/repo', issue.iid, { page: 2 }) subject.each_object_to_import {} end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb index b8282212a90..1dcc466d34c 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_merge_request_notes_importer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointMergeRequestNotesIm expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:issue_comments, 'github/repo', merge_request.iid, page: 1) + .with(:issue_comments, 'github/repo', merge_request.iid, { page: 1 }) .and_yield(page) expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(note) @@ -56,7 +56,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointMergeRequestNotesIm expect(client) .to receive(:each_page) .exactly(:once) # ensure to be cached on the second call - .with(:issue_comments, 'github/repo', merge_request.iid, page: 2) + .with(:issue_comments, 'github/repo', merge_request.iid, { page: 2 }) subject.each_object_to_import {} end diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb index fe8652eb5a2..e7f47d334e8 100644 --- a/spec/lib/gitlab/github_import/milestone_finder_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache it 'builds the cache of all project milestones' do expect(Gitlab::Cache::Import::Caching) .to receive(:write_multiple) - .with("github-import/milestone-finder/#{project.id}/1" => milestone.id) + .with({ "github-import/milestone-finder/#{project.id}/1" => milestone.id }) .and_call_original finder.build_cache diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb index 200898f8f03..999f8ffb21e 100644 --- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb @@ -87,19 +87,23 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - message: 'starting importer', - parallel: false, - project_id: project.id, - importer: 'Class' + { + message: 'starting importer', + parallel: false, + project_id: project.id, + importer: 'Class' + } ) expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - message: 'importer finished', - parallel: false, - project_id: project.id, - importer: 'Class' + { + message: 'importer finished', + parallel: false, + project_id: project.id, + importer: 'Class' + } ) importer.execute @@ -118,20 +122,24 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - message: 'starting importer', - parallel: false, - project_id: project.id, - importer: 'Class' + { + message: 'starting importer', + parallel: false, + project_id: project.id, + importer: 'Class' + } ) expect(Gitlab::Import::ImportFailureService) .to receive(:track) .with( - project_id: project.id, - exception: exception, - error_source: 'MyImporter', - fail_import: false, - metrics: true + { + project_id: project.id, + exception: exception, + error_source: 'MyImporter', + fail_import: false, + metrics: true + } ).and_call_original expect { importer.execute } @@ -184,10 +192,12 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - message: 'starting importer', - parallel: false, - project_id: project.id, - importer: 'Class' + { + message: 'starting importer', + parallel: false, + project_id: project.id, + importer: 'Class' + } ) expect(Gitlab::Import::ImportFailureService) @@ -290,25 +300,6 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do importer.parallel_import end end - - context 'when distribute_github_parallel_import feature flag is disabled' do - before do - stub_feature_flags(distribute_github_parallel_import: false) - end - - it 'imports data in parallel' do - expect(importer) - .to receive(:each_object_to_import) - .and_yield(object) - - expect(worker_class) - .to receive(:perform_async) - .with(project.id, { title: 'Foo' }, an_instance_of(String)) - - expect(importer.parallel_import) - .to be_an_instance_of(Gitlab::JobWaiter) - end - end end describe '#each_object_to_import' do diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index 28cb9125af1..dd4dcca809b 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -44,6 +44,7 @@ RSpec.describe Gitlab::GonHelper do describe '#push_frontend_feature_flag' do before do skip_feature_flags_yaml_validation + skip_default_enabled_yaml_check end it 'pushes a feature flag to the frontend' do diff --git a/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb b/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb deleted file mode 100644 index 1b9301cd1aa..00000000000 --- a/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::FindArgumentInParent do - describe '#find' do - def build_node(parent = nil, args: {}) - props = { irep_node: double(arguments: args) } - props[:parent] = parent if parent # The root node shouldn't respond to parent - - double(props) - end - - let(:parent) do - build_node( - build_node( - build_node( - build_node, - args: { myArg: 1 } - ) - ) - ) - end - - let(:arg_name) { :my_arg } - - it 'searches parents and returns the argument' do - expect(described_class.find(parent, :my_arg)).to eq(1) - end - - it 'can find argument when passed in as both Ruby and GraphQL-formatted symbols and strings' do - [:my_arg, :myArg, 'my_arg', 'myArg'].each do |arg| - expect(described_class.find(parent, arg)).to eq(1) - end - end - - it 'returns nil if no arguments found in parents' do - expect(described_class.find(parent, :bar)).to eq(nil) - end - - it 'can limit the depth it searches to' do - expect(described_class.find(parent, :my_arg, limit_depth: 1)).to eq(nil) - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb index 86e7d4e344c..b6c3cb4e04a 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb @@ -3,13 +3,15 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do + include GraphqlHelpers + # https://gitlab.com/gitlab-org/gitlab/-/issues/334973 # The spec will be merged with connection_spec.rb in the future. let(:nodes) { Project.all.order(id: :asc) } let(:arguments) { {} } let(:query_type) { GraphQL::ObjectType.new } let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} - let(:context) { GraphQL::Query::Context.new(query: double('query', schema: schema), values: nil, object: nil) } + let(:context) { GraphQL::Query::Context.new(query: query_double(schema: schema), values: nil, object: nil) } let_it_be(:column_order_id) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].asc) } let_it_be(:column_order_id_desc) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].desc) } diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index f31ec6c09fd..a4ba288b7f1 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do + include GraphqlHelpers + let(:nodes) { Project.all.order(id: :asc) } let(:arguments) { {} } let(:query_type) { GraphQL::ObjectType.new } let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} - let(:context) { GraphQL::Query::Context.new(query: double('query', schema: schema), values: nil, object: nil) } + let(:context) { GraphQL::Query::Context.new(query: query_double(schema: schema), values: nil, object: nil) } subject(:connection) do described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb index ad1aaac712e..2c2ec821385 100644 --- a/spec/lib/gitlab/graphql/queries_spec.rb +++ b/spec/lib/gitlab/graphql/queries_spec.rb @@ -85,11 +85,15 @@ RSpec.describe Gitlab::Graphql::Queries do describe '.all' do it 'is the combination of finding queries in CE and EE' do expect(described_class) - .to receive(:find).with(Rails.root / 'app/assets/javascripts').and_return([:ce]) + .to receive(:find).with(Rails.root / 'app/assets/javascripts').and_return([:ce_assets]) expect(described_class) - .to receive(:find).with(Rails.root / 'ee/app/assets/javascripts').and_return([:ee]) + .to receive(:find).with(Rails.root / 'ee/app/assets/javascripts').and_return([:ee_assets]) + expect(described_class) + .to receive(:find).with(Rails.root / 'app/graphql/queries').and_return([:ce_gql]) + expect(described_class) + .to receive(:find).with(Rails.root / 'ee/app/graphql/queries').and_return([:ee_gql]) - expect(described_class.all).to eq([:ce, :ee]) + expect(described_class.all).to contain_exactly(:ce_assets, :ee_assets, :ce_gql, :ee_gql) end end diff --git a/spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb b/spec/lib/gitlab/health_checks/middleware_spec.rb index 9ee46a45e7a..3b644539acc 100644 --- a/spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb +++ b/spec/lib/gitlab/health_checks/middleware_spec.rb @@ -2,12 +2,12 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Metrics::Exporter::HealthChecksMiddleware do - let(:app) { double(:app) } +RSpec.describe Gitlab::HealthChecks::Middleware do + let(:app) { instance_double(Proc) } let(:env) { { 'PATH_INFO' => path } } - let(:readiness_probe) { double(:readiness_probe) } - let(:liveness_probe) { double(:liveness_probe) } + let(:readiness_probe) { instance_double(Gitlab::HealthChecks::Probes::Collection) } + let(:liveness_probe) { instance_double(Gitlab::HealthChecks::Probes::Collection) } let(:probe_result) { Gitlab::HealthChecks::Probes::Status.new(200, { status: 'ok' }) } subject(:middleware) { described_class.new(app, readiness_probe, liveness_probe) } diff --git a/spec/lib/gitlab/health_checks/server_spec.rb b/spec/lib/gitlab/health_checks/server_spec.rb new file mode 100644 index 00000000000..65d24acbf22 --- /dev/null +++ b/spec/lib/gitlab/health_checks/server_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HealthChecks::Server do + context 'with running server thread' do + subject(:server) { described_class.new(address: 'localhost', port: 8082) } + + before do + # We need to send a request to localhost + WebMock.allow_net_connect! + + server.start + end + + after do + webmock_enable! + + server.stop + end + + shared_examples 'serves health check at' do |path| + it 'responds with 200 OK' do + response = Gitlab::HTTP.try_get("http://localhost:8082/#{path}", allow_local_requests: true) + + expect(response.code).to eq(200) + end + end + + describe '/readiness' do + it_behaves_like 'serves health check at', 'readiness' + end + + describe '/liveness' do + it_behaves_like 'serves health check at', 'liveness' + end + + describe 'other routes' do + it 'serves 404' do + response = Gitlab::HTTP.try_get("http://localhost:8082/other", allow_local_requests: true) + + expect(response.code).to eq(404) + end + end + end + + context 'when server thread goes away' do + before do + expect_next_instance_of(::WEBrick::HTTPServer) do |webrick| + allow(webrick).to receive(:start) + expect(webrick).to receive(:listeners).and_call_original + end + end + + specify 'stop closes TCP socket' do + server = described_class.new(address: 'localhost', port: 8082) + server.start + + expect(server.thread).to receive(:alive?).and_return(false).at_least(:once) + + server.stop + end + end +end diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index 7dbd21e6914..c2fb987d195 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -246,10 +246,10 @@ RSpec.describe Gitlab::HTTP do context 'when :timeout is set' do it 'does not set any default timeouts' do expect(described_class).to receive(:httparty_perform_request).with( - Net::HTTP::Get, 'http://example.org', timeout: 1 + Net::HTTP::Get, 'http://example.org', { timeout: 1 } ).and_call_original - described_class.get('http://example.org', timeout: 1) + described_class.get('http://example.org', { timeout: 1 }) end end diff --git a/spec/lib/gitlab/import/import_failure_service_spec.rb b/spec/lib/gitlab/import/import_failure_service_spec.rb index e3fec63adde..eb71b307b8d 100644 --- a/spec/lib/gitlab/import/import_failure_service_spec.rb +++ b/spec/lib/gitlab/import/import_failure_service_spec.rb @@ -64,19 +64,23 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do .to receive(:track_exception) .with( exception, - project_id: project.id, - import_type: import_type, - source: 'SomeImporter' + { + project_id: project.id, + import_type: import_type, + source: 'SomeImporter' + } ) expect(Gitlab::Import::Logger) .to receive(:error) .with( - message: 'importer failed', - 'error.message': 'some error', - project_id: project.id, - import_type: import_type, - source: 'SomeImporter' + { + message: 'importer failed', + 'error.message': 'some error', + project_id: project.id, + import_type: import_type, + source: 'SomeImporter' + } ) service.execute @@ -96,19 +100,23 @@ RSpec.describe Gitlab::Import::ImportFailureService, :aggregate_failures do .to receive(:track_exception) .with( exception, - project_id: project.id, - import_type: import_type, - source: 'SomeImporter' + { + project_id: project.id, + import_type: import_type, + source: 'SomeImporter' + } ) expect(Gitlab::Import::Logger) .to receive(:error) .with( - message: 'importer failed', - 'error.message': 'some error', - project_id: project.id, - import_type: import_type, - source: 'SomeImporter' + { + message: 'importer failed', + 'error.message': 'some error', + project_id: project.id, + import_type: import_type, + source: 'SomeImporter' + } ) service.execute diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 730f9035293..1546b6a26c8 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -550,6 +550,7 @@ project: - project_registry - packages - package_files +- packages_cleanup_policy - tracing_setting - alerting_setting - project_setting 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 8e7fe8849d4..9dbe8426f52 100644 --- a/spec/lib/gitlab/import_export/group/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb @@ -88,6 +88,21 @@ RSpec.describe Gitlab::ImportExport::Group::RelationFactory do end end + context 'when relation is namespace_settings' do + let(:relation_sym) { :namespace_settings } + let(:relation_hash) do + { + 'namespace_id' => 1, + 'prevent_forking_outside_group' => true, + 'prevent_sharing_groups_outside_hierarchy' => true + } + end + + it do + expect(created_object).to eq(nil) + end + end + def random_id rand(1000..10000) 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 a3e891db658..d3397e89f1f 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -383,7 +383,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end end - it 'restores releases with links' do + it 'restores releases with links & milestones' do release = @project.releases.last link = release.links.last @@ -393,6 +393,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do expect(release.name).to eq('release-1.1') expect(release.sha).to eq('901de3a8bd5573f4a049b1457d28bc1592ba6bf9') expect(release.released_at).to eq('2019-12-26T10:17:14.615Z') + expect(release.milestone_releases.count).to eq(1) + expect(release.milestone_releases.first.milestone.title).to eq('test milestone') expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download') expect(link.name).to eq('release-1.1.dmg') diff --git a/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb new file mode 100644 index 00000000000..4eb2388f3f7 --- /dev/null +++ b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker do + let_it_be(:project_id) { 1 } + + describe '.notified_projects', :clean_gitlab_redis_shared_state do + before do + freeze_time do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified + end + end + + it 'returns the list of projects for which deletion warning email has been sent' do + expected_hash = { "project:1" => "#{Date.current}" } + + expect(Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects).to eq(expected_hash) + end + end + + describe '.reset_all' do + before do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified + end + + it 'deletes all the projects for which deletion warning email was sent' do + Gitlab::InactiveProjectsDeletionWarningTracker.reset_all + + expect(Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects).to eq({}) + end + end + + describe '#notified?' do + before do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified + end + + it 'returns true if the project has already been notified' do + expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(true) + end + + it 'returns false if the project has not been notified' do + expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(2).notified?).to eq(false) + end + end + + describe '#mark_notified' do + it 'marks the project as being notified' do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified + + expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(true) + end + end + + describe '#reset' do + before do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified + end + + it 'resets the project as not being notified' do + Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).reset + + expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb b/spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb new file mode 100644 index 00000000000..ac308eb7c80 --- /dev/null +++ b/spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Instrumentation::RateLimitingGates, :request_store do + describe '.gates' do + it 'returns an empty array when no gates are tracked' do + expect(described_class.gates).to eq([]) + end + + it 'returns all gates used in the request' do + described_class.track(:foo) + + RequestStore.clear! + + described_class.track(:bar) + described_class.track(:baz) + + expect(described_class.gates).to contain_exactly(:bar, :baz) + end + + it 'deduplicates its results' do + described_class.track(:foo) + described_class.track(:bar) + described_class.track(:foo) + + expect(described_class.gates).to contain_exactly(:foo, :bar) + end + end + + describe '.payload' do + it 'returns the gates in a hash' do + described_class.track(:foo) + described_class.track(:bar) + + expect(described_class.payload).to eq(described_class::GATES => [:foo, :bar]) + end + end +end diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index a9663012e9a..5fea355ab4f 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -77,6 +77,27 @@ RSpec.describe Gitlab::InstrumentationHelper do end end + context 'rate-limiting gates' do + context 'when the request did not pass through any rate-limiting gates' do + it 'logs an empty array of gates' do + subject + + expect(payload[:rate_limiting_gates]).to eq([]) + end + end + + context 'when the request passed through rate-limiting gates' do + it 'logs an array of gates used' do + Gitlab::Instrumentation::RateLimitingGates.track(:foo) + Gitlab::Instrumentation::RateLimitingGates.track(:bar) + + subject + + expect(payload[:rate_limiting_gates]).to contain_exactly(:foo, :bar) + end + end + end + it 'logs cpu_s duration' do subject diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb index 1fe22b145a6..e7a79e40ac5 100644 --- a/spec/lib/gitlab/jira/middleware_spec.rb +++ b/spec/lib/gitlab/jira/middleware_spec.rb @@ -23,8 +23,8 @@ RSpec.describe Gitlab::Jira::Middleware do describe '#call' do it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do - expect(app).to receive(:call).with('HTTP_USER_AGENT' => jira_user_agent, - 'HTTP_AUTHORIZATION' => 'Bearer hash-123') + expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent, + 'HTTP_AUTHORIZATION' => 'Bearer hash-123' }) middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123') end diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index d7d28a94cfe..f4f6624bae9 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -313,9 +313,9 @@ RSpec.describe Gitlab::JsonCache do it 'passes options the underlying cache implementation' do expect(backend).to receive(:write) - .with(expanded_key, "true", expires_in: 15.seconds) + .with(expanded_key, "true", { expires_in: 15.seconds }) - cache.fetch(key, expires_in: 15.seconds) { true } + cache.fetch(key, { expires_in: 15.seconds }) { true } end context 'when the given key does not exist in the cache' do diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb index 5ffe736da54..7c093049e18 100644 --- a/spec/lib/gitlab/json_spec.rb +++ b/spec/lib/gitlab/json_spec.rb @@ -290,7 +290,7 @@ RSpec.describe Gitlab::Json do end it "skips legacy mode handling" do - expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true) + expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode) subject.send(:handle_legacy_mode!, {}) end diff --git a/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb b/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb deleted file mode 100644 index ec1f46100a4..00000000000 --- a/spec/lib/gitlab/kubernetes/cilium_network_policy_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do - let(:policy) do - described_class.new( - name: name, - namespace: namespace, - description: description, - selector: selector, - ingress: ingress, - egress: egress, - labels: labels, - resource_version: resource_version, - annotations: annotations - ) - end - - let(:resource) do - ::Kubeclient::Resource.new( - apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, - kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version, annotations: annotations }, - spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: egress }, - description: description - ) - end - - let(:selector) { endpoint_selector } - let(:labels) { nil } - let(:name) { 'example-name' } - let(:namespace) { 'example-namespace' } - let(:endpoint_selector) { { matchLabels: { role: 'db' } } } - let(:description) { 'example-description' } - let(:partial_class_name) { described_class.name.split('::').last } - let(:resource_version) { 101 } - let(:annotations) { { 'app.gitlab.com/alert': 'true' } } - let(:ingress) do - [ - { - fromEndpoints: [ - { matchLabels: { project: 'myproject' } } - ] - } - ] - end - - let(:egress) do - [ - { - ports: [{ port: 5978 }] - } - ] - end - - include_examples 'network policy common specs' - - describe '.from_yaml' do - let(:manifest) do - <<~POLICY - apiVersion: cilium.io/v2 - kind: CiliumNetworkPolicy - description: example-description - metadata: - name: example-name - namespace: example-namespace - resourceVersion: 101 - annotations: - app.gitlab.com/alert: "true" - spec: - endpointSelector: - matchLabels: - role: db - ingress: - - fromEndpoints: - - matchLabels: - project: myproject - egress: - - ports: - - port: 5978 - POLICY - end - - subject { Gitlab::Kubernetes::CiliumNetworkPolicy.from_yaml(manifest)&.generate } - - it { is_expected.to eq(resource) } - - context 'with nil manifest' do - let(:manifest) { nil } - - it { is_expected.to be_nil } - end - - context 'with invalid manifest' do - let(:manifest) { "\tfoo: bar" } - - it { is_expected.to be_nil } - end - - context 'with manifest without metadata' do - let(:manifest) do - <<~POLICY - apiVersion: cilium.io/v2 - kind: CiliumNetworkPolicy - spec: - endpointSelector: - matchLabels: - role: db - ingress: - - fromEndpoints: - matchLabels: - project: myproject - POLICY - end - - it { is_expected.to be_nil } - end - - context 'with manifest without spec' do - let(:manifest) do - <<~POLICY - apiVersion: cilium.io/v2 - kind: CiliumNetworkPolicy - metadata: - name: example-name - namespace: example-namespace - POLICY - end - - it { is_expected.to be_nil } - end - - context 'with disallowed class' do - let(:manifest) do - <<~POLICY - apiVersion: cilium.io/v2 - kind: CiliumNetworkPolicy - metadata: - name: example-name - namespace: example-namespace - creationTimestamp: 2020-04-14T00:08:30Z - spec: - endpointSelector: - matchLabels: - role: db - ingress: - - fromEndpoints: - matchLabels: - project: myproject - POLICY - end - - it { is_expected.to be_nil } - end - end - - describe '.from_resource' do - let(:resource) do - ::Kubeclient::Resource.new( - description: description, - metadata: { - name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', - labels: { app: 'foo' }, resourceVersion: resource_version, annotations: annotations - }, - spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: nil, labels: nil } - ) - end - - let(:generated_resource) do - ::Kubeclient::Resource.new( - apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, - kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, - description: description, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version, labels: { app: 'foo' }, annotations: annotations }, - spec: { endpointSelector: endpoint_selector, ingress: ingress } - ) - end - - subject { Gitlab::Kubernetes::CiliumNetworkPolicy.from_resource(resource)&.generate } - - it { is_expected.to eq(generated_resource) } - - context 'with nil resource' do - let(:resource) { nil } - - it { is_expected.to be_nil } - end - - context 'with resource without metadata' do - let(:resource) do - ::Kubeclient::Resource.new( - spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: nil, labels: nil } - ) - end - - it { is_expected.to be_nil } - end - - context 'with resource without spec' do - let(:resource) do - ::Kubeclient::Resource.new( - metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: resource_version } - ) - end - - it { is_expected.to be_nil } - end - - context 'with environment_ids' do - subject { Gitlab::Kubernetes::CiliumNetworkPolicy.from_resource(resource, [1, 2, 3]) } - - it 'includes environment_ids in as_json result' do - expect(subject.as_json).to include(environment_ids: [1, 2, 3]) - end - end - end - - describe '#resource' do - subject { policy.resource } - - let(:resource) do - { - apiVersion: Gitlab::Kubernetes::CiliumNetworkPolicy::API_VERSION, - kind: Gitlab::Kubernetes::CiliumNetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, resourceVersion: resource_version, annotations: annotations }, - spec: { endpointSelector: endpoint_selector, ingress: ingress, egress: egress }, - description: description - } - end - - it { is_expected.to eq(resource) } - - context 'with labels' do - let(:labels) { { app: 'foo' } } - - before do - resource[:metadata][:labels] = { app: 'foo' } - end - - it { is_expected.to eq(resource) } - end - - context 'without resource_version' do - let(:resource_version) { nil } - - before do - resource[:metadata].delete(:resourceVersion) - end - - it { is_expected.to eq(resource) } - end - - context 'with nil egress' do - let(:egress) { nil } - - before do - resource[:spec].delete(:egress) - end - - it { is_expected.to eq(resource) } - end - - context 'without annotations' do - let(:annotations) { nil } - - before do - resource[:metadata].delete(:annotations) - end - - it { is_expected.to eq(resource) } - end - end -end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 521f13dc9cc..dfd5092b54d 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -227,20 +227,6 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do end end - describe '#cilium_networking_client' do - subject { client.cilium_networking_client } - - it_behaves_like 'a Kubeclient' - - it 'has the cilium API group endpoint' do - expect(subject.api_endpoint.to_s).to match(%r{\/apis\/cilium.io\Z}) - end - - it 'has the api_version' do - expect(subject.instance_variable_get(:@api_version)).to eq('v2') - end - end - describe '#metrics_client' do subject { client.metrics_client } @@ -428,56 +414,6 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do end end - describe 'networking API group' do - let(:networking_client) { client.networking_client } - - [ - :create_network_policy, - :get_network_policies, - :get_network_policy, - :update_network_policy, - :delete_network_policy - ].each do |method| - describe "##{method}" do - include_examples 'redirection not allowed', method - include_examples 'dns rebinding not allowed', method - - it 'delegates to the networking client' do - expect(client).to delegate_method(method).to(:networking_client) - end - - it 'responds to the method' do - expect(client).to respond_to method - end - end - end - end - - describe 'cilium API group' do - let(:cilium_networking_client) { client.cilium_networking_client } - - [ - :create_cilium_network_policy, - :get_cilium_network_policies, - :get_cilium_network_policy, - :update_cilium_network_policy, - :delete_cilium_network_policy - ].each do |method| - describe "##{method}" do - include_examples 'redirection not allowed', method - include_examples 'dns rebinding not allowed', method - - it 'delegates to the cilium client' do - expect(client).to delegate_method(method).to(:cilium_networking_client) - end - - it 'responds to the method' do - expect(client).to respond_to method - end - end - end - end - describe 'non-entity methods' do it 'does not proxy for non-entity methods' do expect(client).not_to respond_to :proxy_url diff --git a/spec/lib/gitlab/kubernetes/network_policy_spec.rb b/spec/lib/gitlab/kubernetes/network_policy_spec.rb deleted file mode 100644 index 2cba37a1302..00000000000 --- a/spec/lib/gitlab/kubernetes/network_policy_spec.rb +++ /dev/null @@ -1,235 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Kubernetes::NetworkPolicy do - let(:policy) do - described_class.new( - name: name, - namespace: namespace, - selector: selector, - ingress: ingress, - labels: labels - ) - end - - let(:resource) do - ::Kubeclient::Resource.new( - kind: Gitlab::Kubernetes::NetworkPolicy::KIND, - metadata: { name: name, namespace: namespace }, - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - ) - end - - let(:selector) { pod_selector } - let(:labels) { nil } - let(:name) { 'example-name' } - let(:namespace) { 'example-namespace' } - let(:pod_selector) { { matchLabels: { role: 'db' } } } - - let(:ingress) do - [ - { - from: [ - { namespaceSelector: { matchLabels: { project: 'myproject' } } } - ] - } - ] - end - - let(:egress) do - [ - { - ports: [{ port: 5978 }] - } - ] - end - - include_examples 'network policy common specs' - - describe '.from_yaml' do - let(:manifest) do - <<~POLICY - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: example-name - namespace: example-namespace - spec: - podSelector: - matchLabels: - role: db - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - project: myproject - POLICY - end - - subject { Gitlab::Kubernetes::NetworkPolicy.from_yaml(manifest)&.generate } - - it { is_expected.to eq(resource) } - - context 'with nil manifest' do - let(:manifest) { nil } - - it { is_expected.to be_nil } - end - - context 'with invalid manifest' do - let(:manifest) { "\tfoo: bar" } - - it { is_expected.to be_nil } - end - - context 'with manifest without metadata' do - let(:manifest) do - <<~POLICY - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - spec: - podSelector: - matchLabels: - role: db - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - project: myproject - POLICY - end - - it { is_expected.to be_nil } - end - - context 'with manifest without spec' do - let(:manifest) do - <<~POLICY - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: example-name - namespace: example-namespace - POLICY - end - - it { is_expected.to be_nil } - end - - context 'with disallowed class' do - let(:manifest) do - <<~POLICY - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: example-name - namespace: example-namespace - creationTimestamp: 2020-04-14T00:08:30Z - spec: - podSelector: - matchLabels: - role: db - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - project: myproject - POLICY - end - - it { is_expected.to be_nil } - end - end - - describe '.from_resource' do - let(:resource) do - ::Kubeclient::Resource.new( - metadata: { - name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', - labels: { app: 'foo' }, resourceVersion: '4990' - }, - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - ) - end - - let(:generated_resource) do - ::Kubeclient::Resource.new( - kind: Gitlab::Kubernetes::NetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, labels: { app: 'foo' } }, - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - ) - end - - subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)&.generate } - - it { is_expected.to eq(generated_resource) } - - context 'with nil resource' do - let(:resource) { nil } - - it { is_expected.to be_nil } - end - - context 'with resource without metadata' do - let(:resource) do - ::Kubeclient::Resource.new( - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - ) - end - - it { is_expected.to be_nil } - end - - context 'with resource without spec' do - let(:resource) do - ::Kubeclient::Resource.new( - metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: '4990' } - ) - end - - it { is_expected.to be_nil } - end - - context 'with environment_ids' do - subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource, [1, 2, 3]) } - - it 'includes environment_ids in as_json result' do - expect(subject.as_json).to include(environment_ids: [1, 2, 3]) - end - end - end - - describe '#resource' do - subject { policy.resource } - - let(:resource) do - { - kind: Gitlab::Kubernetes::NetworkPolicy::KIND, - metadata: { name: name, namespace: namespace }, - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - } - end - - it { is_expected.to eq(resource) } - - context 'with labels' do - let(:labels) { { app: 'foo' } } - let(:resource) do - { - kind: Gitlab::Kubernetes::NetworkPolicy::KIND, - metadata: { name: name, namespace: namespace, labels: { app: 'foo' } }, - spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil } - } - end - - it { is_expected.to eq(resource) } - end - end -end diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index 9a4d7bd996e..e69edbe6dc0 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -274,8 +274,7 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do it 'instantiates a Client' do allow(project).to receive(:import_data).and_return(double(credentials: credentials)) expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with( - credentials[:user], - **{} + credentials[:user] ) subject.client diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb index d8f351bb8a3..58b05be6ff9 100644 --- a/spec/lib/gitlab/lograge/custom_options_spec.rb +++ b/spec/lib/gitlab/lograge/custom_options_spec.rb @@ -96,23 +96,15 @@ RSpec.describe Gitlab::Lograge::CustomOptions do end end - context 'when feature flags are present', :request_store do + context 'when feature flags are present', :request_store do before do allow(Feature).to receive(:log_feature_flag_states?).and_return(false) - definitions = {} [:enabled_feature, :disabled_feature].each do |flag_name| - definitions[flag_name] = Feature::Definition.new("development/enabled_feature.yml", - name: flag_name, - type: 'development', - log_state_changes: true, - default_enabled: false) - + stub_feature_flag_definition(flag_name, log_state_changes: true) allow(Feature).to receive(:log_feature_flag_states?).with(flag_name).and_call_original end - allow(Feature::Definition).to receive(:definitions).and_return(definitions) - Feature.enable(:enabled_feature) Feature.disable(:disabled_feature) end diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb index 98385cd80cc..d22bef5bda9 100644 --- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -171,9 +171,9 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do expect(thing).to receive(:persisted?).and_return(true) expect(thing).to receive(:update_columns) - .with("title_html" => updated_html, + .with({ "title_html" => updated_html, "description_html" => "", - "cached_markdown_version" => cache_version) + "cached_markdown_version" => cache_version }) thing.refresh_markdown_cache! 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 ff8f5797f9d..c15e717b126 100644 --- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb @@ -12,12 +12,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do 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 } @@ -45,13 +39,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do 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 subject.execute @@ -68,15 +55,6 @@ 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, @@ -87,13 +65,6 @@ 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 @@ -111,21 +82,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do 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/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb index c7afc02f0af..66fba7ab683 100644 --- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb +++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb @@ -152,8 +152,6 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do where(:method_class, :path, :http_status) do Net::HTTP::Get | '/metrics' | 200 - Net::HTTP::Get | '/liveness' | 200 - Net::HTTP::Get | '/readiness' | 200 Net::HTTP::Get | '/' | 404 end diff --git a/spec/lib/gitlab/metrics/methods_spec.rb b/spec/lib/gitlab/metrics/methods_spec.rb index 71135a6e9c5..eb7c8891e98 100644 --- a/spec/lib/gitlab/metrics/methods_spec.rb +++ b/spec/lib/gitlab/metrics/methods_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Gitlab::Metrics::Methods do context 'metric is not cached' do it 'calls fetch_metric' do - expect(subject).to receive(:init_metric).with(metric_type, metric_name, docstring: docstring) + expect(subject).to receive(:init_metric).with(metric_type, metric_name, { docstring: docstring }) subject.public_send(metric_name) end diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb index 2ba06316507..b30eb57101f 100644 --- a/spec/lib/gitlab/metrics/rails_slis_spec.rb +++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb @@ -36,18 +36,8 @@ RSpec.describe Gitlab::Metrics::RailsSlis do } end - expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:rails_request_apdex) { false } - expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:graphql_query_apdex) { false } - expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:rails_request_apdex, array_including(*possible_labels)).and_call_original - expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:graphql_query_apdex, array_including(*possible_graphql_labels)).and_call_original - - described_class.initialize_request_slis! - end - - it 'does not initialize the SLI if they were initialized already', :aggregate_failures do - expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:rails_request_apdex) { true } - expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:graphql_query_apdex) { true } - expect(Gitlab::Metrics::Sli).not_to receive(:initialize_sli) + expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:rails_request, array_including(*possible_labels)).and_call_original + expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:graphql_query, array_including(*possible_graphql_labels)).and_call_original described_class.initialize_request_slis! end diff --git a/spec/lib/gitlab/metrics/sli_spec.rb b/spec/lib/gitlab/metrics/sli_spec.rb index 8ba4bf29568..9b776d6738d 100644 --- a/spec/lib/gitlab/metrics/sli_spec.rb +++ b/spec/lib/gitlab/metrics/sli_spec.rb @@ -10,72 +10,151 @@ RSpec.describe Gitlab::Metrics::Sli do end describe 'Class methods' do - before do - described_class.instance_variable_set(:@known_slis, nil) + it 'does not allow them to be called on the parent module' do + expect(described_class).not_to respond_to(:[]) + expect(described_class).not_to respond_to(:initialize_sli) end - describe '.[]' do - it 'warns about an uninitialized SLI but returns and stores a new one' do - sli = described_class[:bar] + it 'allows different SLIs to be defined on each subclass' do + apdex_counters = [ + fake_total_counter('foo', 'apdex'), + fake_numerator_counter('foo', 'apdex', 'success') + ] - expect(described_class[:bar]).to be(sli) - end + error_rate_counters = [ + fake_total_counter('foo', 'error_rate'), + fake_numerator_counter('foo', 'error_rate', 'error') + ] - it 'returns the same object for multiple accesses' do - sli = described_class.initialize_sli(:huzzah, []) + apdex = described_class::Apdex.initialize_sli(:foo, [{ hello: :world }]) - 2.times do - expect(described_class[:huzzah]).to be(sli) - end - end - end + expect(apdex_counters).to all(have_received(:get).with(hello: :world)) - describe '.initialized?' do - before do - fake_total_counter(:boom) - fake_success_counter(:boom) - end + error_rate = described_class::ErrorRate.initialize_sli(:foo, [{ other: :labels }]) - it 'is true when an SLI was initialized with labels' do - expect { described_class.initialize_sli(:boom, [{ hello: :world }]) } - .to change { described_class.initialized?(:boom) }.from(false).to(true) - end + expect(error_rate_counters).to all(have_received(:get).with(other: :labels)) - it 'is false when an SLI was not initialized with labels' do - expect { described_class.initialize_sli(:boom, []) } - .not_to change { described_class.initialized?(:boom) }.from(false) - end + expect(described_class::Apdex[:foo]).to be(apdex) + expect(described_class::ErrorRate[:foo]).to be(error_rate) end end - describe '#initialize_counters' do - it 'initializes counters for the passed label combinations' do - counters = [fake_total_counter(:hey), fake_success_counter(:hey)] + subclasses = { + Gitlab::Metrics::Sli::Apdex => :success, + Gitlab::Metrics::Sli::ErrorRate => :error + } - described_class.new(:hey).initialize_counters([{ foo: 'bar' }, { foo: 'baz' }]) + subclasses.each do |subclass, numerator_type| + subclass_type = subclass.to_s.demodulize.underscore - expect(counters).to all(have_received(:get).with({ foo: 'bar' })) - expect(counters).to all(have_received(:get).with({ foo: 'baz' })) - end - end + describe subclass do + describe 'Class methods' do + before do + described_class.instance_variable_set(:@known_slis, nil) + end - describe "#increment" do - let!(:sli) { described_class.new(:heyo) } - let!(:total_counter) { fake_total_counter(:heyo) } - let!(:success_counter) { fake_success_counter(:heyo) } + describe '.[]' do + it 'returns and stores a new, uninitialized SLI' do + sli = described_class[:bar] - it 'increments both counters for labels successes' do - sli.increment(labels: { hello: "world" }, success: true) + expect(described_class[:bar]).to be(sli) + expect(described_class[:bar]).not_to be_initialized + end - expect(total_counter).to have_received(:increment).with({ hello: 'world' }) - expect(success_counter).to have_received(:increment).with({ hello: 'world' }) - end + it 'returns the same object for multiple accesses' do + sli = described_class.initialize_sli(:huzzah, []) + + 2.times do + expect(described_class[:huzzah]).to be(sli) + end + end + end + + describe '.initialize_sli' do + it 'returns and stores a new initialized SLI' do + counters = [ + fake_total_counter(:bar, subclass_type), + fake_numerator_counter(:bar, subclass_type, numerator_type) + ] + + sli = described_class.initialize_sli(:bar, [{ hello: :world }]) + + expect(sli).to be_initialized + expect(counters).to all(have_received(:get).with(hello: :world)) + expect(counters).to all(have_received(:get).with(hello: :world)) + end + + it 'does not change labels for an already-initialized SLI' do + counters = [ + fake_total_counter(:bar, subclass_type), + fake_numerator_counter(:bar, subclass_type, numerator_type) + ] + + sli = described_class.initialize_sli(:bar, [{ hello: :world }]) - it 'only increments the total counters for labels when not successful' do - sli.increment(labels: { hello: "world" }, success: false) + expect(sli).to be_initialized + expect(counters).to all(have_received(:get).with(hello: :world)) + expect(counters).to all(have_received(:get).with(hello: :world)) - expect(total_counter).to have_received(:increment).with({ hello: 'world' }) - expect(success_counter).not_to have_received(:increment).with({ hello: 'world' }) + counters.each do |counter| + expect(counter).not_to receive(:get) + end + + expect(described_class.initialize_sli(:bar, [{ other: :labels }])).to eq(sli) + end + end + + describe '.initialized?' do + before do + fake_total_counter(:boom, subclass_type) + fake_numerator_counter(:boom, subclass_type, numerator_type) + end + + it 'is true when an SLI was initialized with labels' do + expect { described_class.initialize_sli(:boom, [{ hello: :world }]) } + .to change { described_class.initialized?(:boom) }.from(false).to(true) + end + + it 'is false when an SLI was not initialized with labels' do + expect { described_class.initialize_sli(:boom, []) } + .not_to change { described_class.initialized?(:boom) }.from(false) + end + end + end + + describe '#initialize_counters' do + it 'initializes counters for the passed label combinations' do + counters = [ + fake_total_counter(:hey, subclass_type), + fake_numerator_counter(:hey, subclass_type, numerator_type) + ] + + described_class.new(:hey).initialize_counters([{ foo: 'bar' }, { foo: 'baz' }]) + + expect(counters).to all(have_received(:get).with({ foo: 'bar' })) + expect(counters).to all(have_received(:get).with({ foo: 'baz' })) + end + end + + describe "#increment" do + let!(:sli) { described_class.new(:heyo) } + let!(:total_counter) { fake_total_counter(:heyo, subclass_type) } + let!(:numerator_counter) { fake_numerator_counter(:heyo, subclass_type, numerator_type) } + + it "increments both counters for labels when #{numerator_type} is true" do + sli.increment(labels: { hello: "world" }, numerator_type => true) + + expect(total_counter).to have_received(:increment).with({ hello: 'world' }) + expect(numerator_counter).to have_received(:increment).with({ hello: 'world' }) + end + + it "only increments the total counters for labels when #{numerator_type} is false" do + sli.increment(labels: { hello: "world" }, numerator_type => false) + + expect(total_counter).to have_received(:increment).with({ hello: 'world' }) + expect(numerator_counter).not_to have_received(:increment).with({ hello: 'world' }) + end + end end end @@ -89,11 +168,11 @@ RSpec.describe Gitlab::Metrics::Sli do fake_counter end - def fake_total_counter(name) - fake_prometheus_counter("gitlab_sli:#{name}:total") + def fake_total_counter(name, type) + fake_prometheus_counter("gitlab_sli:#{name}_#{type}:total") end - def fake_success_counter(name) - fake_prometheus_counter("gitlab_sli:#{name}:success_total") + def fake_numerator_counter(name, type, numerator_name) + fake_prometheus_counter("gitlab_sli:#{name}_#{type}:#{numerator_name}_total") end end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 389b0ef1044..28c3ef229ab 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -10,6 +10,124 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do let(:connection) { ActiveRecord::Base.retrieve_connection } let(:db_config_name) { ::Gitlab::Database.db_config_name(connection) } + describe '.load_balancing_metric_counter_keys' do + context 'multiple databases' do + before do + skip_if_multiple_databases_not_setup + end + + it 'has expected keys' do + expect(described_class.load_balancing_metric_counter_keys).to include( + :db_replica_count, + :db_primary_count, + :db_main_count, + :db_main_replica_count, + :db_ci_count, + :db_ci_replica_count, + :db_replica_cached_count, + :db_primary_cached_count, + :db_main_cached_count, + :db_main_replica_cached_count, + :db_ci_cached_count, + :db_ci_replica_cached_count, + :db_replica_wal_count, + :db_primary_wal_count, + :db_main_wal_count, + :db_main_replica_wal_count, + :db_ci_wal_count, + :db_ci_replica_wal_count, + :db_replica_wal_cached_count, + :db_primary_wal_cached_count, + :db_main_wal_cached_count, + :db_main_replica_wal_cached_count, + :db_ci_wal_cached_count, + :db_ci_replica_wal_cached_count + ) + end + end + + context 'single database' do + before do + skip_if_multiple_databases_are_setup + end + + it 'has expected keys' do + expect(described_class.load_balancing_metric_counter_keys).to include( + :db_replica_count, + :db_primary_count, + :db_main_count, + :db_main_replica_count, + :db_replica_cached_count, + :db_primary_cached_count, + :db_main_cached_count, + :db_main_replica_cached_count, + :db_replica_wal_count, + :db_primary_wal_count, + :db_main_wal_count, + :db_main_replica_wal_count, + :db_replica_wal_cached_count, + :db_primary_wal_cached_count, + :db_main_wal_cached_count, + :db_main_replica_wal_cached_count + ) + end + + it 'does not have ci keys' do + expect(described_class.load_balancing_metric_counter_keys).not_to include( + :db_ci_count, + :db_ci_replica_count, + :db_ci_cached_count, + :db_ci_replica_cached_count, + :db_ci_wal_count, + :db_ci_replica_wal_count, + :db_ci_wal_cached_count, + :db_ci_replica_wal_cached_count + ) + end + end + end + + describe '.load_balancing_metric_duration_keys' do + context 'multiple databases' do + before do + skip_if_multiple_databases_not_setup + end + + it 'has expected keys' do + expect(described_class.load_balancing_metric_duration_keys).to include( + :db_replica_duration_s, + :db_primary_duration_s, + :db_main_duration_s, + :db_main_replica_duration_s, + :db_ci_duration_s, + :db_ci_replica_duration_s + ) + end + end + + context 'single database' do + before do + skip_if_multiple_databases_are_setup + end + + it 'has expected keys' do + expect(described_class.load_balancing_metric_duration_keys).to include( + :db_replica_duration_s, + :db_primary_duration_s, + :db_main_duration_s, + :db_main_replica_duration_s + ) + end + + it 'does not have ci keys' do + expect(described_class.load_balancing_metric_duration_keys).not_to include( + :db_ci_duration_s, + :db_ci_replica_duration_s + ) + end + end + end + describe '#transaction' do let(:web_transaction) { double('Gitlab::Metrics::WebTransaction') } let(:background_transaction) { double('Gitlab::Metrics::WebTransaction') } @@ -37,7 +155,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do end it 'captures the metrics for web only' do - expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, db_config_name: db_config_name) + expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name }) expect(background_transaction).not_to receive(:observe) expect(background_transaction).not_to receive(:increment) @@ -77,7 +195,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do end it 'captures the metrics for web only' do - expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, db_config_name: db_config_name) + expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name }) expect(web_transaction).not_to receive(:observe) expect(web_transaction).not_to receive(:increment) diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb index fda4b94bd78..9f939d0d7d6 100644 --- a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb @@ -77,8 +77,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do end it 'logs request information' do - expect(Gitlab::AuthLogger).to receive(:error).with( - include( + expect(Gitlab::AuthLogger).to receive(:error) do |arguments| + expect(arguments).to include( message: 'Rack_Attack', env: match_type, remote_ip: '1.2.3.4', @@ -86,7 +86,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do path: '/api/v4/internal/authorized_keys', matched: 'throttle_unauthenticated' ) - ) + + if expected_status + expect(arguments).to include(status: expected_status) + else + expect(arguments).not_to have_key(:status) + end + end + subscriber.send(match_type, event) end end @@ -111,8 +118,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do end it 'logs request information and user id' do - expect(Gitlab::AuthLogger).to receive(:error).with( - include( + expect(Gitlab::AuthLogger).to receive(:error) do |arguments| + expect(arguments).to include( message: 'Rack_Attack', env: match_type, remote_ip: '1.2.3.4', @@ -121,7 +128,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do matched: 'throttle_authenticated_api', user_id: non_existing_record_id ) - ) + + if expected_status + expect(arguments).to include(status: expected_status) + else + expect(arguments).not_to have_key(:status) + end + end + subscriber.send(match_type, event) end end @@ -145,8 +159,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do end it 'logs request information and user meta' do - expect(Gitlab::AuthLogger).to receive(:error).with( - include( + expect(Gitlab::AuthLogger).to receive(:error) do |arguments| + expect(arguments).to include( message: 'Rack_Attack', env: match_type, remote_ip: '1.2.3.4', @@ -156,7 +170,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do user_id: user.id, 'meta.user' => user.username ) - ) + + if expected_status + expect(arguments).to include(status: expected_status) + else + expect(arguments).not_to have_key(:status) + end + end + subscriber.send(match_type, event) end end @@ -182,8 +203,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do end it 'logs request information and user meta' do - expect(Gitlab::AuthLogger).to receive(:error).with( - include( + expect(Gitlab::AuthLogger).to receive(:error) do |arguments| + expect(arguments).to include( message: 'Rack_Attack', env: match_type, remote_ip: '1.2.3.4', @@ -192,7 +213,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do matched: 'throttle_authenticated_api', deploy_token_id: deploy_token.id ) - ) + + if expected_status + expect(arguments).to include(status: expected_status) + else + expect(arguments).not_to have_key(:status) + end + end + subscriber.send(match_type, event) end end @@ -202,6 +230,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do describe '#throttle' do let(:match_type) { :throttle } + let(:expected_status) { 429 } let(:event_name) { 'throttle.rack_attack' } it_behaves_like 'log into auth logger' @@ -209,6 +238,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do describe '#blocklist' do let(:match_type) { :blocklist } + let(:expected_status) { 403 } let(:event_name) { 'blocklist.rack_attack' } it_behaves_like 'log into auth logger' @@ -216,6 +246,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do describe '#track' do let(:match_type) { :track } + let(:expected_status) { nil } let(:event_name) { 'track.rack_attack' } it_behaves_like 'log into auth logger' diff --git a/spec/lib/gitlab/patch/database_config_spec.rb b/spec/lib/gitlab/patch/database_config_spec.rb index d6f36ab86d5..73dc84bb2ef 100644 --- a/spec/lib/gitlab/patch/database_config_spec.rb +++ b/spec/lib/gitlab/patch/database_config_spec.rb @@ -34,9 +34,8 @@ RSpec.describe Gitlab::Patch::DatabaseConfig do end end - context 'when a new syntax is used' do - let(:database_yml) do - <<-EOS + let(:database_yml) do + <<-EOS production: main: adapter: postgresql @@ -68,59 +67,9 @@ RSpec.describe Gitlab::Patch::DatabaseConfig do prepared_statements: false variables: statement_timeout: 15s - EOS - end - - include_examples 'hash containing main: connection name' - - it 'configuration is not legacy one' do - configuration.database_configuration - - expect(configuration.uses_legacy_database_config).to eq(false) - end + EOS end - context 'when a legacy syntax is used' do - let(:database_yml) do - <<-EOS - production: - adapter: postgresql - encoding: unicode - database: gitlabhq_production - username: git - password: "secure password" - host: localhost - - development: - adapter: postgresql - encoding: unicode - database: gitlabhq_development - username: postgres - password: "secure password" - host: localhost - variables: - statement_timeout: 15s - - test: &test - adapter: postgresql - encoding: unicode - database: gitlabhq_test - username: postgres - password: - host: localhost - prepared_statements: false - variables: - statement_timeout: 15s - EOS - end - - include_examples 'hash containing main: connection name' - - it 'configuration is legacy' do - configuration.database_configuration - - expect(configuration.uses_legacy_database_config).to eq(true) - end - end + include_examples 'hash containing main: connection name' end end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index e5fa7538515..0a647befb50 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -183,7 +183,7 @@ RSpec.describe Gitlab::PathRegex do # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362 it 'does not allow expansion' do - expect(described_class::TOP_LEVEL_ROUTES.size).to eq(40) + expect(described_class::TOP_LEVEL_ROUTES.size).to eq(39) end end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 8211806a809..0a186b07d19 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -103,7 +103,7 @@ RSpec.describe Gitlab::Popen do it 'raises error' do expect do @klass.new.popen(%w[foobar]) - end.to raise_error + end.to raise_error(Errno::ENOENT) end end end diff --git a/spec/lib/gitlab/process_supervisor_spec.rb b/spec/lib/gitlab/process_supervisor_spec.rb index 60b127dadda..8356197805c 100644 --- a/spec/lib/gitlab/process_supervisor_spec.rb +++ b/spec/lib/gitlab/process_supervisor_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Gitlab::ProcessSupervisor do let(:health_check_interval_seconds) { 0.1 } let(:check_terminate_interval_seconds) { 1 } let(:forwarded_signals) { [] } + let(:term_signals) { [] } let(:process_ids) { [spawn_process, spawn_process] } def spawn_process @@ -19,7 +20,8 @@ RSpec.describe Gitlab::ProcessSupervisor do health_check_interval_seconds: health_check_interval_seconds, check_terminate_interval_seconds: check_terminate_interval_seconds, terminate_timeout_seconds: 1 + check_terminate_interval_seconds, - forwarded_signals: forwarded_signals + forwarded_signals: forwarded_signals, + term_signals: term_signals ) end @@ -29,6 +31,8 @@ RSpec.describe Gitlab::ProcessSupervisor do rescue Errno::ESRCH # Ignore if a process wasn't actually alive. end + + supervisor.stop end describe '#supervise' do @@ -60,7 +64,7 @@ RSpec.describe Gitlab::ProcessSupervisor do [42] # Fake starting a new process in place of the terminated one. end - # Terminate the supervised process. + # Terminate a supervised process. Process.kill('TERM', process_ids.first) await_condition(sleep_sec: health_check_interval_seconds) do @@ -71,6 +75,72 @@ RSpec.describe Gitlab::ProcessSupervisor do expect(Gitlab::ProcessManagement.process_alive?(process_ids.last)).to be(true) expect(supervisor.supervised_pids).to match_array([process_ids.last, 42]) end + + it 'deduplicates PIDs returned from callback' do + expect(Gitlab::ProcessManagement.all_alive?(process_ids)).to be(true) + pids_killed = [] + + supervisor.supervise(process_ids) do |dead_pids| + pids_killed = dead_pids + # Fake a new process having the same pid as one that was just terminated. + [process_ids.last] + end + + # Terminate a supervised process. + Process.kill('TERM', process_ids.first) + + await_condition(sleep_sec: health_check_interval_seconds) do + pids_killed == [process_ids.first] + end + + expect(supervisor.supervised_pids).to contain_exactly(process_ids.last) + end + + it 'accepts single PID returned from callback' do + expect(Gitlab::ProcessManagement.all_alive?(process_ids)).to be(true) + pids_killed = [] + + supervisor.supervise(process_ids) do |dead_pids| + pids_killed = dead_pids + 42 + end + + # Terminate a supervised process. + Process.kill('TERM', process_ids.first) + + await_condition(sleep_sec: health_check_interval_seconds) do + pids_killed == [process_ids.first] + end + + expect(supervisor.supervised_pids).to contain_exactly(42, process_ids.last) + end + + context 'but supervisor has entered shutdown' do + it 'does not trigger callback again' do + expect(Gitlab::ProcessManagement.all_alive?(process_ids)).to be(true) + callback_count = 0 + + supervisor.supervise(process_ids) do |dead_pids| + callback_count += 1 + + Thread.new { supervisor.shutdown } + + [42] + end + + # Terminate the supervised processes to trigger more than 1 callback. + Process.kill('TERM', process_ids.first) + Process.kill('TERM', process_ids.last) + + await_condition(sleep_sec: health_check_interval_seconds * 3) do + supervisor.alive == false + end + + # Since we shut down the supervisor during the first callback, it should not + # be called anymore. + expect(callback_count).to eq(1) + end + end end context 'signal handling' do @@ -82,6 +152,8 @@ RSpec.describe Gitlab::ProcessSupervisor do end context 'termination signals' do + let(:term_signals) { %i(INT TERM) } + context 'when TERM results in timely shutdown of processes' do it 'forwards them to observed processes without waiting for grace period to expire' do allow(Gitlab::ProcessManagement).to receive(:any_alive?).and_return(false) diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb index 76bb2b4c4cc..27da1f23556 100644 --- a/spec/lib/gitlab/query_limiting/transaction_spec.rb +++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb @@ -78,6 +78,21 @@ RSpec.describe Gitlab::QueryLimiting::Transaction do expect { transaction.increment }.not_to change { transaction.count } end + + it 'does not increment the number of executed queries when the query is known to be ignorable' do + transaction = described_class.new + + expect do + transaction.increment(described_class::GEO_NODES_LOAD) + transaction.increment(described_class::LICENSES_LOAD) + transaction.increment('SELECT a.attname, a.other_column FROM pg_attribute a') + transaction.increment('SELECT x.foo, a.attname FROM some_table x JOIN pg_attribute a') + transaction.increment(<<-SQL) + SELECT a.attname, a.other_column + FROM pg_attribute a + SQL + end.not_to change(transaction, :count) + end end describe '#raise_error?' do diff --git a/spec/lib/gitlab/request_profiler/profile_spec.rb b/spec/lib/gitlab/request_profiler/profile_spec.rb deleted file mode 100644 index 30e23a99b22..00000000000 --- a/spec/lib/gitlab/request_profiler/profile_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::RequestProfiler::Profile do - let(:profile) { described_class.new(filename) } - - describe '.new' do - context 'using old filename' do - let(:filename) { '|api|v4|version.txt_1562854738.html' } - - it 'returns valid data' do - expect(profile).to be_valid - expect(profile.request_path).to eq('/api/v4/version.txt') - expect(profile.time).to eq(Time.at(1562854738).utc) - expect(profile.type).to eq('html') - end - end - - context 'using new filename' do - let(:filename) { '|api|v4|version.txt_1563547949_execution.html' } - - it 'returns valid data' do - expect(profile).to be_valid - expect(profile.request_path).to eq('/api/v4/version.txt') - expect(profile.profile_mode).to eq('execution') - expect(profile.time).to eq(Time.at(1563547949).utc) - expect(profile.type).to eq('html') - end - end - end - - describe '#content_type' do - context 'when using html file' do - let(:filename) { '|api|v4|version.txt_1562854738_memory.html' } - - it 'returns valid data' do - expect(profile).to be_valid - expect(profile.content_type).to eq('text/html') - end - end - - context 'when using text file' do - let(:filename) { '|api|v4|version.txt_1562854738_memory.txt' } - - it 'returns valid data' do - expect(profile).to be_valid - expect(profile.content_type).to eq('text/plain') - end - end - - context 'when file is unknown' do - let(:filename) { '|api|v4|version.txt_1562854738_memory.xxx' } - - it 'returns valid data' do - expect(profile).not_to be_valid - expect(profile.content_type).to be_nil - end - end - end -end diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb deleted file mode 100644 index 4d3b361efcb..00000000000 --- a/spec/lib/gitlab/request_profiler_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::RequestProfiler do - describe '.profile_token' do - it 'returns a token' do - expect(described_class.profile_token).to be_present - end - - it 'caches the token' do - expect(Rails.cache).to receive(:fetch).with('profile-token') - - described_class.profile_token - end - end - - context 'with temporary PROFILES_DIR' do - let(:tmpdir) { Dir.mktmpdir('profiler-test') } - let(:profile_name) { '|api|v4|version.txt_1562854738_memory.html' } - let(:profile_path) { File.join(tmpdir, profile_name) } - - before do - stub_const('Gitlab::RequestProfiler::PROFILES_DIR', tmpdir) - FileUtils.touch(profile_path) - end - - after do - FileUtils.rm_rf(tmpdir) - end - - describe '.remove_all_profiles' do - it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do - described_class.remove_all_profiles - - expect(Dir.exist?(tmpdir)).to be false - end - end - - describe '.all' do - subject { described_class.all } - - it 'returns all profiles' do - expect(subject.map(&:name)).to contain_exactly(profile_name) - end - end - - describe '.find' do - subject { described_class.find(profile_name) } - - it 'returns all profiles' do - expect(subject.name).to eq(profile_name) - end - end - end -end diff --git a/spec/lib/gitlab/saas_spec.rb b/spec/lib/gitlab/saas_spec.rb index 1be36a60a97..a8656c44831 100644 --- a/spec/lib/gitlab/saas_spec.rb +++ b/spec/lib/gitlab/saas_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe Gitlab::Saas do + include SaasTestHelper + describe '.canary_toggle_com_url' do subject { described_class.canary_toggle_com_url } - let(:next_url) { 'https://next.gitlab.com' } - - it { is_expected.to eq(next_url) } + it { is_expected.to eq(get_next_url) } end end diff --git a/spec/lib/gitlab/safe_request_purger_spec.rb b/spec/lib/gitlab/safe_request_purger_spec.rb new file mode 100644 index 00000000000..02f3f11d469 --- /dev/null +++ b/spec/lib/gitlab/safe_request_purger_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SafeRequestPurger do + let(:resource_key) { '_key_' } + let(:resource_ids) { ['foo'] } + let(:args) { { resource_key: resource_key, resource_ids: resource_ids } } + let(:resource_data) { { 'foo' => 'bar' } } + + before do + Gitlab::SafeRequestStore[resource_key] = resource_data + end + + describe '.execute', :request_store do + subject(:execute_instance) { described_class.execute(**args) } + + it 'purges an entry from the store' do + execute_instance + + expect(Gitlab::SafeRequestStore.fetch(resource_key)).to be_empty + end + end + + describe '#execute' do + subject(:execute_instance) { described_class.new(**args).execute } + + context 'when request store is active', :request_store do + it 'purges an entry from the store' do + execute_instance + + expect(Gitlab::SafeRequestStore.fetch(resource_key)).to be_empty + end + + context 'when there are multiple resource_ids to purge' do + let(:resource_data) do + { + 'foo' => 'bar', + 'two' => '_two_', + 'three' => '_three_', + 'four' => '_four_' + } + end + + let(:resource_ids) { %w[two three] } + + it 'purges an entry from the store' do + execute_instance + + expect(Gitlab::SafeRequestStore.fetch(resource_key)).to eq resource_data.slice('foo', 'four') + end + end + + context 'when there is no matching resource_ids' do + let(:resource_ids) { ['_bogus_resource_id_'] } + + it 'purges an entry from the store' do + execute_instance + + expect(Gitlab::SafeRequestStore.fetch(resource_key)).to eq resource_data + end + end + end + + context 'when request store is not active' do + let(:resource_ids) { ['_bogus_resource_id_'] } + + it 'does offer the ability to interact with data store' do + expect(execute_instance).to eq({}) + end + end + end +end diff --git a/spec/lib/gitlab/setup_helper/praefect_spec.rb b/spec/lib/gitlab/setup_helper/praefect_spec.rb new file mode 100644 index 00000000000..f7da6c19d68 --- /dev/null +++ b/spec/lib/gitlab/setup_helper/praefect_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SetupHelper::Praefect do + describe '.configuration_toml' do + let(:opt_per_repo) do + { per_repository: true, + pghost: 'my-host', + pgport: 555432, + pguser: 'me' } + end + + it 'defaults to in memory queue' do + toml = described_class.configuration_toml('/here', nil, {}) + + expect(toml).to match(/i_understand_my_election_strategy_is_unsupported_and_will_be_removed_without_warning/) + expect(toml).to match(/memory_queue_enabled = true/) + expect(toml).to match(/election_strategy = "local"/) + expect(toml).not_to match(/\[database\]/) + end + + it 'provides database details if wanted' do + toml = described_class.configuration_toml('/here', nil, opt_per_repo) + + expect(toml).not_to match(/i_understand_my_election_strategy_is_unsupported_and_will_be_removed_without_warning/) + expect(toml).not_to match(/memory_queue_enabled = true/) + expect(toml).to match(/\[database\]/) + expect(toml).to match(/election_strategy = "per_repository"/) + end + + %i[pghost pgport pguser].each do |pg_key| + it "fails when #{pg_key} is missing" do + opt = opt_per_repo.dup + opt.delete(pg_key) + + expect do + described_class.configuration_toml('/here', nil, opt) + end.to raise_error(KeyError) + end + + it "uses the provided #{pg_key}" do + toml = described_class.configuration_toml('/here', nil, opt_per_repo) + + expect(toml).to match(/#{pg_key.to_s.delete_prefix('pg')} = "?#{opt_per_repo[pg_key]}"?/) + end + end + + it 'defaults to praefect_test if dbname is missing' do + toml = described_class.configuration_toml('/here', nil, opt_per_repo) + + expect(toml).to match(/dbname = "praefect_test"/) + end + + it 'uses the provided dbname' do + opt = opt_per_repo.merge(dbname: 'my_db') + + toml = described_class.configuration_toml('/here', nil, opt) + + expect(toml).to match(/dbname = "my_db"/) + end + end + + describe '.get_config_path' do + it 'defaults to praefect.config.toml' do + expect(described_class).to receive(:generate_configuration).with(anything, '/tmp/praefect.config.toml', anything) + + described_class.create_configuration('/tmp', {}) + end + + it 'takes the provided config_filename' do + opt = { config_filename: 'yo.toml' } + + expect(described_class).to receive(:generate_configuration).with(anything, '/tmp/yo.toml', anything) + + described_class.create_configuration('/tmp', {}, options: opt) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index da135f202f6..4a1a9beb21a 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqConfig do + before do + # Remove cache + described_class.instance_variable_set(:@workers, nil) + end + describe '.workers' do it 'includes all workers' do worker_classes = described_class.workers.map(&:klass) @@ -44,9 +49,10 @@ RSpec.describe Gitlab::SidekiqConfig do before do allow(described_class).to receive(:workers).and_return(workers) allow(Gitlab).to receive(:ee?).and_return(false) + allow(Gitlab).to receive(:jh?).and_return(false) end - it 'returns true if the YAML file does not matcph the application code' do + it 'returns true if the YAML file does not match the application code' do allow(YAML).to receive(:load_file) .with(described_class::FOSS_QUEUE_CONFIG_PATH) .and_return(workers.first(2).map(&:to_yaml)) @@ -96,6 +102,7 @@ RSpec.describe Gitlab::SidekiqConfig do ].map { |worker| described_class::Worker.new(worker, ee: false) } allow(described_class).to receive(:workers).and_return(workers) + allow(Gitlab).to receive(:jh?).and_return(false) end let(:expected_queues) do @@ -161,4 +168,35 @@ RSpec.describe Gitlab::SidekiqConfig do expect(mappings).not_to include('AdminEmailWorker' => 'cronjob:admin_email') end end + + describe '.routing_queues' do + let(:test_routes) do + [ + ['tags=needs_own_queue', nil], + ['urgency=high', 'high_urgency'], + ['feature_category=gitaly', 'gitaly'], + ['feature_category=not_exist', 'not_exist'], + ['*', 'default'] + ] + end + + before do + described_class.instance_variable_set(:@routing_queues, nil) + allow(::Gitlab::SidekiqConfig::WorkerRouter) + .to receive(:global).and_return(::Gitlab::SidekiqConfig::WorkerRouter.new(test_routes)) + end + + after do + described_class.instance_variable_set(:@routing_queues, nil) + end + + it 'returns worker queue mappings that have queues in the current Sidekiq options' do + queues = described_class.routing_queues + + expect(queues).to match_array(%w[ + default mailers high_urgency gitaly email_receiver service_desk_email_receiver + ]) + expect(queues).not_to include('not_exist') + end + end end diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb index 96fef88de4e..e3f9f8277a0 100644 --- a/spec/lib/gitlab/sidekiq_death_handler_spec.rb +++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb @@ -23,9 +23,9 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do it 'uses the attributes from the worker' do expect(described_class.counter) .to receive(:increment) - .with(queue: 'test_queue', worker: 'TestWorker', + .with({ queue: 'test_queue', worker: 'TestWorker', urgency: 'low', external_dependencies: 'yes', - feature_category: 'users', boundary: 'cpu') + feature_category: 'users', boundary: 'cpu' }) described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) end @@ -39,9 +39,9 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do it 'uses blank attributes' do expect(described_class.counter) .to receive(:increment) - .with(queue: 'test_queue', worker: 'TestWorker', + .with({ queue: 'test_queue', worker: 'TestWorker', urgency: '', external_dependencies: 'no', - feature_category: '', boundary: '') + feature_category: '', boundary: '' }) described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 210b9162be0..00ae55237e9 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -287,7 +287,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do 'job_status' => 'done', 'duration_s' => 0.0, 'completed_at' => timestamp.to_f, - 'cpu_s' => 1.111112 + 'cpu_s' => 1.111112, + 'rate_limiting_gates' => [] ) end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index ffa92126cc9..7d31979a393 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -21,40 +21,40 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default') expect(completion_seconds_metric) - .to receive(:get).with(queue: 'merge', + .to receive(:get).with({ queue: 'merge', worker: 'MergeWorker', urgency: 'high', external_dependencies: 'no', feature_category: 'source_code_management', boundary: '', - job_status: 'done') + job_status: 'done' }) expect(completion_seconds_metric) - .to receive(:get).with(queue: 'merge', + .to receive(:get).with({ queue: 'merge', worker: 'MergeWorker', urgency: 'high', external_dependencies: 'no', feature_category: 'source_code_management', boundary: '', - job_status: 'fail') + job_status: 'fail' }) expect(completion_seconds_metric) - .to receive(:get).with(queue: 'default', + .to receive(:get).with({ queue: 'default', worker: 'Ci::BuildFinishedWorker', urgency: 'high', external_dependencies: 'no', feature_category: 'continuous_integration', boundary: 'cpu', - job_status: 'done') + job_status: 'done' }) expect(completion_seconds_metric) - .to receive(:get).with(queue: 'default', + .to receive(:get).with({ queue: 'default', worker: 'Ci::BuildFinishedWorker', urgency: 'high', external_dependencies: 'no', feature_category: 'continuous_integration', boundary: 'cpu', - job_status: 'fail') + job_status: 'fail' }) described_class.initialize_process_metrics end diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb index fd3654afee0..8d5a39baf77 100644 --- a/spec/lib/gitlab/subscription_portal_spec.rb +++ b/spec/lib/gitlab/subscription_portal_spec.rb @@ -56,6 +56,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do where(:method_name, :result) do :default_subscriptions_url | 'https://customers.staging.gitlab.com' :payment_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_validation' + :payment_validation_form_id | 'payment_method_validation' :registration_validation_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_registration_validation' :subscriptions_graphql_url | 'https://customers.staging.gitlab.com/graphql' :subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes' diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index 226fdb9c948..26c83ed6793 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -21,55 +21,6 @@ RSpec.describe Gitlab::Template::GitlabCiYmlTemplate do end end - describe '.find' do - let_it_be(:project) { create(:project) } - let_it_be(:other_project) { create(:project) } - - described_class::TEMPLATES_WITH_LATEST_VERSION.keys.each do |key| - it "finds the latest template for #{key}" do - result = described_class.find(key, project) - expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml") - expect(result.content).to be_present - end - - context 'when `redirect_to_latest_template` feature flag is disabled' do - before do - stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => false) - end - - it "finds the stable template for #{key}" do - result = described_class.find(key, project) - expect(result.full_name).to eq("#{key}.gitlab-ci.yml") - expect(result.content).to be_present - end - end - - context 'when `redirect_to_latest_template` feature flag is enabled on the project' do - before do - stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => project) - end - - it "finds the latest template for #{key}" do - result = described_class.find(key, project) - expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml") - expect(result.content).to be_present - end - end - - context 'when `redirect_to_latest_template` feature flag is enabled on the other project' do - before do - stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => other_project) - end - - it "finds the stable template for #{key}" do - result = described_class.find(key, project) - expect(result.full_name).to eq("#{key}.gitlab-ci.yml") - expect(result.content).to be_present - end - end - end - end - describe '#content' do it 'loads the full file' do gitignore = subject.new(Rails.root.join('lib/gitlab/ci/templates/Ruby.gitlab-ci.yml')) diff --git a/spec/lib/gitlab/tracking/event_definition_spec.rb b/spec/lib/gitlab/tracking/event_definition_spec.rb index 51c62840819..623009e9a30 100644 --- a/spec/lib/gitlab/tracking/event_definition_spec.rb +++ b/spec/lib/gitlab/tracking/event_definition_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::Tracking::EventDefinition do end it 'has all definitions valid' do - expect { described_class.definitions }.not_to raise_error(Gitlab::Tracking::InvalidEventError) + expect { described_class.definitions }.not_to raise_error end describe '#validate' do diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 8e372ba795b..d4f96f1a37f 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -22,6 +22,8 @@ RSpec.describe Gitlab::UrlBuilder do :group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" } :commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" } :issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" } + [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.id}" } + :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.id}" } :merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" } :project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" } :project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" } @@ -57,7 +59,7 @@ RSpec.describe Gitlab::UrlBuilder do end with_them do - let(:object) { build_stubbed(factory) } + let(:object) { build_stubbed(*Array(factory)) } let(:path) { path_generator.call(object) } it 'returns the full URL' do @@ -69,6 +71,18 @@ RSpec.describe Gitlab::UrlBuilder do end end + context 'when work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'returns an issue path for an issue of type task' do + task = create(:issue, :task) + + expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/issues/#{task.iid}") + end + end + context 'when passing a compare' do # NOTE: The Compare requires an actual repository, which isn't available # with the `build_stubbed` strategy used by the table tests above diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index 1127d1cd477..070586319a5 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -20,7 +20,8 @@ RSpec.describe Gitlab::Usage::MetricDefinition do distribution: %w(ee ce), tier: %w(free starter premium ultimate bronze silver gold), name: 'uuid', - data_category: 'standard' + data_category: 'standard', + removed_by_url: 'http://gdk.test' } end @@ -132,6 +133,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do :tier | %w(test ee) :name | 'count_<adjective_describing>_boards' :repair_issue_url | nil + :removed_by_url | 1 :instrumentation_class | 'Metric_Class' :instrumentation_class | 'metricClass' @@ -177,6 +179,24 @@ RSpec.describe Gitlab::Usage::MetricDefinition do end end + describe '#valid_service_ping_status?' do + context 'when metric has active status' do + it 'has to return true' do + attributes[:status] = 'active' + + expect(described_class.new(path, attributes).valid_service_ping_status?).to be_truthy + end + end + + context 'when metric has removed status' do + it 'has to return false' do + attributes[:status] = 'removed' + + expect(described_class.new(path, attributes).valid_service_ping_status?).to be_falsey + end + end + end + describe 'statuses' do using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index 19d2d3048eb..10ae94e746b 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -51,4 +51,31 @@ RSpec.describe Gitlab::Usage::Metric do expect(described_class.new(issue_count_metric_definiton).with_suggested_name).to eq({ counts: { issues: 'count_issues' } }) end end + + context 'unavailable metric' do + let(:instrumentation_class) { "UnavailableMetric" } + let(:issue_count_metric_definiton) do + double(:issue_count_metric_definiton, + attributes.merge({ attributes: attributes, instrumentation_class: instrumentation_class }) + ) + end + + before do + unavailable_metric_class = Class.new(Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric) do + def available? + false + end + end + + stub_const("Gitlab::Usage::Metrics::Instrumentations::#{instrumentation_class}", unavailable_metric_class) + end + + [:with_value, :with_instrumentation, :with_suggested_name].each do |method_name| + describe "##{method_name}" do + it 'returns an empty hash' do + expect(described_class.new(issue_count_metric_definiton).public_send(method_name)).to eq({}) + end + end + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb index 1b2170baf17..92d4de3c462 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CollectedDataCategories let(:expected_value) { %w[standard subscription operational optional] } before do - allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance| + allow_next_instance_of(ServicePing::PermitDataCategories) do |instance| expect(instance).to receive(:execute).and_return(expected_value) end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb new file mode 100644 index 00000000000..b85d5a3ebf9 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitiesMetric do + let_it_be(:user) { create(:user) } + let_it_be(:bulk_import_projects) do + create_list(:bulk_import_entity, 3, source_type: 'project_entity', created_at: 3.weeks.ago) + end + + let_it_be(:bulk_import_groups) do + create_list(:bulk_import_entity, 3, source_type: 'group_entity', created_at: 3.weeks.ago) + end + + let_it_be(:old_bulk_import_project) do + create(:bulk_import_entity, source_type: 'project_entity', created_at: 2.months.ago) + end + + context 'with no source_type' do + context 'with all time frame' do + let(:expected_value) { 7 } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\"" + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: 'all', options: {} + end + + context 'for 28d time frame' do + let(:expected_value) { 6 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'" + end + + it_behaves_like 'a correct instrumented metric value and query', time_frame: '28d', options: {} + end + end + + context 'with invalid source_type' do + it 'raises ArgumentError' do + expect { described_class.new(time_frame: 'all', options: { source_type: 'random' }) } + .to raise_error(ArgumentError, /source_type/) + end + end + + context 'with source_type project_entity' do + context 'with all time frame' do + let(:expected_value) { 4 } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"source_type\" = 1" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { source_type: 'project_entity' } + end + + context 'for 28d time frame' do + let(:expected_value) { 3 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\ + " AND \"bulk_import_entities\".\"source_type\" = 1" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { source_type: 'project_entity' } + end + end + + context 'with source_type group_entity' do + context 'with all time frame' do + let(:expected_value) { 3 } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"source_type\" = 0" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { source_type: 'group_entity' } + end + + context 'for 28d time frame' do + let(:expected_value) { 3 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\""\ + " WHERE \"bulk_import_entities\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"\ + " AND \"bulk_import_entities\".\"source_type\" = 0" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { source_type: 'group_entity' } + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb new file mode 100644 index 00000000000..4c86410d609 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountImportedProjectsMetric do + let_it_be(:user) { create(:user) } + let_it_be(:gitea_imports) do + create_list(:project, 3, import_type: 'gitea', creator_id: user.id, created_at: 3.weeks.ago) + end + + let_it_be(:bitbucket_imports) do + create_list(:project, 2, import_type: 'bitbucket', creator_id: user.id, created_at: 3.weeks.ago) + end + + let_it_be(:old_import) { create(:project, import_type: 'gitea', creator_id: user.id, created_at: 2.months.ago) } + + context 'with import_type gitea' do + context 'with all time frame' do + let(:expected_value) { 4 } + let(:expected_query) do + "SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"import_type\" = 'gitea'" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { import_type: 'gitea' } + end + + context 'for 28d time frame' do + let(:expected_value) { 3 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"created_at\""\ + " BETWEEN '#{start}' AND '#{finish}' AND \"projects\".\"import_type\" = 'gitea'" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { import_type: 'gitea' } + end + end + + context 'with import_type bitbucket' do + context 'with all time frame' do + let(:expected_value) { 2 } + let(:expected_query) do + "SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"import_type\" = 'bitbucket'" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: 'all', + options: { import_type: 'bitbucket' } + end + + context 'for 28d time frame' do + let(:expected_value) { 2 } + let(:start) { 30.days.ago.to_s(:db) } + let(:finish) { 2.days.ago.to_s(:db) } + let(:expected_query) do + "SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"created_at\""\ + " BETWEEN '#{start}' AND '#{finish}' AND \"projects\".\"import_type\" = 'bitbucket'" + end + + it_behaves_like 'a correct instrumented metric value and query', + time_frame: '28d', + options: { import_type: 'bitbucket' } + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb index ea5ae1970de..8e7bd7b84e6 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb @@ -71,6 +71,33 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do end end + context 'with availability defined' do + subject do + described_class.tap do |metric_class| + metric_class.relation { Issue } + metric_class.operation :count + metric_class.available? { false } + end.new(time_frame: 'all') + end + + it 'responds to #available? properly' do + expect(subject.available?).to eq(false) + end + end + + context 'with availability not defined' do + subject do + Class.new(described_class) do + relation { Issue } + operation :count + end.new(time_frame: 'all') + end + + it 'responds to #available? properly' do + expect(subject.available?).to eq(true) + end + end + context 'with cache_start_and_finish_as called' do subject do described_class.tap do |metric_class| @@ -134,4 +161,17 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do end end end + + context 'with unimplemented operation method used' do + subject do + described_class.tap do |metric_class| + metric_class.relation { Issue } + metric_class.operation :invalid_operation + end.new(time_frame: 'all') + end + + it 'raises an error' do + expect { subject }.to raise_error(described_class::UnimplementedOperationError) + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb index 347a2c779cb..97306051533 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb @@ -25,4 +25,28 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_ it 'raise exception if events options is not present' do expect { described_class.new(time_frame: '28d') }.to raise_error(ArgumentError) end + + describe 'children classes' do + let(:options) { { events: ['i_quickactions_approve'] } } + + context 'availability not defined' do + subject { Class.new(described_class).new(time_frame: nil, options: options) } + + it 'returns default availability' do + expect(subject.available?).to eq(true) + end + end + + context 'availability defined' do + subject do + Class.new(described_class) do + available? { false } + end.new(time_frame: nil, options: options) + end + + it 'returns defined availability' do + expect(subject.available?).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb index fb3bd1ba834..831f775ec9a 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb @@ -20,4 +20,28 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git it 'raises an exception if counter_class option is not present' do expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError) end + + describe 'children classes' do + let(:options) { { event: 'pushes', counter_class: 'SourceCodeCounter' } } + + context 'availability not defined' do + subject { Class.new(described_class).new(time_frame: nil, options: options) } + + it 'returns default availability' do + expect(subject.available?).to eq(true) + end + end + + context 'availability defined' do + subject do + Class.new(described_class) do + available? { false } + end.new(time_frame: nil, options: options) + end + + it 'returns defined availability' do + expect(subject.available?).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/usage/metrics/query_spec.rb b/spec/lib/gitlab/usage/metrics/query_spec.rb index 60c8d044a64..65b8a7a046b 100644 --- a/spec/lib/gitlab/usage/metrics/query_spec.rb +++ b/spec/lib/gitlab/usage/metrics/query_spec.rb @@ -11,6 +11,22 @@ RSpec.describe Gitlab::Usage::Metrics::Query do it 'does not mix a nil column with keyword arguments' do expect(described_class.for(:count, User, nil)).to eq('SELECT COUNT("users"."id") FROM "users"') end + + it 'removes order from passed relation' do + expect(described_class.for(:count, User.order(:email), nil)).to eq('SELECT COUNT("users"."id") FROM "users"') + end + + it 'returns valid raw SQL for join relations' do + expect(described_class.for(:count, User.joins(:issues), :email)).to eq( + 'SELECT COUNT("users"."email") FROM "users" INNER JOIN "issues" ON "issues"."author_id" = "users"."id"' + ) + end + + it 'returns valid raw SQL for join relations with joined columns' do + expect(described_class.for(:count, User.joins(:issues), 'issue.weight')).to eq( + 'SELECT COUNT("issue"."weight") FROM "users" INNER JOIN "issues" ON "issues"."author_id" = "users"."id"' + ) + end end describe '.distinct_count' do @@ -21,6 +37,22 @@ RSpec.describe Gitlab::Usage::Metrics::Query do it 'does not mix a nil column with keyword arguments' do expect(described_class.for(:distinct_count, Issue, nil)).to eq('SELECT COUNT(DISTINCT "issues"."id") FROM "issues"') end + + it 'removes order from passed relation' do + expect(described_class.for(:distinct_count, User.order(:email), nil)).to eq('SELECT COUNT(DISTINCT "users"."id") FROM "users"') + end + + it 'returns valid raw SQL for join relations' do + expect(described_class.for(:distinct_count, User.joins(:issues), :email)).to eq( + 'SELECT COUNT(DISTINCT "users"."email") FROM "users" INNER JOIN "issues" ON "issues"."author_id" = "users"."id"' + ) + end + + it 'returns valid raw SQL for join relations with joined columns' do + expect(described_class.for(:distinct_count, User.joins(:issues), 'issue.weight')).to eq( + 'SELECT COUNT(DISTINCT "issue"."weight") FROM "users" INNER JOIN "issues" ON "issues"."author_id" = "users"."id"' + ) + end end describe '.sum' do diff --git a/spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb b/spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb new file mode 100644 index 00000000000..46592379b3d --- /dev/null +++ b/spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator do + using RSpec::Parameterized::TableSyntax + + let(:duration) { 123 } + + where(:metric_value, :metric_class) do + 1 | Integer + "value" | String + true | TrueClass + false | FalseClass + nil | NilClass + end + + with_them do + let(:decorated_object) { described_class.new(metric_value, duration) } + + it 'exposes a duration with the correct value' do + expect(decorated_object.duration).to eq(duration) + end + + it 'imitates wrapped class', :aggregate_failures do + expect(decorated_object).to eq metric_value + expect(decorated_object.class).to eq metric_class + expect(decorated_object.is_a?(metric_class)).to be_truthy + # rubocop:disable Style/ClassCheck + expect(decorated_object.kind_of?(metric_class)).to be_truthy + # rubocop:enable Style/ClassCheck + expect({ metric: decorated_object }.to_json).to eql({ metric: metric_value }.to_json) + end + end +end diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb index b6119ab52ec..e7096988035 100644 --- a/spec/lib/gitlab/usage/service_ping_report_spec.rb +++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb @@ -92,49 +92,6 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c end context 'cross test values against queries' do - # TODO: fix failing metrics https://gitlab.com/gitlab-org/gitlab/-/issues/353559 - let(:failing_todo_metrics) do - ["counts.labels", - "counts.jira_imports_total_imported_issues_count", - "counts.in_product_marketing_email_create_0_sent", - "counts.in_product_marketing_email_create_0_cta_clicked", - "counts.in_product_marketing_email_create_1_sent", - "counts.in_product_marketing_email_create_1_cta_clicked", - "counts.in_product_marketing_email_create_2_sent", - "counts.in_product_marketing_email_create_2_cta_clicked", - "counts.in_product_marketing_email_verify_0_sent", - "counts.in_product_marketing_email_verify_0_cta_clicked", - "counts.in_product_marketing_email_verify_1_sent", - "counts.in_product_marketing_email_verify_1_cta_clicked", - "counts.in_product_marketing_email_verify_2_sent", - "counts.in_product_marketing_email_verify_2_cta_clicked", - "counts.in_product_marketing_email_trial_0_sent", - "counts.in_product_marketing_email_trial_0_cta_clicked", - "counts.in_product_marketing_email_trial_1_sent", - "counts.in_product_marketing_email_trial_1_cta_clicked", - "counts.in_product_marketing_email_trial_2_sent", - "counts.in_product_marketing_email_trial_2_cta_clicked", - "counts.in_product_marketing_email_team_0_sent", - "counts.in_product_marketing_email_team_0_cta_clicked", - "counts.in_product_marketing_email_team_1_sent", - "counts.in_product_marketing_email_team_1_cta_clicked", - "counts.in_product_marketing_email_team_2_sent", - "counts.in_product_marketing_email_team_2_cta_clicked", - "counts.in_product_marketing_email_experience_0_sent", - "counts.in_product_marketing_email_team_short_0_sent", - "counts.in_product_marketing_email_team_short_0_cta_clicked", - "counts.in_product_marketing_email_trial_short_0_sent", - "counts.in_product_marketing_email_trial_short_0_cta_clicked", - "counts.in_product_marketing_email_admin_verify_0_sent", - "counts.in_product_marketing_email_admin_verify_0_cta_clicked", - "counts.ldap_users", - "usage_activity_by_stage.create.projects_with_sectional_code_owner_rules", - "usage_activity_by_stage.monitor.clusters_integrations_prometheus", - "usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram", - "usage_activity_by_stage_monthly.create.projects_with_sectional_code_owner_rules", - "usage_activity_by_stage_monthly.monitor.clusters_integrations_prometheus"] - end - def fetch_value_by_query(query) # Because test cases are run inside a transaction, if any query raise and error all queries that follows # it are automatically canceled by PostgreSQL, to avoid that problem, and to provide exhaustive information @@ -157,6 +114,24 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c accumulator end + def type_cast_to_defined_type(value, metric_definition) + case metric_definition&.attributes&.fetch(:value_type) + when "string" + value.to_s + when "number" + value.to_i + when "object" + case metric_definition&.json_schema&.fetch("type") + when "array" + value.to_a + else + value.to_h + end + else + value + end + end + before do stub_usage_data_connections stub_object_store_settings @@ -169,12 +144,13 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c let(:service_ping_payload) { described_class.for(output: :all_metrics_values) } let(:metrics_queries_with_values) { build_payload_from_queries(described_class.for(output: :metrics_queries)) } + let(:metric_definitions) { ::Gitlab::Usage::MetricDefinition.definitions } it 'generates queries that match collected data', :aggregate_failures do message = "Expected %{query} result to match %{value} for %{key_path} metric" metrics_queries_with_values.each do |key_path, query, value| - next if failing_todo_metrics.include?(key_path.join('.')) + value = type_cast_to_defined_type(value, metric_definitions[key_path.join('.')]) expect(value).to( eq(service_ping_payload.dig(*key_path)), diff --git a/spec/lib/gitlab/usage_counters/pod_logs_spec.rb b/spec/lib/gitlab/usage_counters/pod_logs_spec.rb deleted file mode 100644 index 1059c519b19..00000000000 --- a/spec/lib/gitlab/usage_counters/pod_logs_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::UsageCounters::PodLogs, :clean_gitlab_redis_shared_state do - it_behaves_like 'a usage counter' -end diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb index 5f66387c82b..9aecb8f8b25 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 @@ -80,10 +80,13 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red it 'can return the count of actions per user deduplicated' do described_class.track_web_ide_edit_action(author: user1) + described_class.track_live_preview_edit_action(author: user1) described_class.track_snippet_editor_edit_action(author: user1) described_class.track_sfe_edit_action(author: user1) described_class.track_web_ide_edit_action(author: user2, time: time - 2.days) described_class.track_web_ide_edit_action(author: user3, time: time - 3.days) + described_class.track_live_preview_edit_action(author: user2, time: time - 2.days) + described_class.track_live_preview_edit_action(author: user3, time: time - 3.days) described_class.track_snippet_editor_edit_action(author: user3, time: time - 3.days) described_class.track_sfe_edit_action(author: user3, time: time - 3.days) diff --git a/spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb new file mode 100644 index 00000000000..60c4424d2ae --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/ipynb_diff_activity_counter_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::IpynbDiffActivityCounter, :clean_gitlab_redis_shared_state do + let(:user) { build(:user, id: 1) } + let(:for_mr) { false } + let(:for_commit) { false } + let(:first_note) { build(:note, author: user, id: 1) } + let(:second_note) { build(:note, author: user, id: 2) } + + before do + allow(first_note).to receive(:for_merge_request?).and_return(for_mr) + allow(second_note).to receive(:for_merge_request?).and_return(for_mr) + allow(first_note).to receive(:for_commit?).and_return(for_commit) + allow(second_note).to receive(:for_commit?).and_return(for_commit) + end + + subject do + described_class.note_created(first_note) + described_class.note_created(first_note) + described_class.note_created(second_note) + end + + shared_examples_for 'an action that tracks events' do + specify do + expect { 2.times { subject } } + .to change { event_count(action) }.by(2) + .and change { event_count(per_user_action) }.by(1) + end + end + + shared_examples_for 'an action that does not track events' do + specify do + expect { 2.times { subject } } + .to change { event_count(action) }.by(0) + .and change { event_count(per_user_action) }.by(0) + end + end + + describe '#track_note_created_in_ipynb_diff' do + context 'note is for commit' do + let(:for_commit) { true } + + it_behaves_like 'an action that tracks events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_ACTION} + end + + it_behaves_like 'an action that tracks events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_COMMIT_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_COMMIT_ACTION} + end + + it_behaves_like 'an action that does not track events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_MR_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_MR_ACTION} + end + end + + context 'note is for MR' do + let(:for_mr) { true } + + it_behaves_like 'an action that tracks events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_MR_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_MR_ACTION} + end + + it_behaves_like 'an action that tracks events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_ACTION} + end + + it_behaves_like 'an action that does not track events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_COMMIT_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_COMMIT_ACTION} + end + end + + context 'note is for neither MR nor Commit' do + it_behaves_like 'an action that does not track events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_ACTION} + end + + it_behaves_like 'an action that does not track events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_MR_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_MR_ACTION} + end + + it_behaves_like 'an action that does not track events' do + let(:action) {described_class::NOTE_CREATED_IN_IPYNB_DIFF_COMMIT_ACTION} + let(:per_user_action) {described_class::USER_CREATED_NOTE_IN_IPYNB_DIFF_COMMIT_ACTION} + end + end + end + + private + + def event_count(event_name) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: event_name, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + end +end diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb index 88322e1b971..7c64a31c499 100644 --- a/spec/lib/gitlab/usage_data_queries_spec.rb +++ b/spec/lib/gitlab/usage_data_queries_spec.rb @@ -11,6 +11,12 @@ RSpec.describe Gitlab::UsageDataQueries do end end + describe '.with_duration' do + it 'yields passed block' do + expect { |block| described_class.with_duration(&block) }.to yield_with_no_args + end + end + describe '.count' do it 'returns the raw SQL' do expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"') diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 8a919a0a72e..7edec6d13f4 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1080,7 +1080,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do it 'reports collected data categories' do expected_value = %w[standard subscription operational optional] - allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance| + allow_next_instance_of(ServicePing::PermitDataCategories) do |instance| expect(instance).to receive(:execute).and_return(expected_value) end @@ -1470,4 +1470,31 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end end + + describe ".with_duration" do + context 'with feature flag measure_service_ping_metric_collection turned off' do + before do + stub_feature_flags(measure_service_ping_metric_collection: false) + end + + it 'does NOT record duration and return block response' do + expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator).not_to receive(:new) + + expect(described_class.with_duration { 1 + 1 }).to be 2 + end + end + + context 'with feature flag measure_service_ping_metric_collection turned off' do + before do + stub_feature_flags(measure_service_ping_metric_collection: true) + end + + it 'records duration' do + expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator) + .to receive(:new).with(2, kind_of(Float)) + + described_class.with_duration { 1 + 1 } + end + end + end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 01890305df4..b1de3e21b77 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -30,17 +30,6 @@ RSpec.describe Gitlab::UserAccess do end end - describe 'push to branch in an internal project' do - it 'will not infinitely loop when a project is internal' do - project.visibility_level = Gitlab::VisibilityLevel::INTERNAL - project.save! - - expect(project).not_to receive(:branch_allows_collaboration?) - - access.can_push_to_branch?('master') - end - end - describe 'push to empty project' do let(:empty_project) { create(:project_empty_repo) } let(:project_access) { described_class.new(user, container: empty_project) } diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index b44c6565538..a74a9f06c6f 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -31,6 +31,12 @@ RSpec.describe Gitlab::Utils::UsageData do end end + describe '.with_duration' do + it 'yields passed block' do + expect { |block| described_class.with_duration(&block) }.to yield_with_no_args + end + end + describe '#add_metric' do let(:metric) { 'UuidMetric'} @@ -48,6 +54,13 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.count(relation, batch: false)).to eq(1) end + it 'records duration' do + expect(described_class).to receive(:with_duration) + allow(relation).to receive(:count).and_return(1) + + described_class.count(relation, batch: false) + end + context 'when counting fails' do subject { described_class.count(relation, batch: false) } @@ -68,6 +81,13 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.distinct_count(relation, batch: false)).to eq(1) end + it 'records duration' do + expect(described_class).to receive(:with_duration) + allow(relation).to receive(:distinct_count_by).and_return(1) + + described_class.distinct_count(relation, batch: false) + end + context 'when counting fails' do subject { described_class.distinct_count(relation, batch: false) } @@ -206,14 +226,6 @@ RSpec.describe Gitlab::Utils::UsageData do it_behaves_like 'failing hardening method' end - - it 'logs error and returns DISTRIBUTED_HLL_FALLBACK value when counting raises any error', :aggregate_failures do - error = StandardError.new('') - allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(error) - - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) - expect(described_class.estimate_batch_distinct_count(relation)).to eq(4) - end end end @@ -229,6 +241,13 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.sum(relation, :column, batch_size: 100, start: 2, finish: 3)).to eq(1) end + it 'records duration' do + expect(described_class).to receive(:with_duration) + allow(Gitlab::Database::BatchCount).to receive(:batch_sum).and_return(1) + + described_class.sum(relation, :column) + end + context 'when counting fails' do subject { described_class.sum(relation, :column) } @@ -316,6 +335,12 @@ RSpec.describe Gitlab::Utils::UsageData do expect(histogram).to eq('2' => 1) end + it 'records duration' do + expect(described_class).to receive(:with_duration) + + described_class.histogram(relation, column, buckets: 1..100) + end + context 'when query timeout' do subject do with_statement_timeout(0.001) do @@ -368,6 +393,12 @@ RSpec.describe Gitlab::Utils::UsageData do expect(described_class.add).to eq(0) end + it 'records duration' do + expect(described_class).to receive(:with_duration) + + described_class.add + end + context 'when adding fails' do subject { described_class.add(nil, 3) } @@ -392,6 +423,12 @@ RSpec.describe Gitlab::Utils::UsageData do it_behaves_like 'failing hardening method', StandardError end + it 'records duration' do + expect(described_class).to receive(:with_duration) + + described_class.alt_usage_data + end + it 'returns the evaluated block when give' do expect(described_class.alt_usage_data { Gitlab::CurrentSettings.uuid } ).to eq(Gitlab::CurrentSettings.uuid) end @@ -402,6 +439,12 @@ RSpec.describe Gitlab::Utils::UsageData do end describe '#redis_usage_data' do + it 'records duration' do + expect(described_class).to receive(:with_duration) + + described_class.redis_usage_data + end + context 'with block given' do context 'when method fails' do subject { described_class.redis_usage_data { raise ::Redis::CommandError } } @@ -445,6 +488,12 @@ RSpec.describe Gitlab::Utils::UsageData do end describe '#with_prometheus_client' do + it 'records duration' do + expect(described_class).to receive(:with_duration) + + described_class.with_prometheus_client { |client| client } + end + it 'returns fallback with for an exception in yield block' do allow(described_class).to receive(:prometheus_client).and_return(Gitlab::PrometheusClient.new('http://localhost:9090')) result = described_class.with_prometheus_client(fallback: -42) { |client| raise StandardError } diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 6b12fb4a84a..0648d276a6b 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Utils do using RSpec::Parameterized::TableSyntax - delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, + delegate :to_boolean, :boolean_to_yes_no, :slugify, :which, :ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes, :append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class @@ -311,12 +311,6 @@ RSpec.describe Gitlab::Utils do end end - describe '.random_string' do - it 'generates a string' do - expect(random_string).to be_kind_of(String) - end - end - describe '.which' do before do stub_env('PATH', '/sbin:/usr/bin:/home/joe/bin') diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 3bab9aec454..703a4b5399e 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -244,15 +244,13 @@ RSpec.describe Gitlab::Workhorse do GitalyServer: { features: { 'gitaly-feature-enforce-requests-limits' => 'true' }, address: Gitlab::GitalyClient.address('default'), - token: Gitlab::GitalyClient.token('default'), - sidechannel: false + token: Gitlab::GitalyClient.token('default') } } end before do allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) - stub_feature_flags(workhorse_use_sidechannel: false) end it 'includes a Repository param' do @@ -334,46 +332,6 @@ RSpec.describe Gitlab::Workhorse do it { expect { subject }.to raise_exception('Unsupported action: download') } end - - context 'when workhorse_use_sidechannel flag is set' do - context 'when a feature flag is set globally' do - before do - stub_feature_flags(workhorse_use_sidechannel: true) - end - - it 'sets the flag to true' do - response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) - - expect(response.dig(:GitalyServer, :sidechannel)).to eq(true) - end - end - - context 'when a feature flag is set for a single project' do - before do - stub_feature_flags(workhorse_use_sidechannel: project) - end - - it 'sets the flag to true for that project' do - response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) - - expect(response.dig(:GitalyServer, :sidechannel)).to eq(true) - end - - it 'sets the flag to false for other projects' do - other_project = create(:project, :public, :repository) - response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action) - - expect(response.dig(:GitalyServer, :sidechannel)).to eq(false) - end - - it 'sets the flag to false when there is no project' do - snippet = create(:personal_snippet, :repository) - response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action) - - expect(response.dig(:GitalyServer, :sidechannel)).to eq(false) - end - end - end end context 'when receive_max_input_size has been updated' do @@ -448,6 +406,14 @@ RSpec.describe Gitlab::Workhorse do end end + describe '.detect_content_type' do + subject { described_class.detect_content_type } + + it 'returns array setting detect content type in workhorse' do + expect(subject).to eq(%w[Gitlab-Workhorse-Detect-Content-Type true]) + end + end + describe '.send_git_blob' do include FakeBlobHelpers diff --git a/spec/lib/gitlab/zentao/client_spec.rb b/spec/lib/gitlab/zentao/client_spec.rb index 86b310fe417..135f13e6265 100644 --- a/spec/lib/gitlab/zentao/client_spec.rb +++ b/spec/lib/gitlab/zentao/client_spec.rb @@ -130,4 +130,36 @@ RSpec.describe Gitlab::Zentao::Client do end end end + + describe '#url' do + context 'api url' do + shared_examples 'joins api_url correctly' do + it 'verify url' do + expect(integration.send(:url, "products/1").to_s) + .to eq("https://jihudemo.zentao.net/zentao/api.php/v1/products/1") + end + end + + context 'no ends slash' do + let(:zentao_integration) { create(:zentao_integration, api_url: 'https://jihudemo.zentao.net/zentao') } + + include_examples 'joins api_url correctly' + end + + context 'ends slash' do + let(:zentao_integration) { create(:zentao_integration, api_url: 'https://jihudemo.zentao.net/zentao/') } + + include_examples 'joins api_url correctly' + end + end + + context 'no api url' do + let(:zentao_integration) { create(:zentao_integration, url: 'https://jihudemo.zentao.net') } + + it 'joins url correctly' do + expect(integration.send(:url, "products/1").to_s) + .to eq("https://jihudemo.zentao.net/api.php/v1/products/1") + end + end + end end diff --git a/spec/lib/service_ping/build_payload_spec.rb b/spec/lib/service_ping/build_payload_spec.rb new file mode 100644 index 00000000000..6cce07262b2 --- /dev/null +++ b/spec/lib/service_ping/build_payload_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServicePing::BuildPayload do + describe '#execute', :without_license do + subject(:service_ping_payload) { described_class.new.execute } + + include_context 'stubbed service ping metrics definitions' do + let(:subscription_metrics) do + [ + metric_attributes('active_user_count', "subscription") + ] + end + end + + context 'when usage_ping_enabled setting is false' do + before do + # Gitlab::CurrentSettings.usage_ping_enabled? == false + stub_config_setting(usage_ping_enabled: false) + end + + it 'returns empty service ping payload' do + expect(service_ping_payload).to eq({}) + end + end + + context 'when usage_ping_enabled setting is true' do + before do + # Gitlab::CurrentSettings.usage_ping_enabled? == true + stub_config_setting(usage_ping_enabled: true) + end + + it_behaves_like 'complete service ping payload' + + context 'with require stats consent enabled' do + before do + allow(User).to receive(:single_user) + .and_return(instance_double(User, :user, requires_usage_stats_consent?: true)) + end + + it 'returns empty service ping payload' do + expect(service_ping_payload).to eq({}) + end + end + end + end +end diff --git a/spec/lib/service_ping/devops_report_spec.rb b/spec/lib/service_ping/devops_report_spec.rb new file mode 100644 index 00000000000..793f3066097 --- /dev/null +++ b/spec/lib/service_ping/devops_report_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServicePing::DevopsReport do + let_it_be(:data) { { "conv_index": {} }.to_json } + let_it_be(:subject) { ServicePing::DevopsReport.new(Gitlab::Json.parse(data)) } + let_it_be(:devops_report) { DevOpsReport::Metric.new } + + describe '#execute' do + context 'when metric is persisted' do + before do + allow(DevOpsReport::Metric).to receive(:create).and_return(devops_report) + allow(devops_report).to receive(:persisted?).and_return(true) + end + + it 'does not call `track_and_raise_for_dev_exception`' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + subject.execute + end + end + + context 'when metric is not persisted' do + before do + allow(DevOpsReport::Metric).to receive(:create).and_return(devops_report) + allow(devops_report).to receive(:persisted?).and_return(false) + end + + it 'calls `track_and_raise_for_dev_exception`' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + subject.execute + end + end + end +end diff --git a/spec/lib/service_ping/permit_data_categories_spec.rb b/spec/lib/service_ping/permit_data_categories_spec.rb new file mode 100644 index 00000000000..d1027a6f1ab --- /dev/null +++ b/spec/lib/service_ping/permit_data_categories_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServicePing::PermitDataCategories do + describe '#execute', :without_license do + subject(:permitted_categories) { described_class.new.execute } + + context 'when usage ping setting is set to true' do + before do + allow(User).to receive(:single_user) + .and_return(instance_double(User, :user, requires_usage_stats_consent?: false)) + stub_config_setting(usage_ping_enabled: true) + end + + it 'returns all categories' do + expect(permitted_categories).to match_array(%w[standard subscription operational optional]) + end + end + + context 'when usage ping setting is set to false' do + before do + allow(User).to receive(:single_user) + .and_return(instance_double(User, :user, requires_usage_stats_consent?: false)) + stub_config_setting(usage_ping_enabled: false) + end + + it 'returns no categories' do + expect(permitted_categories).to match_array([]) + end + end + + context 'when User.single_user&.requires_usage_stats_consent? is required' do + before do + allow(User).to receive(:single_user) + .and_return(instance_double(User, :user, requires_usage_stats_consent?: true)) + stub_config_setting(usage_ping_enabled: true) + end + + it 'returns no categories' do + expect(permitted_categories).to match_array([]) + end + end + end +end diff --git a/spec/lib/service_ping/service_ping_settings_spec.rb b/spec/lib/service_ping/service_ping_settings_spec.rb new file mode 100644 index 00000000000..040a5027274 --- /dev/null +++ b/spec/lib/service_ping/service_ping_settings_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServicePing::ServicePingSettings do + using RSpec::Parameterized::TableSyntax + + describe '#product_intelligence_enabled?' do + where(:usage_ping_enabled, :requires_usage_stats_consent, :expected_product_intelligence_enabled) do + # Usage ping enabled + true | false | true + true | true | false + + # Usage ping disabled + false | false | false + false | true | false + end + + with_them do + before do + allow(User).to receive(:single_user) + .and_return(instance_double(User, :user, requires_usage_stats_consent?: requires_usage_stats_consent)) + stub_config_setting(usage_ping_enabled: usage_ping_enabled) + end + + it 'has the correct product_intelligence_enabled?' do + expect(described_class.product_intelligence_enabled?).to eq(expected_product_intelligence_enabled) + end + end + end + + describe '#enabled?' do + describe 'has the correct enabled' do + it 'when false' do + stub_config_setting(usage_ping_enabled: false) + + expect(described_class.enabled?).to eq(false) + end + + it 'when true' do + stub_config_setting(usage_ping_enabled: true) + + expect(described_class.enabled?).to eq(true) + end + end + end +end diff --git a/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb index 1ba89af1b02..246df2e409b 100644 --- a/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb @@ -22,14 +22,6 @@ RSpec.describe Sidebars::Groups::Menus::CiCdMenu do specify { is_expected.not_to be_nil } - describe 'when feature flag :runner_list_group_view_vue_ui is disabled' do - before do - stub_feature_flags(runner_list_group_view_vue_ui: false) - end - - specify { is_expected.to be_nil } - end - describe 'when the user does not have access' do let(:user) { nil } diff --git a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb index 36d5b3376b7..5bf8be9d6e5 100644 --- a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do +RSpec.describe Sidebars::Groups::Menus::KubernetesMenu, :request_store do let_it_be(:owner) { create(:user) } let_it_be(:group) do build(:group, :private).tap do |g| diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb index 71b696516b6..252da8ea699 100644 --- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb @@ -72,18 +72,6 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do let(:item_id) { :ci_cd } it_behaves_like 'access rights checks' - - describe 'when runner list group view is disabled' do - before do - stub_feature_flags(runner_list_group_view_vue_ui: false) - end - - it_behaves_like 'access rights checks' - - it 'has group runners as active_routes' do - expect(subject.active_routes[:path]).to match_array %w[ci_cd#show groups/runners#show groups/runners#edit] - end - end end describe 'Applications menu' do diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb index 81114f5a0b3..2da7d324708 100644 --- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb @@ -39,27 +39,17 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do subject.renderable_items.delete(find_menu_item(:kubernetes)) end - it 'menu link points to Serverless page' do - expect(subject.link).to eq find_menu_item(:serverless).link + it 'menu link points to Terraform page' do + expect(subject.link).to eq find_menu_item(:terraform).link end - context 'when Serverless menu is not visible' do + context 'when Terraform menu is not visible' do before do - subject.renderable_items.delete(find_menu_item(:serverless)) + subject.renderable_items.delete(find_menu_item(:terraform)) end - it 'menu link points to Terraform page' do - expect(subject.link).to eq find_menu_item(:terraform).link - end - - context 'when Terraform menu is not visible' do - before do - subject.renderable_items.delete(find_menu_item(:terraform)) - end - - it 'menu link points to Google Cloud page' do - expect(subject.link).to eq find_menu_item(:google_cloud).link - end + it 'menu link points to Google Cloud page' do + expect(subject.link).to eq find_menu_item(:google_cloud).link end end end @@ -88,20 +78,6 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do it_behaves_like 'access rights checks' end - describe 'Serverless' do - let(:item_id) { :serverless } - - it_behaves_like 'access rights checks' - - context 'when feature :deprecated_serverless is disabled' do - before do - stub_feature_flags(deprecated_serverless: false) - end - - it { is_expected.to be_nil } - end - end - describe 'Terraform' do let(:item_id) { :terraform } diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb index e8c6fb790c3..b11c9db4e46 100644 --- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb @@ -72,12 +72,28 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do let(:item_id) { :logs } it_behaves_like 'access rights checks' + + context 'when feature disabled' do + before do + stub_feature_flags(monitor_logging: false) + end + + specify { is_expected.to be_nil } + end end describe 'Tracing' do let(:item_id) { :tracing } it_behaves_like 'access rights checks' + + context 'when feature disabled' do + before do + stub_feature_flags(monitor_tracing: false) + end + + specify { is_expected.to be_nil } + end end describe 'Error Tracking' do diff --git a/spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb b/spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb new file mode 100644 index 00000000000..dfb3c511470 --- /dev/null +++ b/spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rake_helper' +require_relative '../../../support/helpers/next_instance_of' + +RSpec.describe 'gitlab:metrics_exporter:install' do + before do + Rake.application.rake_require 'tasks/gitlab/metrics_exporter' + end + + subject(:task) do + Rake::Task['gitlab:metrics_exporter:install'] + end + + context 'when no target directory is specified' do + it 'aborts with an error message' do + expect do + expect { task.execute }.to output(/Please specify the directory/).to_stdout + end.to raise_error(SystemExit) + end + end + + context 'when target directory is specified' do + let(:args) { Rake::TaskArguments.new(%w(dir), %w(path/to/exporter)) } + let(:context) { TOPLEVEL_BINDING.eval('self') } + let(:expected_clone_params) do + { + repo: 'https://gitlab.com/gitlab-org/gitlab-metrics-exporter.git', + version: 'main', + target_dir: 'path/to/exporter' + } + end + + context 'when dependencies are missing' do + it 'aborts with an error message' do + expect(Gitlab::Utils).to receive(:which).with('gmake').ordered + expect(Gitlab::Utils).to receive(:which).with('make').ordered + + expect do + expect { task.execute(args) }.to output(/Couldn't find a 'make' binary/).to_stdout + end.to raise_error(SystemExit) + end + end + + it 'installs the exporter with gmake' do + expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered + expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered + expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered + expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered + + task.execute(args) + end + + it 'installs the exporter with make' do + expect(Gitlab::Utils).to receive(:which).with('gmake').ordered + expect(Gitlab::Utils).to receive(:which).with('make').and_return('path/to/make').ordered + expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered + expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered + expect(context).to receive(:run_command!).with(['path/to/make']).ordered + + task.execute(args) + end + + context 'when overriding version via environment variable' do + before do + stub_env('GITLAB_METRICS_EXPORTER_VERSION', '1.0') + end + + it 'clones from repository with that version instead' do + expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered + expect(context).to receive(:checkout_or_clone_version).with( + hash_including(expected_clone_params.merge(version: '1.0')) + ).ordered + expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered + expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered + + task.execute(args) + end + end + end +end |