diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /spec/lib/gitlab | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'spec/lib/gitlab')
311 files changed, 7275 insertions, 3165 deletions
diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb index d3c1a96253c..ad2a3c7b462 100644 --- a/spec/lib/gitlab/alert_management/payload/base_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -228,6 +228,46 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) } end end + + context 'with present, non-string values for string fields' do + let_it_be(:stubs) do + { + description: { "description" => "description" }, + monitoring_tool: ['datadog', 5], + service: 4356875, + title: true + } + end + + before do + allow(parsed_payload).to receive_messages(stubs) + end + + it 'casts values to strings' do + is_expected.to eq({ + description: "{\"description\"=>\"description\"}", + monitoring_tool: "[\"datadog\", 5]", + service: '4356875', + project_id: project.id, + title: "true" + }) + end + end + + context 'with blank values for string fields' do + let_it_be(:stubs) do + { + description: nil, + monitoring_tool: '', + service: {}, + title: [] + } + end + + it 'leaves the fields blank' do + is_expected.to eq({ project_id: project.id }) + end + end end describe '#gitlab_fingerprint' do diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb index f9e18a65af4..8b2a228b935 100644 --- a/spec/lib/gitlab/application_context_spec.rb +++ b/spec/lib/gitlab/application_context_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Gitlab::ApplicationContext do end it 'raises an error when passing invalid options' do - expect { described_class.push(no: 'option')}.to raise_error(ArgumentError) + expect { described_class.push(no: 'option') }.to raise_error(ArgumentError) end end diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb index 177ce1134d8..41e79f811fa 100644 --- a/spec/lib/gitlab/application_rate_limiter_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter_spec.rb @@ -111,23 +111,35 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting shared_examples 'throttles based on key and scope' do let(:start_time) { Time.current.beginning_of_hour } - it 'returns true when threshold is exceeded' do + let(:threshold) { nil } + let(:interval) { nil } + + it 'returns true when threshold is exceeded', :aggregate_failures do travel_to(start_time) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(false) end travel_to(start_time + 1.minute) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(true) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(true) # Assert that it does not affect other actions or scope expect(subject.throttled?(:another_action, scope: scope)).to eq(false) - expect(subject.throttled?(:test_action, scope: [user])).to eq(false) + + expect(subject.throttled?( + :test_action, scope: [user], threshold: threshold, interval: interval) + ).to eq(false) end end - it 'returns false when interval has elapsed' do + it 'returns false when interval has elapsed', :aggregate_failures do travel_to(start_time) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(false) # another_action has a threshold of 2 so we simulate 2 requests expect(subject.throttled?(:another_action, scope: scope)).to eq(false) @@ -135,21 +147,34 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting end travel_to(start_time + 2.minutes) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(false) # Assert that another_action has its own interval that hasn't elapsed expect(subject.throttled?(:another_action, scope: scope)).to eq(true) end end - it 'allows peeking at the current state without changing its value' do + it 'allows peeking at the current state without changing its value', :aggregate_failures do travel_to(start_time) do - expect(subject.throttled?(:test_action, scope: scope)).to eq(false) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(false) + 2.times do - expect(subject.throttled?(:test_action, scope: scope, peek: true)).to eq(false) + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval, peek: true) + ).to eq(false) end - expect(subject.throttled?(:test_action, scope: scope)).to eq(true) - expect(subject.throttled?(:test_action, scope: scope, peek: true)).to eq(true) + + expect(subject.throttled?( + :test_action, scope: scope, threshold: threshold, interval: interval) + ).to eq(true) + + expect(subject.throttled?( + :test_action, scope: scope, peek: true, threshold: threshold, interval: interval) + ).to eq(true) end end end @@ -165,6 +190,28 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting it_behaves_like 'throttles based on key and scope' end + + context 'when threshold and interval get overwritten from rate_limits' do + let(:rate_limits) do + { + test_action: { + threshold: 0, + interval: 0 + }, + another_action: { + threshold: -> { 2 }, + interval: -> { 3.minutes } + } + } + end + + let(:scope) { [user, project] } + + it_behaves_like 'throttles based on key and scope' do + let(:threshold) { 1 } + let(:interval) { 2.minutes } + end + end end describe '.peek' do diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index bfea1315d90..b2bce2076b0 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -791,7 +791,7 @@ module Gitlab end context 'when the file does not exist' do - it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")} + it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]") } end end diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb new file mode 100644 index 00000000000..fc5917ca583 --- /dev/null +++ b/spec/lib/gitlab/audit/auditor_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::Auditor do + let(:name) { 'audit_operation' } + let(:author) { create(:user) } + let(:group) { create(:group) } + let(:provider) { 'standard' } + let(:context) do + { name: name, + author: author, + scope: group, + target: group, + authentication_event: true, + authentication_provider: provider, + message: "Signed in using standard authentication" } + end + + let(:logger) { instance_spy(Gitlab::AuditJsonLogger) } + + subject(:auditor) { described_class } + + describe '.audit' do + context 'when authentication event' do + let(:audit!) { auditor.audit(context) } + + it 'creates an authentication event' do + expect(AuthenticationEvent).to receive(:new).with( + { + user: author, + user_name: author.name, + ip_address: author.current_sign_in_ip, + result: AuthenticationEvent.results[:success], + provider: provider + } + ).and_call_original + + audit! + end + + it 'logs audit events to database', :aggregate_failures do + freeze_time do + audit! + + audit_event = AuditEvent.last + + expect(audit_event.author_id).to eq(author.id) + expect(audit_event.entity_id).to eq(group.id) + expect(audit_event.entity_type).to eq(group.class.name) + expect(audit_event.created_at).to eq(Time.zone.now) + expect(audit_event.details[:target_id]).to eq(group.id) + expect(audit_event.details[:target_type]).to eq(group.class.name) + end + end + + it 'logs audit events to file' do + expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) + + audit! + + expect(logger).to have_received(:info).with( + hash_including( + 'author_id' => author.id, + 'author_name' => author.name, + 'entity_id' => group.id, + 'entity_type' => group.class.name, + 'details' => kind_of(Hash) + ) + ) + end + + context 'when overriding the create datetime' do + let(:context) do + { name: name, + author: author, + scope: group, + target: group, + created_at: 3.weeks.ago, + authentication_event: true, + authentication_provider: provider, + message: "Signed in using standard authentication" } + end + + it 'logs audit events to database', :aggregate_failures do + freeze_time do + audit! + + audit_event = AuditEvent.last + + expect(audit_event.author_id).to eq(author.id) + expect(audit_event.entity_id).to eq(group.id) + expect(audit_event.entity_type).to eq(group.class.name) + expect(audit_event.created_at).to eq(3.weeks.ago) + expect(audit_event.details[:target_id]).to eq(group.id) + expect(audit_event.details[:target_type]).to eq(group.class.name) + end + end + + it 'logs audit events to file' do + freeze_time do + expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) + + audit! + + expect(logger).to have_received(:info).with( + hash_including( + 'author_id' => author.id, + 'author_name' => author.name, + 'entity_id' => group.id, + 'entity_type' => group.class.name, + 'details' => kind_of(Hash), + 'created_at' => 3.weeks.ago.iso8601(3) + ) + ) + end + end + end + + context 'when overriding the additional_details' do + additional_details = { action: :custom, from: false, to: true } + let(:context) do + { name: name, + author: author, + scope: group, + target: group, + created_at: Time.zone.now, + additional_details: additional_details, + authentication_event: true, + authentication_provider: provider, + message: "Signed in using standard authentication" } + end + + it 'logs audit events to database' do + freeze_time do + audit! + + expect(AuditEvent.last.details).to include(additional_details) + end + end + + it 'logs audit events to file' do + freeze_time do + expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) + + audit! + + expect(logger).to have_received(:info).with( + hash_including( + 'details' => hash_including('action' => 'custom', 'from' => 'false', 'to' => 'true'), + 'action' => 'custom', + 'from' => 'false', + 'to' => 'true' + ) + ) + end + end + end + + context 'when overriding the target_details' do + target_details = "this is my target details" + let(:context) do + { + name: name, + author: author, + scope: group, + target: group, + created_at: Time.zone.now, + target_details: target_details, + authentication_event: true, + authentication_provider: provider, + message: "Signed in using standard authentication" + } + end + + it 'logs audit events to database' do + freeze_time do + audit! + + audit_event = AuditEvent.last + expect(audit_event.details).to include({ target_details: target_details }) + expect(audit_event.target_details).to eq(target_details) + end + end + + it 'logs audit events to file' do + freeze_time do + expect(::Gitlab::AuditJsonLogger).to receive(:build).and_return(logger) + + audit! + + expect(logger).to have_received(:info).with( + hash_including( + 'details' => hash_including('target_details' => target_details), + 'target_details' => target_details + ) + ) + end + end + end + end + + context 'when authentication event is false' do + let(:context) do + { name: name, author: author, scope: group, + target: group, authentication_event: false, message: "sample message" } + end + + it 'does not create an authentication event' do + expect { auditor.audit(context) }.not_to change(AuthenticationEvent, :count) + end + end + + context 'when authentication event is invalid' do + let(:audit!) { auditor.audit(context) } + + before do + allow(AuthenticationEvent).to receive(:new).and_raise(ActiveRecord::RecordInvalid) + allow(Gitlab::ErrorTracking).to receive(:track_exception) + end + + it 'tracks error' do + audit! + + expect(Gitlab::ErrorTracking).to have_received(:track_exception).with( + kind_of(ActiveRecord::RecordInvalid), + { audit_operation: name } + ) + end + + it 'does not throw exception' do + expect { auditor.audit(context) }.not_to raise_exception + end + end + + context 'when audit events are invalid' do + let(:audit!) { auditor.audit(context) } + + before do + allow(AuditEvent).to receive(:bulk_insert!).and_raise(ActiveRecord::RecordInvalid) + allow(Gitlab::ErrorTracking).to receive(:track_exception) + end + + it 'tracks error' do + audit! + + expect(Gitlab::ErrorTracking).to have_received(:track_exception).with( + kind_of(ActiveRecord::RecordInvalid), + { audit_operation: name } + ) + end + + it 'does not throw exception' do + expect { auditor.audit(context) }.not_to raise_exception + end + end + end +end diff --git a/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb b/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb index f55e1b44936..89664c57084 100644 --- a/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb +++ b/spec/lib/gitlab/audit/ci_runner_token_author_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Audit::CiRunnerTokenAuthor do describe '.initialize' do subject { described_class.new(audit_event) } - let(:details) { } + let(:details) {} let(:audit_event) { instance_double(AuditEvent, details: details, entity_type: 'Project', entity_path: 'd/e') } context 'with runner_authentication_token' do diff --git a/spec/lib/gitlab/audit/deploy_key_author_spec.rb b/spec/lib/gitlab/audit/deploy_key_author_spec.rb new file mode 100644 index 00000000000..72444f77c91 --- /dev/null +++ b/spec/lib/gitlab/audit/deploy_key_author_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::DeployKeyAuthor do + describe '#initialize' do + it 'sets correct attributes' do + expect(described_class.new(name: 'Lorem deploy key')) + .to have_attributes(id: -3, name: 'Lorem deploy key') + end + + it 'sets default name when it is not provided' do + expect(described_class.new) + .to have_attributes(id: -3, name: 'Deploy Key') + end + end +end diff --git a/spec/lib/gitlab/audit/null_author_spec.rb b/spec/lib/gitlab/audit/null_author_spec.rb index 2045139a5f7..e2f71a34534 100644 --- a/spec/lib/gitlab/audit/null_author_spec.rb +++ b/spec/lib/gitlab/audit/null_author_spec.rb @@ -57,6 +57,15 @@ RSpec.describe Gitlab::Audit::NullAuthor do 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 + + it 'returns DeployKeyAuthor when id equals -3', :aggregate_failures do + allow(audit_event).to receive(:[]).with(:author_name).and_return('Test deploy key') + allow(audit_event).to receive(:details).and_return({}) + allow(audit_event).to receive(:target_type) + + expect(subject.for(-3, audit_event)).to be_a(Gitlab::Audit::DeployKeyAuthor) + expect(subject.for(-3, audit_event)).to have_attributes(id: -3, name: 'Test deploy key') + end end describe '#current_sign_in_ip' do diff --git a/spec/lib/gitlab/audit/null_target_spec.rb b/spec/lib/gitlab/audit/null_target_spec.rb new file mode 100644 index 00000000000..f192e0cd8db --- /dev/null +++ b/spec/lib/gitlab/audit/null_target_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::NullTarget do + subject { described_class.new } + + describe '#id' do + it 'returns nil' do + expect(subject.id).to eq(nil) + end + end + + describe '#type' do + it 'returns nil' do + expect(subject.type).to eq(nil) + end + end + + describe '#details' do + it 'returns nil' do + expect(subject.details).to eq(nil) + end + end +end diff --git a/spec/lib/gitlab/audit/target_spec.rb b/spec/lib/gitlab/audit/target_spec.rb new file mode 100644 index 00000000000..5c06cd117a9 --- /dev/null +++ b/spec/lib/gitlab/audit/target_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Audit::Target do + let(:object) { double('object') } # rubocop:disable RSpec/VerifiedDoubles + + subject { described_class.new(object) } + + describe '#id' do + it 'returns object id' do + allow(object).to receive(:id).and_return(object_id) + + expect(subject.id).to eq(object_id) + end + end + + describe '#type' do + it 'returns object class name' do + allow(object).to receive_message_chain(:class, :name).and_return('User') + + expect(subject.type).to eq('User') + end + end + + describe '#details' do + using RSpec::Parameterized::TableSyntax + + where(:name, :audit_details, :details) do + 'jackie' | 'wanderer' | 'jackie' + 'jackie' | nil | 'jackie' + nil | 'wanderer' | 'wanderer' + nil | nil | 'unknown' + end + + before do + allow(object).to receive(:name).and_return(name) if name + allow(object).to receive(:audit_details).and_return(audit_details) if audit_details + end + + with_them do + it 'returns details' do + expect(subject.details).to eq(details) + end + end + end +end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index e985f66bfe9..d0b44135a2f 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -127,7 +127,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } before do - set_bearer_token(doorkeeper_access_token.token) + set_bearer_token(doorkeeper_access_token.plaintext_token) end it { is_expected.to eq user } @@ -577,7 +577,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do context 'passed as header' do before do - set_bearer_token(doorkeeper_access_token.token) + set_bearer_token(doorkeeper_access_token.plaintext_token) end it 'returns token if valid oauth_access_token' do @@ -587,7 +587,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do context 'passed as param' do it 'returns user if valid oauth_access_token' do - set_param(:access_token, doorkeeper_access_token.token) + set_param(:access_token, doorkeeper_access_token.plaintext_token) expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token end diff --git a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb index f23fdd3fbcb..3d9be4c3489 100644 --- a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb +++ b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin } end - subject { described_class.new(ip) } + subject(:rate_limiter) { described_class.new(ip) } before do stub_rack_attack_setting(options) @@ -25,7 +25,7 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin end after do - subject.reset! + rate_limiter.reset! end describe '#register_fail!' do @@ -86,7 +86,7 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin end end - context 'when IP is whitlisted' do + context 'when IP is allow listed' do let(:ip) { '127.0.0.1' } it_behaves_like 'skips the rate limiter' @@ -97,4 +97,20 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin it_behaves_like 'skips the rate limiter' end + + describe '#trusted_ip?' do + subject { rate_limiter.trusted_ip? } + + context 'when ip is in the trusted list' do + let(:ip) { '127.0.0.1' } + + it { is_expected.to be_truthy } + end + + context 'when ip is not in the trusted list' do + let(:ip) { '10.0.0.1' } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb index 69068883096..a044094179c 100644 --- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb @@ -14,6 +14,7 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash do ) end + let(:provider_config) { { 'args' => { 'gitlab_username_claim' => 'first_name' } } } let(:uid_raw) do +"CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net" end @@ -35,6 +36,7 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash do let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } let(:name_utf8) { name_ascii.force_encoding(Encoding::UTF_8) } + let(:first_name_utf8) { first_name_ascii.force_encoding(Encoding::UTF_8) } let(:info_hash) do { @@ -91,6 +93,34 @@ RSpec.describe Gitlab::Auth::OAuth::AuthHash do end end + context 'custom username field provided' do + before do + allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for).and_return(provider_config) + end + + it 'uses the custom field for the username' do + expect(auth_hash.username).to eql first_name_utf8 + end + + it 'uses the default claim for the username when the custom claim is not found' do + provider_config['args']['gitlab_username_claim'] = 'nonexistent' + + expect(auth_hash.username).to eql nickname_utf8 + end + + it 'uses the default claim for the username when the custom claim is empty' do + info_hash[:first_name] = '' + + expect(auth_hash.username).to eql nickname_utf8 + end + + it 'uses the default claim for the username when the custom claim is nil' do + info_hash[:first_name] = nil + + expect(auth_hash.username).to eql nickname_utf8 + end + end + context 'auth_hash constructed with ASCII-8BIT encoding' do it 'forces utf8 encoding on uid' do expect(auth_hash.uid.encoding).to eql Encoding::UTF_8 diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 5f5e7f211f8..b160f322fb8 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -727,6 +727,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do context 'signup with linked omniauth and LDAP account' do before do stub_omniauth_config(auto_link_ldap_user: true) + stub_ldap_setting(enabled: true) allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 1e869df0988..c2d64aa2fb3 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end context 'when IP is already banned' do - subject { gl_auth.find_for_git_client('username', 'password', project: nil, ip: 'ip') } + subject { gl_auth.find_for_git_client('username-does-not-matter', 'password-does-not-matter', project: nil, ip: 'ip') } before do expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter| @@ -219,16 +219,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end it 'recognizes master passwords' do - user = create(:user, password: 'password') + user = create(:user) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities) + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities) end include_examples 'user login operation with unique ip limit' do - let(:user) { create(:user, password: 'password') } + let(:user) { create(:user) } def operation - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities) + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities) end end @@ -502,8 +502,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do user = create( :user, :blocked, - username: 'normal_user', - password: 'my-secret' + username: 'normal_user' ) expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) @@ -512,7 +511,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do context 'when 2fa is enabled globally' do let_it_be(:user) do - create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago) + create(:user, username: 'normal_user', otp_grace_period_started_at: 1.day.ago) end before do @@ -536,7 +535,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do context 'when 2fa is enabled personally' do let(:user) do - create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago) + create(:user, :two_factor, username: 'normal_user', otp_grace_period_started_at: 1.day.ago) end it 'fails' do @@ -548,8 +547,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'goes through lfs authentication' do user = create( :user, - username: 'normal_user', - password: 'my-secret' + username: 'normal_user' ) expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) @@ -559,8 +557,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'goes through oauth authentication when the username is oauth2' do user = create( :user, - username: 'oauth2', - password: 'my-secret' + username: 'oauth2' ) expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) @@ -635,7 +632,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do context 'when deploy token and user have the same username' do let(:username) { 'normal_user' } - let(:user) { create(:user, username: username, password: 'my-secret') } + let(:user) { create(:user, username: username) } let(:deploy_token) { create(:deploy_token, username: username, read_registry: false, projects: [project]) } it 'succeeds for the token' do @@ -648,7 +645,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'succeeds for the user' do auth_success = { actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities } - expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip')) + expect(gl_auth.find_for_git_client(username, user.password, project: project, ip: 'ip')) .to have_attributes(auth_success) end end @@ -834,72 +831,64 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end describe 'find_with_user_password' do - let!(:user) do - create(:user, - username: username, - password: password, - password_confirmation: password) - end - + let!(:user) { create(:user, username: username) } let(:username) { 'John' } # username isn't lowercase, test this - let(:password) { 'my-secret' } it "finds user by valid login/password" do - expect(gl_auth.find_with_user_password(username, password)).to eql user + expect(gl_auth.find_with_user_password(username, user.password)).to eql user end it 'finds user by valid email/password with case-insensitive email' do - expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user + expect(gl_auth.find_with_user_password(user.email.upcase, user.password)).to eql user end it 'finds user by valid username/password with case-insensitive username' do - expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user + expect(gl_auth.find_with_user_password(username.upcase, user.password)).to eql user end it "does not find user with invalid password" do - password = 'wrong' - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + expect(gl_auth.find_with_user_password(username, 'incorrect_password')).not_to eql user end it "does not find user with invalid login" do - user = 'wrong' - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + username = 'wrong' + expect(gl_auth.find_with_user_password(username, user.password)).not_to eql user end include_examples 'user login operation with unique ip limit' do def operation - expect(gl_auth.find_with_user_password(username, password)).to eq(user) + expect(gl_auth.find_with_user_password(username, user.password)).to eq(user) end end it 'finds the user in deactivated state' do user.deactivate! - expect(gl_auth.find_with_user_password(username, password)).to eql user + expect(gl_auth.find_with_user_password(username, user.password)).to eql user end it "does not find user in blocked state" do user.block - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + expect(gl_auth.find_with_user_password(username, user.password)).not_to eql user end it 'does not find user in locked state' do user.lock_access! - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + expect(gl_auth.find_with_user_password(username, user.password)).not_to eql user end it "does not find user in ldap_blocked state" do user.ldap_block - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + expect(gl_auth.find_with_user_password(username, user.password)).not_to eql user end it 'does not find user in blocked_pending_approval state' do user.block_pending_approval - expect(gl_auth.find_with_user_password(username, password)).not_to eql user + expect(gl_auth.find_with_user_password(username, user.password)).not_to eql user end context 'with increment_failed_attempts' do @@ -917,7 +906,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do user.save! expect do - gl_auth.find_with_user_password(username, password, increment_failed_attempts: true) + gl_auth.find_with_user_password(username, user.password, increment_failed_attempts: true) user.reload end.to change(user, :failed_attempts).from(2).to(0) end @@ -946,7 +935,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do user.save! expect do - gl_auth.find_with_user_password(username, password, increment_failed_attempts: true) + gl_auth.find_with_user_password(username, user.password, increment_failed_attempts: true) user.reload end.not_to change(user, :failed_attempts) end @@ -961,7 +950,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it "tries to autheticate with db before ldap" do expect(Gitlab::Auth::Ldap::Authentication).not_to receive(:login) - expect(gl_auth.find_with_user_password(username, password)).to eq(user) + expect(gl_auth.find_with_user_password(username, user.password)).to eq(user) end it "does not find user by using ldap as fallback to for authentication" do @@ -983,7 +972,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end it "does not find user by valid login/password" do - expect(gl_auth.find_with_user_password(username, password)).to be_nil + expect(gl_auth.find_with_user_password(username, user.password)).to be_nil end context "with ldap enabled" do @@ -992,7 +981,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end it "does not find non-ldap user by valid login/password" do - expect(gl_auth.find_with_user_password(username, password)).to be_nil + expect(gl_auth.find_with_user_password(username, user.password)).to be_nil end end end diff --git a/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb deleted file mode 100644 index 8980a26932b..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillCiNamespaceMirrors, :migration, schema: 20211208122200 do - let(:namespaces) { table(:namespaces) } - let(:ci_namespace_mirrors) { table(:ci_namespace_mirrors) } - - subject { described_class.new } - - describe '#perform' do - it 'creates hierarchies for all namespaces in range' do - namespaces.create!(id: 5, name: 'test1', path: 'test1') - namespaces.create!(id: 7, name: 'test2', path: 'test2') - namespaces.create!(id: 8, name: 'test3', path: 'test3') - - subject.perform(5, 7) - - expect(ci_namespace_mirrors.all).to contain_exactly( - an_object_having_attributes(namespace_id: 5, traversal_ids: [5]), - an_object_having_attributes(namespace_id: 7, traversal_ids: [7]) - ) - end - - it 'handles existing hierarchies gracefully' do - namespaces.create!(id: 5, name: 'test1', path: 'test1') - test2 = namespaces.create!(id: 7, name: 'test2', path: 'test2') - namespaces.create!(id: 8, name: 'test3', path: 'test3', parent_id: 7) - namespaces.create!(id: 9, name: 'test4', path: 'test4') - - # Simulate a situation where a user has had a chance to move a group to another parent - # before the background migration has had a chance to run - test2.update!(parent_id: 5) - ci_namespace_mirrors.create!(namespace_id: test2.id, traversal_ids: [5, 7]) - - subject.perform(5, 8) - - expect(ci_namespace_mirrors.all).to contain_exactly( - an_object_having_attributes(namespace_id: 5, traversal_ids: [5]), - an_object_having_attributes(namespace_id: 7, traversal_ids: [5, 7]), - an_object_having_attributes(namespace_id: 8, traversal_ids: [5, 7, 8]) - ) - end - end -end diff --git a/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb deleted file mode 100644 index 4eec83879e3..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillCiProjectMirrors, :migration, schema: 20211208122201 do - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:ci_project_mirrors) { table(:ci_project_mirrors) } - - subject { described_class.new } - - describe '#perform' do - it 'creates ci_project_mirrors for all projects in range' do - namespaces.create!(id: 10, name: 'namespace1', path: 'namespace1') - projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1') - projects.create!(id: 7, namespace_id: 10, name: 'test2', path: 'test2') - projects.create!(id: 8, namespace_id: 10, name: 'test3', path: 'test3') - - subject.perform(5, 7) - - expect(ci_project_mirrors.all).to contain_exactly( - an_object_having_attributes(project_id: 5, namespace_id: 10), - an_object_having_attributes(project_id: 7, namespace_id: 10) - ) - end - - it 'handles existing ci_project_mirrors gracefully' do - namespaces.create!(id: 10, name: 'namespace1', path: 'namespace1') - namespaces.create!(id: 11, name: 'namespace2', path: 'namespace2', parent_id: 10) - projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1') - projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2') - projects.create!(id: 8, namespace_id: 11, name: 'test3', path: 'test3') - - # Simulate a situation where a user has had a chance to move a project to another namespace - # before the background migration has had a chance to run - ci_project_mirrors.create!(project_id: 7, namespace_id: 10) - - subject.perform(5, 7) - - expect(ci_project_mirrors.all).to contain_exactly( - an_object_having_attributes(project_id: 5, namespace_id: 10), - an_object_having_attributes(project_id: 7, namespace_id: 10) - ) - end - end -end diff --git a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb index 1aac5970a77..aaf8c124a83 100644 --- a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, schema: 20220208115439 do +RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, + :suppress_gitlab_schemas_validate_connection, schema: 20220208115439 do let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } let(:ci_cd_settings) { table(:project_ci_cd_settings) } diff --git a/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb deleted file mode 100644 index 7c78d8b0305..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillCiRunnerSemver, :migration, schema: 20220601151900 do - let(:ci_runners) { table(:ci_runners, database: :ci) } - - subject do - described_class.new( - start_id: 10, - end_id: 15, - batch_table: :ci_runners, - batch_column: :id, - sub_batch_size: 10, - pause_ms: 0, - connection: Ci::ApplicationRecord.connection) - end - - describe '#perform' do - it 'populates semver column on all runners in range' do - ci_runners.create!(id: 10, runner_type: 1, version: %q(HEAD-fd84d97)) - ci_runners.create!(id: 11, runner_type: 1, version: %q(v1.2.3)) - ci_runners.create!(id: 12, runner_type: 1, version: %q(2.1.0)) - ci_runners.create!(id: 13, runner_type: 1, version: %q(11.8.0~beta.935.g7f6d2abc)) - ci_runners.create!(id: 14, runner_type: 1, version: %q(13.2.2/1.1.0)) - ci_runners.create!(id: 15, runner_type: 1, version: %q('14.3.4')) - - subject.perform - - expect(ci_runners.all).to contain_exactly( - an_object_having_attributes(id: 10, semver: nil), - an_object_having_attributes(id: 11, semver: '1.2.3'), - an_object_having_attributes(id: 12, semver: '2.1.0'), - an_object_having_attributes(id: 13, semver: '11.8.0'), - an_object_having_attributes(id: 14, semver: '13.2.2'), - an_object_having_attributes(id: 15, semver: '14.3.4') - ) - end - - it 'skips runners that already have semver value' do - ci_runners.create!(id: 10, runner_type: 1, version: %q(1.2.4), semver: '1.2.3') - ci_runners.create!(id: 11, runner_type: 1, version: %q(1.2.5)) - ci_runners.create!(id: 12, runner_type: 1, version: %q(HEAD), semver: '1.2.4') - - subject.perform - - expect(ci_runners.all).to contain_exactly( - an_object_having_attributes(id: 10, semver: '1.2.3'), - an_object_having_attributes(id: 11, semver: '1.2.5'), - an_object_having_attributes(id: 12, semver: '1.2.4') - ) - end - end -end diff --git a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb index d84bc479554..e0be5a785b8 100644 --- a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb @@ -13,6 +13,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, s batch_column: :id, sub_batch_size: 10, pause_ms: 0, + job_arguments: [4], connection: ActiveRecord::Base.connection) end @@ -27,7 +28,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(4) }.to change { group_features.count }.by(2) + expect { subject.perform }.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_namespace_id_of_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb new file mode 100644 index 00000000000..564aa3b8c01 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdOfVulnerabilityReads, schema: 20220722145845 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:scanners) { table(:vulnerability_scanners) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_reads) { table(:vulnerability_reads) } + + let(:namespace) { namespaces.create!(name: 'user', path: 'user') } + let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') } + let(:vulnerability) do + vulnerabilities.create!( + project_id: project.id, + author_id: user.id, + title: 'test', + severity: 1, + confidence: 1, + report_type: 1 + ) + end + + let(:vulnerability_read) do + vulnerability_reads.create!( + project_id: project.id, + vulnerability_id: vulnerability.id, + scanner_id: scanner.id, + severity: 1, + report_type: 1, + state: 1, + uuid: SecureRandom.uuid + ) + end + + subject(:perform_migration) do + described_class.new(start_id: vulnerability_read.vulnerability_id, + end_id: vulnerability_read.vulnerability_id, + batch_table: :vulnerability_reads, + batch_column: :vulnerability_id, + sub_batch_size: 1, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'sets the namespace_id of existing record' do + expect { perform_migration }.to change { vulnerability_read.reload.namespace_id }.from(nil).to(namespace.id) + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb new file mode 100644 index 00000000000..ae296483166 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +# rubocop:disable Layout/HashAlignment +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectImportLevel do + let(:migration) do + described_class.new( + start_id: table(:namespaces).minimum(:id), + end_id: table(:namespaces).maximum(:id), + batch_table: :namespaces, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ApplicationRecord.connection + ) + end + # rubocop:enable Layout/HashAlignment + + let(:namespaces_table) { table(:namespaces) } + let(:namespace_settings_table) { table(:namespace_settings) } + + let!(:user_namespace) do + namespaces_table.create!( + name: 'user_namespace', + path: 'user_namespace', + type: 'User', + project_creation_level: 100 + ) + end + + let!(:group_namespace_nil) do + namespaces_table.create!( + name: 'group_namespace_nil', + path: 'group_namespace_nil', + type: 'Group', + project_creation_level: nil + ) + end + + let!(:group_namespace_0) do + namespaces_table.create!( + name: 'group_namespace_0', + path: 'group_namespace_0', + type: 'Group', + project_creation_level: 0 + ) + end + + let!(:group_namespace_1) do + namespaces_table.create!( + name: 'group_namespace_1', + path: 'group_namespace_1', + type: 'Group', + project_creation_level: 1 + ) + end + + let!(:group_namespace_2) do + namespaces_table.create!( + name: 'group_namespace_2', + path: 'group_namespace_2', + type: 'Group', + project_creation_level: 2 + ) + end + + let!(:group_namespace_9999) do + namespaces_table.create!( + name: 'group_namespace_9999', + path: 'group_namespace_9999', + type: 'Group', + project_creation_level: 9999 + ) + end + + subject(:perform_migration) { migration.perform } + + before do + namespace_settings_table.create!(namespace_id: user_namespace.id) + namespace_settings_table.create!(namespace_id: group_namespace_nil.id) + namespace_settings_table.create!(namespace_id: group_namespace_0.id) + namespace_settings_table.create!(namespace_id: group_namespace_1.id) + namespace_settings_table.create!(namespace_id: group_namespace_2.id) + namespace_settings_table.create!(namespace_id: group_namespace_9999.id) + end + + describe 'Groups' do + using RSpec::Parameterized::TableSyntax + + where(:namespace_id, :prev_level, :new_level) do + lazy { group_namespace_0.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::NO_ACCESS + lazy { group_namespace_1.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::MAINTAINER + lazy { group_namespace_2.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::DEVELOPER + end + + with_them do + it 'backfills the correct project_import_level of Group namespaces' do + expect { perform_migration } + .to change { namespace_settings_table.find_by(namespace_id: namespace_id).project_import_level } + .from(prev_level).to(new_level) + end + end + + it 'does not update `User` namespaces or values outside range' do + expect { perform_migration } + .not_to change { namespace_settings_table.find_by(namespace_id: user_namespace.id).project_import_level } + + expect { perform_migration } + .not_to change { namespace_settings_table.find_by(namespace_id: group_namespace_9999.id).project_import_level } + end + + it 'maintains default import_level if creation_level is nil' do + project_import_level = namespace_settings_table.find_by(namespace_id: group_namespace_nil.id).project_import_level + + expect { perform_migration } + .not_to change { project_import_level } + + expect(project_import_level).to eq(::Gitlab::Access::OWNER) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb b/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb index 49056154744..4a65ecf8c75 100644 --- a/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillProjectsWithCoverage, schema: 20210818185845 do +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectsWithCoverage, + :suppress_gitlab_schemas_validate_connection, schema: 20210818185845 do let(:projects) { table(:projects) } let(:project_ci_feature_usages) { table(:project_ci_feature_usages) } let(:ci_pipelines) { table(:ci_pipelines) } diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index b5122af5cd4..6f75d3faef3 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat let(:file_name) { 'file_name.rb' } let(:content) { 'content' } - let(:ids) { snippets.pluck('MIN(id)', 'MAX(id)').first } + let(:ids) { snippets.pick('MIN(id)', 'MAX(id)') } let(:service) { described_class.new } subject { service.perform(*ids) } diff --git a/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb new file mode 100644 index 00000000000..79699375a8d --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_vulnerability_reads_cluster_agent_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityReadsClusterAgent, :migration, schema: 20220525221133 do # rubocop:disable Layout/LineLength + let(:migration) do + described_class.new(start_id: 1, end_id: 10, + batch_table: table_name, batch_column: batch_column, + sub_batch_size: sub_batch_size, pause_ms: pause_ms, + connection: ApplicationRecord.connection) + end + + let(:users_table) { table(:users) } + let(:vulnerability_reads_table) { table(:vulnerability_reads) } + let(:vulnerability_scanners_table) { table(:vulnerability_scanners) } + let(:vulnerabilities_table) { table(:vulnerabilities) } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:cluster_agents_table) { table(:cluster_agents) } + + let(:table_name) { 'vulnerability_reads' } + let(:batch_column) { :id } + let(:sub_batch_size) { 1_000 } + let(:pause_ms) { 0 } + + subject(:perform_migration) { migration.perform } + + before do + users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5) + + namespaces_table.create!(id: 1, name: 'Namespace 1', path: 'namespace-1') + namespaces_table.create!(id: 2, name: 'Namespace 2', path: 'namespace-2') + + projects_table.create!(id: 1, namespace_id: 1, name: 'Project 1', path: 'project-1', project_namespace_id: 1) + projects_table.create!(id: 2, namespace_id: 2, name: 'Project 2', path: 'project-2', project_namespace_id: 2) + + cluster_agents_table.create!(id: 1, name: 'Agent 1', project_id: 1) + cluster_agents_table.create!(id: 2, name: 'Agent 2', project_id: 2) + + vulnerability_scanners_table.create!(id: 1, project_id: 1, external_id: 'starboard', name: 'Starboard') + vulnerability_scanners_table.create!(id: 2, project_id: 2, external_id: 'starboard', name: 'Starboard') + + add_vulnerability_read!(1, project_id: 1, cluster_agent_id: 1, report_type: 7) + add_vulnerability_read!(3, project_id: 1, cluster_agent_id: 2, report_type: 7) + add_vulnerability_read!(5, project_id: 2, cluster_agent_id: 2, report_type: 5) + add_vulnerability_read!(7, project_id: 2, cluster_agent_id: 3, report_type: 7) + add_vulnerability_read!(9, project_id: 2, cluster_agent_id: 2, report_type: 7) + add_vulnerability_read!(10, project_id: 1, cluster_agent_id: 1, report_type: 7) + add_vulnerability_read!(11, project_id: 1, cluster_agent_id: 1, report_type: 7) + end + + it 'backfills `casted_cluster_agent_id` for the selected records', :aggregate_failures do + queries = ActiveRecord::QueryRecorder.new do + perform_migration + end + + expect(queries.count).to eq(3) + expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).count).to eq 3 + expect(vulnerability_reads_table.where.not(casted_cluster_agent_id: nil).pluck(:id)).to match_array([1, 9, 10]) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end + + private + + def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:) + vulnerabilities_table.create!( + id: id, + project_id: project_id, + author_id: 1, + title: "Vulnerability #{id}", + severity: 5, + confidence: 5, + report_type: report_type + ) + + vulnerability_reads_table.create!( + id: id, + uuid: SecureRandom.uuid, + severity: 5, + state: 1, + vulnerability_id: id, + scanner_id: project_id, + cluster_agent_id: cluster_agent_id.to_s, + project_id: project_id, + report_type: report_type + ) + 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 index 98866bb765f..f03f90ddbbb 100644 --- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb @@ -3,6 +3,113 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + describe '.generic_instance' do + it 'defines generic instance with only some of the attributes set' do + generic_instance = described_class.generic_instance( + batch_table: 'projects', batch_column: 'id', + job_arguments: %w(x y), connection: connection + ) + + expect(generic_instance.send(:batch_table)).to eq('projects') + expect(generic_instance.send(:batch_column)).to eq('id') + expect(generic_instance.instance_variable_get('@job_arguments')).to eq(%w(x y)) + expect(generic_instance.send(:connection)).to eq(connection) + + %i(start_id end_id sub_batch_size pause_ms).each do |attr| + expect(generic_instance.send(attr)).to eq(0) + end + end + end + + describe '.job_arguments' do + let(:job_class) do + Class.new(described_class) do + job_arguments :value_a, :value_b + end + end + + subject(: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, + job_arguments: %w(a b), + connection: connection) + end + + it 'defines methods' do + expect(job_instance.value_a).to eq('a') + expect(job_instance.value_b).to eq('b') + expect(job_class.job_arguments_count).to eq(2) + end + + context 'when no job arguments are defined' do + let(:job_class) do + Class.new(described_class) + end + + it 'job_arguments_count is 0' do + expect(job_class.job_arguments_count).to eq(0) + end + end + end + + describe '.scope_to' do + subject(: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, + job_arguments: %w(a b), + connection: connection) + end + + context 'when additional scoping is defined' do + let(:job_class) do + Class.new(described_class) do + job_arguments :value_a, :value_b + scope_to ->(r) { "#{r}-#{value_a}-#{value_b}".upcase } + end + end + + it 'applies additional scope to the provided relation' do + expect(job_instance.filter_batch('relation')).to eq('RELATION-A-B') + end + end + + context 'when there is no additional scoping defined' do + let(:job_class) do + Class.new(described_class) do + end + end + + it 'returns provided relation as is' do + expect(job_instance.filter_batch('relation')).to eq('relation') + end + end + end + + describe 'descendants', :eager_load do + it 'have the same method signature for #perform' do + expected_arity = described_class.instance_method(:perform).arity + offences = described_class.descendants.select { |klass| klass.instance_method(:perform).arity != expected_arity } + + expect(offences).to be_empty, "expected no descendants of #{described_class} to accept arguments for " \ + "'#perform', but some do: #{offences.join(", ")}" + end + + it 'do not use .batching_scope' do + offences = described_class.descendants.select { |klass| klass.respond_to?(:batching_scope) } + + expect(offences).to be_empty, "expected no descendants of #{described_class} to define '.batching_scope', " \ + "but some do: #{offences.join(", ")}" + end + end + describe '#perform' do let(:connection) { Gitlab::Database.database_base_models[:main].connection } @@ -66,6 +173,30 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(5, 10, nil, 20) end + context 'with additional scoping' do + let(:job_class) do + Class.new(described_class) do + scope_to ->(r) { r.where('mod(id, 2) = 0') } + + 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 + + it 'respects #filter_batch' do + expect { perform_job }.to change { test_table.where(to_column: nil).count }.from(4).to(2) + + expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(nil, 10, nil, 20) + end + end + it 'instruments the batch operation' do expect(job_instance.batch_metrics.affected_rows).to be_empty @@ -84,7 +215,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do 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(job_instance).to receive(:base_relation).and_return(test_table) expect(test_table).to receive(:each_batch).with(column: 'id', of: 2, order_hint: :updated_at) @@ -155,6 +286,24 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1) end + + context 'when used in combination with scope_to' do + let(:job_class) do + Class.new(described_class) do + scope_to ->(r) { r.where.not(from_column: 10) } + + def perform(*job_arguments) + distinct_each_batch(operation_name: :insert) do |sub_batch| + end + end + end + end + + it 'raises an error' do + expect { perform_job }.to raise_error RuntimeError, + /distinct_each_batch can not be used when additional filters are defined with scope_to/ + end + end end end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb index 943b5744b64..9fdd7bf8adc 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb @@ -45,19 +45,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi end end - context 'when job_class is provided with a batching_scope' do + context 'when job class supports batch scope DSL' do let(:job_class) do - Class.new(described_class) do - def self.batching_scope(relation, job_arguments:) - min_id = job_arguments.first - - relation.where.not(type: 'Project').where('id >= ?', min_id) - end + Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do + job_arguments :min_id + scope_to ->(r) { r.where.not(type: 'Project').where('id >= ?', min_id) } end end - it 'applies the batching scope' do - expect(job_class).to receive(:batching_scope).and_call_original + it 'applies the additional scope' do + expect(job_class).to receive(:generic_instance).and_call_original batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1], job_class: job_class) diff --git a/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb b/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb deleted file mode 100644 index db822f36c21..00000000000 --- a/spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::CopyCiBuildsColumnsToSecurityScans, schema: 20210728174349 do - let(:migration) { described_class.new } - - let_it_be(:namespaces) { table(:namespaces) } - let_it_be(:projects) { table(:projects) } - let_it_be(:ci_pipelines) { table(:ci_pipelines) } - let_it_be(:ci_builds) { table(:ci_builds) } - let_it_be(:security_scans) { table(:security_scans) } - - let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') } - let!(:project1) { projects.create!(namespace_id: namespace.id) } - let!(:project2) { projects.create!(namespace_id: namespace.id) } - let!(:pipeline1) { ci_pipelines.create!(status: "success")} - let!(:pipeline2) { ci_pipelines.create!(status: "success")} - - let!(:build1) { ci_builds.create!(commit_id: pipeline1.id, type: 'Ci::Build', project_id: project1.id) } - let!(:build2) { ci_builds.create!(commit_id: pipeline2.id, type: 'Ci::Build', project_id: project2.id) } - let!(:build3) { ci_builds.create!(commit_id: pipeline1.id, type: 'Ci::Build', project_id: project1.id) } - - let!(:scan1) { security_scans.create!(build_id: build1.id, scan_type: 1) } - let!(:scan2) { security_scans.create!(build_id: build2.id, scan_type: 1) } - let!(:scan3) { security_scans.create!(build_id: build3.id, scan_type: 1) } - - subject { migration.perform(scan1.id, scan2.id) } - - before do - stub_const("#{described_class}::UPDATE_BATCH_SIZE", 2) - end - - it 'copies `project_id`, `commit_id` from `ci_builds` to `security_scans`', :aggregate_failures do - expect(migration).to receive(:mark_job_as_succeeded).with(scan1.id, scan2.id) - - subject - - scan1.reload - expect(scan1.project_id).to eq(project1.id) - expect(scan1.pipeline_id).to eq(pipeline1.id) - - scan2.reload - expect(scan2.project_id).to eq(project2.id) - expect(scan2.pipeline_id).to eq(pipeline2.id) - - scan3.reload - expect(scan3.project_id).to be_nil - expect(scan3.pipeline_id).to be_nil - 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 78bd1afd8d2..9c33100a0b3 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 @@ -16,6 +16,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers) end + let(:job_arguments) { %w(name name_convert_to_text) } let(:copy_job) do described_class.new(start_id: 12, end_id: 20, @@ -23,6 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo batch_column: 'id', sub_batch_size: sub_batch_size, pause_ms: pause_ms, + job_arguments: job_arguments, connection: connection) end @@ -53,32 +55,42 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo SQL end - it 'copies all primary keys in range' do - temporary_column = helpers.convert_to_bigint_column(:id) + context 'primary keys' do + let(:temporary_column) { helpers.convert_to_bigint_column(:id) } + let(:job_arguments) { ['id', temporary_column] } - copy_job.perform('id', temporary_column) + it 'copies all in range' do + copy_job.perform - 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) + 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 end - it 'copies all foreign keys in range' do - temporary_column = helpers.convert_to_bigint_column(:fk) + context 'foreign keys' do + let(:temporary_column) { helpers.convert_to_bigint_column(:fk) } + let(:job_arguments) { ['fk', temporary_column] } - copy_job.perform('fk', temporary_column) + it 'copies all in range' do + copy_job.perform - 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) + 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 end - it 'copies columns with NULLs' do - 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) + context 'columns with NULLs' do + let(:job_arguments) { %w(name name_convert_to_text) } - 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) + it 'copies all in range' do + expect { copy_job.perform } + .to change { test_table.where("name_convert_to_text = 'no name'").count }.from(4).to(1) + + 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) + end end context 'when multiple columns are given' do @@ -87,8 +99,10 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo let(:columns_to_copy_from) { %w[id fk] } let(:columns_to_copy_to) { [id_tmp_column, fk_tmp_column] } + let(:job_arguments) { [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) + copy_job.perform 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) @@ -100,7 +114,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo it 'raises an error' do expect do - copy_job.perform(columns_to_copy_from, columns_to_copy_to) + copy_job.perform end.to raise_error(ArgumentError, 'number of source and destination columns must match') end end @@ -109,7 +123,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo it 'tracks timings of queries' do expect(copy_job.batch_metrics.timings).to be_empty - copy_job.perform('name', 'name_convert_to_text') + copy_job.perform expect(copy_job.batch_metrics.timings[:update_all]).not_to be_empty end @@ -120,7 +134,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo it 'sleeps for the specified time between sub-batches' do expect(copy_job).to receive(:sleep).with(0.005) - copy_job.perform('name', 'name_convert_to_text') + copy_job.perform end context 'when pause_ms value is negative' do @@ -129,7 +143,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo it 'treats it as a 0' do expect(copy_job).to receive(:sleep).with(0) - copy_job.perform('name', 'name_convert_to_text') + copy_job.perform end end end diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb new file mode 100644 index 00000000000..d20eaef3650 --- /dev/null +++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects, + :migration, + schema: 20220722084543 do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + let(:project_statistics_table) { table(:project_statistics) } + let(:issues_table) { table(:issues) } + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'sets `legacy_open_source_license_available` to false only for public projects with no issues and no repo', + :aggregate_failures do + project_with_no_issues_no_repo = create_legacy_license_public_project('project-with-no-issues-no-repo') + project_with_repo = create_legacy_license_public_project('project-with-repo', repo_size: 1) + project_with_issues = create_legacy_license_public_project('project-with-issues', with_issue: true) + project_with_issues_and_repo = + create_legacy_license_public_project('project-with-issues-and-repo', repo_size: 1, with_issue: true) + + queries = ActiveRecord::QueryRecorder.new { perform_migration } + + expect(queries.count).to eq(7) + expect(migrated_attribute(project_with_no_issues_no_repo)).to be_falsey + expect(migrated_attribute(project_with_repo)).to be_truthy + expect(migrated_attribute(project_with_issues)).to be_truthy + expect(migrated_attribute(project_with_issues_and_repo)).to be_truthy + end + + def create_legacy_license_public_project(path, repo_size: 0, with_issue: false) + namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}") + project_namespace = + namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project') + project = projects_table + .create!( + name: path, path: path, namespace_id: namespace.id, + project_namespace_id: project_namespace.id, visibility_level: 20 + ) + + project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size) + issues_table.create!(project_id: project.id) if with_issue + project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true) + + project + end + + def migrated_attribute(project) + project_settings_table.find(project.id).legacy_open_source_license_available + end +end diff --git a/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb new file mode 100644 index 00000000000..0dba1d7c8a2 --- /dev/null +++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForOneMemberNoRepoProjects, + :migration, + schema: 20220721031446 do + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:project_settings_table) { table(:project_settings) } + let(:project_statistics_table) { table(:project_statistics) } + let(:users_table) { table(:users) } + let(:project_authorizations_table) { table(:project_authorizations) } + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ActiveRecord::Base.connection) + .perform + end + + it 'sets `legacy_open_source_license_available` to false only for public projects with 1 member and no repo', + :aggregate_failures do + project_with_no_repo_one_member = create_legacy_license_public_project('project-with-one-member-no-repo') + project_with_repo_one_member = create_legacy_license_public_project('project-with-repo', repo_size: 1) + project_with_no_repo_two_members = create_legacy_license_public_project('project-with-two-members', members: 2) + project_with_repo_two_members = + create_legacy_license_public_project('project-with-repo', repo_size: 1, members: 2) + + queries = ActiveRecord::QueryRecorder.new { perform_migration } + + expect(queries.count).to eq(7) + expect(migrated_attribute(project_with_no_repo_one_member)).to be_falsey + expect(migrated_attribute(project_with_repo_one_member)).to be_truthy + expect(migrated_attribute(project_with_no_repo_two_members)).to be_truthy + expect(migrated_attribute(project_with_repo_two_members)).to be_truthy + end + + def create_legacy_license_public_project(path, repo_size: 0, members: 1) + namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}") + project_namespace = + namespaces_table.create!(name: "-project-namespace-#{path}", path: "project-namespace-#{path}", type: 'Project') + project = projects_table + .create!( + name: path, path: path, namespace_id: namespace.id, + project_namespace_id: project_namespace.id, visibility_level: 20 + ) + + members.times do |member_id| + user = users_table.create!(email: "user#{member_id}-project-#{project.id}@gitlab.com", projects_limit: 100) + project_authorizations_table.create!(project_id: project.id, user_id: user.id, access_level: 50) + end + project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size) + project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true) + + project + end + + def migrated_attribute(project) + project_settings_table.find(project.id).legacy_open_source_license_available + end +end diff --git a/spec/lib/gitlab/background_migration/drop_invalid_security_findings_spec.rb b/spec/lib/gitlab/background_migration/drop_invalid_security_findings_spec.rb index 7cc64889fc8..5fdd8683d06 100644 --- a/spec/lib/gitlab/background_migration/drop_invalid_security_findings_spec.rb +++ b/spec/lib/gitlab/background_migration/drop_invalid_security_findings_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::DropInvalidSecurityFindings, schema: 20211108211434 do +RSpec.describe Gitlab::BackgroundMigration::DropInvalidSecurityFindings, :suppress_gitlab_schemas_validate_connection, + schema: 20211108211434 do let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user', type: Namespaces::UserNamespace.sti_name) } let(:project) { table(:projects).create!(namespace_id: namespace.id) } 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 65d55f85a98..51a09d50a19 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 @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::ExtractProjectTopicsIntoSeparateTable, schema: 20210730104800 do +RSpec.describe Gitlab::BackgroundMigration::ExtractProjectTopicsIntoSeparateTable, + :suppress_gitlab_schemas_validate_connection, schema: 20210730104800 do it 'correctly extracts project topics into separate table' do namespaces = table(:namespaces) projects = table(:projects) diff --git a/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb b/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb index 5e2f32c54be..5495d786a48 100644 --- a/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::MigrateProjectTaggingsContextFromTagsToTopics, schema: 20210511095658 do +RSpec.describe Gitlab::BackgroundMigration::MigrateProjectTaggingsContextFromTagsToTopics, + :suppress_gitlab_schemas_validate_connection, schema: 20210511095658 do it 'correctly migrates project taggings context from tags to topics' do taggings = table(:taggings) 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 e38edfc3643..2f0eef3c399 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 @@ -2,12 +2,13 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, :migration, schema: 20220223112304 do +RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, + :suppress_gitlab_schemas_validate_connection, migration: :gitlab_ci, schema: 20220223112304 do let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:ci_runners) { table(:ci_runners, database: :ci) } - let(:ci_pipelines) { table(:ci_pipelines, database: :ci) } - let(:ci_builds) { table(:ci_builds, database: :ci) } + let(:ci_runners) { table(:ci_runners) } + let(:ci_pipelines) { table(:ci_pipelines) } + let(:ci_builds) { table(:ci_builds) } subject { described_class.new } @@ -20,7 +21,9 @@ RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, :mi end after do - helpers.add_concurrent_foreign_key(:ci_builds, :ci_runners, column: :runner_id, on_delete: :nullify, validate: false) + helpers.add_concurrent_foreign_key( + :ci_builds, :ci_runners, column: :runner_id, on_delete: :nullify, validate: false + ) end describe '#perform' do diff --git a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb index 2ad561ead87..bff803e2035 100644 --- a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb +++ b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb @@ -5,199 +5,211 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNamespaces, :migration, schema: 20220326161803 do include MigrationsHelpers - context 'when migrating data', :aggregate_failures do - let(:projects) { table(:projects) } - let(:namespaces) { table(:namespaces) } + RSpec.shared_examples 'backfills project namespaces' do + context 'when migrating data', :aggregate_failures do + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } - let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') } - let(:parent_group2) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') } + let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') } + let(:parent_group2) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') } - let(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) } - let(:parent_group2_project) { projects.create!(name: 'parent_group2_project', path: 'parent_group2_project', namespace_id: parent_group2.id, visibility_level: 20) } + let(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) } + let(:parent_group2_project) { projects.create!(name: 'parent_group2_project', path: 'parent_group2_project', namespace_id: parent_group2.id, visibility_level: 20) } - let(:child_nodes_count) { 2 } - let(:tree_depth) { 3 } + let(:child_nodes_count) { 2 } + let(:tree_depth) { 3 } - let(:backfilled_namespace) { nil } + let(:backfilled_namespace) { nil } - before do - BackfillProjectNamespaces::TreeGenerator.new(namespaces, projects, [parent_group1, parent_group2], child_nodes_count, tree_depth).build_tree - end - - describe '#up' do - shared_examples 'back-fill project namespaces' do - it 'back-fills all project namespaces' do - start_id = ::Project.minimum(:id) - end_id = ::Project.maximum(:id) - projects_count = ::Project.count - batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil - project_namespaces_count = ::Namespace.where(type: 'Project').count - migration = described_class.new - - expect(projects_count).not_to eq(project_namespaces_count) - expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original - expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original - expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original - - expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count) - - expect(projects_count).to eq(::Namespace.where(type: 'Project').count) - check_projects_in_sync_with(Namespace.where(type: 'Project')) - end - - context 'when passing specific group as parameter' do - let(:backfilled_namespace) { parent_group1 } - - it 'back-fills project namespaces for the specified group hierarchy' do - backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects - start_id = backfilled_namespace_projects.minimum(:id) - end_id = backfilled_namespace_projects.maximum(:id) - group_projects_count = backfilled_namespace_projects.count - batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil - project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) + before do + BackfillProjectNamespaces::TreeGenerator.new(namespaces, projects, [parent_group1, parent_group2], child_nodes_count, tree_depth).build_tree + end + describe '#up' do + shared_examples 'back-fill project namespaces' do + it 'back-fills all project namespaces' do + start_id = ::Project.minimum(:id) + end_id = ::Project.maximum(:id) + projects_count = ::Project.count + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil + project_namespaces_count = ::Namespace.where(type: 'Project').count migration = described_class.new - expect(project_namespaces_in_hierarchy.count).to eq(0) + expect(projects_count).not_to eq(project_namespaces_count) expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original - expect(group_projects_count).to eq(14) - expect(project_namespaces_in_hierarchy.count).to eq(0) - - migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'up') + expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count) - expect(project_namespaces_in_hierarchy.count).to eq(14) - check_projects_in_sync_with(project_namespaces_in_hierarchy) + expect(projects_count).to eq(::Namespace.where(type: 'Project').count) + check_projects_in_sync_with(Namespace.where(type: 'Project')) end - end - context 'when projects already have project namespaces' do - before do - hierarchy1_projects = base_ancestor(parent_group1).first.all_projects - start_id = hierarchy1_projects.minimum(:id) - end_id = hierarchy1_projects.maximum(:id) + context 'when passing specific group as parameter' do + let(:backfilled_namespace) { parent_group1 } - described_class.new.perform(start_id, end_id, nil, nil, nil, nil, parent_group1.id, 'up') - end + it 'back-fills project namespaces for the specified group hierarchy' do + backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects + start_id = backfilled_namespace_projects.minimum(:id) + end_id = backfilled_namespace_projects.maximum(:id) + group_projects_count = backfilled_namespace_projects.count + batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil + project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) - it 'does not duplicate project namespaces' do - # check there are already some project namespaces but not for all - projects_count = ::Project.count - start_id = ::Project.minimum(:id) - end_id = ::Project.maximum(:id) - batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil - project_namespaces = ::Namespace.where(type: 'Project') - migration = described_class.new + migration = described_class.new - expect(project_namespaces_in_hierarchy(base_ancestor(parent_group1)).count).to be >= 14 - expect(project_namespaces_in_hierarchy(base_ancestor(parent_group2)).count).to eq(0) - expect(projects_count).not_to eq(project_namespaces.count) + expect(project_namespaces_in_hierarchy.count).to eq(0) + expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original + expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original + expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original - # run migration again to test we do not generate extra project namespaces - expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original - expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original - expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original + expect(group_projects_count).to eq(14) + expect(project_namespaces_in_hierarchy.count).to eq(0) - expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(project_namespaces, :count).by(14) + migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'up') - expect(projects_count).to eq(project_namespaces.count) + expect(project_namespaces_in_hierarchy.count).to eq(14) + check_projects_in_sync_with(project_namespaces_in_hierarchy) + end end - end - end - it 'checks no project namespaces exist in the defined hierarchies' do - hierarchy1_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group1)) - hierarchy2_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group2)) - hierarchy1_projects_count = base_ancestor(parent_group1).first.all_projects.count - hierarchy2_projects_count = base_ancestor(parent_group2).first.all_projects.count + context 'when projects already have project namespaces' do + before do + hierarchy1_projects = base_ancestor(parent_group1).first.all_projects + start_id = hierarchy1_projects.minimum(:id) + end_id = hierarchy1_projects.maximum(:id) + + described_class.new.perform(start_id, end_id, nil, nil, nil, nil, parent_group1.id, 'up') + end + + it 'does not duplicate project namespaces' do + # check there are already some project namespaces but not for all + projects_count = ::Project.count + start_id = ::Project.minimum(:id) + end_id = ::Project.maximum(:id) + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil + project_namespaces = ::Namespace.where(type: 'Project') + migration = described_class.new + + expect(project_namespaces_in_hierarchy(base_ancestor(parent_group1)).count).to be >= 14 + expect(project_namespaces_in_hierarchy(base_ancestor(parent_group2)).count).to eq(0) + expect(projects_count).not_to eq(project_namespaces.count) + + # run migration again to test we do not generate extra project namespaces + expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original + expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original + expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original + + expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(project_namespaces, :count).by(14) + + expect(projects_count).to eq(project_namespaces.count) + end + end + end - expect(hierarchy1_project_namespaces).to be_empty - expect(hierarchy2_project_namespaces).to be_empty - expect(hierarchy1_projects_count).to eq(14) - expect(hierarchy2_projects_count).to eq(14) - end + it 'checks no project namespaces exist in the defined hierarchies' do + hierarchy1_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group1)) + hierarchy2_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group2)) + hierarchy1_projects_count = base_ancestor(parent_group1).first.all_projects.count + hierarchy2_projects_count = base_ancestor(parent_group2).first.all_projects.count - context 'back-fill project namespaces in a single batch' do - it_behaves_like 'back-fill project namespaces' - end + expect(hierarchy1_project_namespaces).to be_empty + expect(hierarchy2_project_namespaces).to be_empty + expect(hierarchy1_projects_count).to eq(14) + expect(hierarchy2_projects_count).to eq(14) + end - context 'back-fill project namespaces in batches' do - before do - stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) + context 'back-fill project namespaces in a single batch' do + it_behaves_like 'back-fill project namespaces' end - it_behaves_like 'back-fill project namespaces' - end - end + context 'back-fill project namespaces in batches' do + before do + stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) + end - describe '#down' do - before do - start_id = ::Project.minimum(:id) - end_id = ::Project.maximum(:id) - # back-fill first - described_class.new.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') + it_behaves_like 'back-fill project namespaces' + end end - shared_examples 'cleanup project namespaces' do - it 'removes project namespaces' do - projects_count = ::Project.count + describe '#down' do + before do start_id = ::Project.minimum(:id) end_id = ::Project.maximum(:id) - migration = described_class.new - batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil + # back-fill first + described_class.new.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') + end - expect(projects_count).to be > 0 - expect(projects_count).to eq(::Namespace.where(type: 'Project').count) + shared_examples 'cleanup project namespaces' do + it 'removes project namespaces' do + projects_count = ::Project.count + start_id = ::Project.minimum(:id) + end_id = ::Project.maximum(:id) + migration = described_class.new + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil - expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original - expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original + expect(projects_count).to be > 0 + expect(projects_count).to eq(::Namespace.where(type: 'Project').count) - migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'down') + expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original + expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original - expect(::Project.count).to be > 0 - expect(::Namespace.where(type: 'Project').count).to eq(0) - end + migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'down') + + expect(::Project.count).to be > 0 + expect(::Namespace.where(type: 'Project').count).to eq(0) + end - context 'when passing specific group as parameter' do - let(:backfilled_namespace) { parent_group1 } + context 'when passing specific group as parameter' do + let(:backfilled_namespace) { parent_group1 } - it 'removes project namespaces only for the specific group hierarchy' do - backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects - start_id = backfilled_namespace_projects.minimum(:id) - end_id = backfilled_namespace_projects.maximum(:id) - group_projects_count = backfilled_namespace_projects.count - batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil - project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) - migration = described_class.new + it 'removes project namespaces only for the specific group hierarchy' do + backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects + start_id = backfilled_namespace_projects.minimum(:id) + end_id = backfilled_namespace_projects.maximum(:id) + group_projects_count = backfilled_namespace_projects.count + batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil + project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) + migration = described_class.new - expect(project_namespaces_in_hierarchy.count).to eq(14) - expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original - expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original + expect(project_namespaces_in_hierarchy.count).to eq(14) + expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original + expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original - migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'down') + migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'down') - expect(::Namespace.where(type: 'Project').count).to be > 0 - expect(project_namespaces_in_hierarchy.count).to eq(0) + expect(::Namespace.where(type: 'Project').count).to be > 0 + expect(project_namespaces_in_hierarchy.count).to eq(0) + end end end - end - context 'cleanup project namespaces in a single batch' do - it_behaves_like 'cleanup project namespaces' - end - - context 'cleanup project namespaces in batches' do - before do - stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) + context 'cleanup project namespaces in a single batch' do + it_behaves_like 'cleanup project namespaces' end - it_behaves_like 'cleanup project namespaces' + context 'cleanup project namespaces in batches' do + before do + stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) + end + + it_behaves_like 'cleanup project namespaces' + end end end end + it_behaves_like 'backfills project namespaces' + + context 'when namespaces.id is bigint' do + before do + namespaces.connection.execute("ALTER TABLE namespaces ALTER COLUMN id TYPE bigint") + end + + it_behaves_like 'backfills project namespaces' + end + def base_ancestor(ancestor) ::Namespace.where(id: ancestor.id) end @@ -209,7 +221,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa def check_projects_in_sync_with(namespaces) project_namespaces_attrs = namespaces.order(:id).pluck(:id, :name, :path, :parent_id, :visibility_level, :shared_runners_enabled) corresponding_projects_attrs = Project.where(project_namespace_id: project_namespaces_attrs.map(&:first)) - .order(:project_namespace_id).pluck(:project_namespace_id, :name, :path, :namespace_id, :visibility_level, :shared_runners_enabled) + .order(:project_namespace_id).pluck(:project_namespace_id, :name, :path, :namespace_id, :visibility_level, :shared_runners_enabled) expect(project_namespaces_attrs).to eq(corresponding_projects_attrs) end diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb index 8d71b117107..a609227be05 100644 --- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb +++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb @@ -20,7 +20,7 @@ def create_background_migration_job(ids, status) ) end -RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, schema: 20211124132705 do +RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, :suppress_gitlab_schemas_validate_connection, schema: 20211124132705 do let(:background_migration_jobs) { table(:background_migration_jobs) } let(:pending_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']) } let(:succeeded_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']) } diff --git a/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb index 8cdcec9621c..eabc012f98b 100644 --- a/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::RemoveAllTraceExpirationDates, :migration, schema: 20220131000001 do +RSpec.describe Gitlab::BackgroundMigration::RemoveAllTraceExpirationDates, :migration, + :suppress_gitlab_schemas_validate_connection, schema: 20220131000001 do subject(:perform) { migration.perform(1, 99) } let(:migration) { described_class.new } diff --git a/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb index 07cff32304e..33ad74fbee8 100644 --- a/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings, :migration, schema: 20220326161803 do +RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings, :migration, + :suppress_gitlab_schemas_validate_connection, schema: 20220326161803 do let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } let(:users) { table(:users) } let(:user) { create_user! } diff --git a/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb b/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb index 035ea6eadcf..e9f73672144 100644 --- a/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/set_legacy_open_source_license_available_for_non_public_projects_spec.rb @@ -4,14 +4,14 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableForNonPublicProjects, :migration, - schema: 20220520040416 do + schema: 20220722110026 do let(:namespaces_table) { table(:namespaces) } let(:projects_table) { table(:projects) } let(:project_settings_table) { table(:project_settings) } subject(:perform_migration) do - described_class.new(start_id: 1, - end_id: 30, + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), batch_table: :projects, batch_column: :id, sub_batch_size: 2, @@ -20,35 +20,34 @@ RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableF .perform end - let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } } + it 'sets `legacy_open_source_license_available` attribute to false for non-public projects', :aggregate_failures do + private_project = create_legacy_license_project('private-project', visibility_level: 0) + internal_project = create_legacy_license_project('internal-project', visibility_level: 10) + public_project = create_legacy_license_project('public-project', visibility_level: 20) - before do - namespaces_table.create!(id: 1, name: 'namespace', path: 'namespace-path-1') - namespaces_table.create!(id: 2, name: 'namespace', path: 'namespace-path-2', type: 'Project') - namespaces_table.create!(id: 3, name: 'namespace', path: 'namespace-path-3', type: 'Project') - namespaces_table.create!(id: 4, name: 'namespace', path: 'namespace-path-4', type: 'Project') + queries = ActiveRecord::QueryRecorder.new { perform_migration } - projects_table - .create!(id: 11, name: 'proj-1', path: 'path-1', namespace_id: 1, project_namespace_id: 2, visibility_level: 0) - projects_table - .create!(id: 12, name: 'proj-2', path: 'path-2', namespace_id: 1, project_namespace_id: 3, visibility_level: 10) - projects_table - .create!(id: 13, name: 'proj-3', path: 'path-3', namespace_id: 1, project_namespace_id: 4, visibility_level: 20) + expect(queries.count).to eq(5) - project_settings_table.create!(project_id: 11, legacy_open_source_license_available: true) - project_settings_table.create!(project_id: 12, legacy_open_source_license_available: true) - project_settings_table.create!(project_id: 13, legacy_open_source_license_available: true) + expect(migrated_attribute(private_project)).to be_falsey + expect(migrated_attribute(internal_project)).to be_falsey + expect(migrated_attribute(public_project)).to be_truthy end - it 'sets `legacy_open_source_license_available` attribute to false for non-public projects', :aggregate_failures do - expect(queries.count).to eq(3) - - expect(migrated_attribute(11)).to be_falsey - expect(migrated_attribute(12)).to be_falsey - expect(migrated_attribute(13)).to be_truthy + def create_legacy_license_project(path, visibility_level:) + namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}") + project_namespace = namespaces_table.create!(name: "project-namespace-#{path}", path: path, type: 'Project') + project = projects_table.create!(name: path, + path: path, + namespace_id: namespace.id, + project_namespace_id: project_namespace.id, + visibility_level: visibility_level) + project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true) + + project end - def migrated_attribute(project_id) - project_settings_table.find(project_id).legacy_open_source_license_available + def migrated_attribute(project) + project_settings_table.find(project.id).legacy_open_source_license_available end end diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb index b96d3f7f0b5..c090c1df424 100644 --- a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb +++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb @@ -2,10 +2,26 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl, schema: 20210421163509 do - let(:services_table) { table(:services) } - let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') } - let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') } +RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do + let(:integrations_table) { table(:integrations) } + let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') } + let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') } + let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') } + + let(:table_name) { :jira_tracker_data } + let(:batch_column) { :id } + let(:sub_batch_size) { 1 } + let(:pause_ms) { 0 } + let(:migration) do + described_class.new(start_id: 1, end_id: 10, + batch_table: table_name, batch_column: batch_column, + sub_batch_size: sub_batch_size, pause_ms: pause_ms, + connection: ApplicationRecord.connection) + end + + subject(:perform_migration) do + migration.perform + end before do jira_tracker_data = Class.new(ApplicationRecord) do @@ -27,18 +43,21 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeB end stub_const('JiraTrackerData', jira_tracker_data) - end - let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) } - let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) } + stub_const('UNKNOWN', 0) + stub_const('SERVER', 1) + stub_const('CLOUD', 2) + end - subject { described_class.new.perform(tracker_data_cloud.id, tracker_data_server.id) } + let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, integration_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: UNKNOWN) } + let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, integration_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: UNKNOWN) } + let!(:tracker_data_unknown) { JiraTrackerData.create!(id: 3, integration_id: service_jira_unknown.id, url: "", deployment_type: UNKNOWN) } it "changes unknown deployment_types based on URL" do - expect(JiraTrackerData.pluck(:deployment_type)).to eq([0, 0]) + expect(JiraTrackerData.pluck(:deployment_type)).to match_array([UNKNOWN, UNKNOWN, UNKNOWN]) - subject + perform_migration - expect(JiraTrackerData.pluck(:deployment_type)).to eq([2, 1]) + expect(JiraTrackerData.order(:id).pluck(:deployment_type)).to match_array([CLOUD, SERVER, UNKNOWN]) end end diff --git a/spec/lib/gitlab/background_task_spec.rb b/spec/lib/gitlab/background_task_spec.rb new file mode 100644 index 00000000000..102556b6b2f --- /dev/null +++ b/spec/lib/gitlab/background_task_spec.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +# We need to capture task state from a closure, which requires instance variables. +# rubocop: disable RSpec/InstanceVariable +RSpec.describe Gitlab::BackgroundTask do + let(:options) { {} } + let(:task) do + proc do + @task_run = true + @task_thread = Thread.current + end + end + + subject(:background_task) { described_class.new(task, **options) } + + def expect_condition + Timeout.timeout(3) do + sleep 0.1 until yield + end + end + + context 'when stopped' do + it 'is not running' do + expect(background_task).not_to be_running + end + + describe '#start' do + it 'runs the given task on a background thread' do + test_thread = Thread.current + + background_task.start + + expect_condition { @task_run == true } + expect_condition { @task_thread != test_thread } + expect(background_task).to be_running + end + + it 'returns self' do + expect(background_task.start).to be(background_task) + end + + context 'when installing exit handler' do + it 'stops a running background task' do + expect(background_task).to receive(:at_exit).and_yield + + background_task.start + + expect(background_task).not_to be_running + end + end + + context 'when task responds to start' do + let(:task_class) do + Struct.new(:started, :start_retval, :run) do + def start + self.started = true + self.start_retval + end + + def call + self.run = true + end + end + end + + let(:task) { task_class.new } + + it 'calls start' do + background_task.start + + expect_condition { task.started == true } + end + + context 'when start returns true' do + it 'runs the task' do + task.start_retval = true + + background_task.start + + expect_condition { task.run == true } + end + end + + context 'when start returns false' do + it 'does not run the task' do + task.start_retval = false + + background_task.start + + expect_condition { task.run.nil? } + end + end + end + + context 'when synchronous is set to true' do + let(:options) { { synchronous: true } } + + it 'calls join on the thread' do + # Thread has to be run in a block, expect_next_instance_of does not support this. + allow_any_instance_of(Thread).to receive(:join) # rubocop:disable RSpec/AnyInstanceOf + + background_task.start + + expect_condition { @task_run == true } + expect(@task_thread).to have_received(:join) + end + end + end + + describe '#stop' do + it 'is a no-op' do + expect { background_task.stop }.not_to change { subject.running? } + expect_condition { @task_run.nil? } + end + end + end + + context 'when running' do + before do + background_task.start + end + + describe '#start' do + it 'raises an error' do + expect { background_task.start }.to raise_error(described_class::AlreadyStartedError) + end + end + + describe '#stop' do + it 'stops running' do + expect { background_task.stop }.to change { subject.running? }.from(true).to(false) + end + + context 'when task responds to stop' do + let(:task_class) do + Struct.new(:stopped, :call) do + def stop + self.stopped = true + end + end + end + + let(:task) { task_class.new } + + it 'calls stop' do + background_task.stop + + expect_condition { task.stopped == true } + end + end + + context 'when task stop raises an error' do + let(:error) { RuntimeError.new('task error') } + let(:options) { { name: 'test_background_task' } } + + let(:task_class) do + Struct.new(:call, :error, keyword_init: true) do + def stop + raise error + end + end + end + + let(:task) { task_class.new(error: error) } + + it 'stops gracefully' do + expect { background_task.stop }.not_to raise_error + expect(background_task).not_to be_running + end + + it 'reports the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + error, { extra: { reported_by: 'test_background_task' } } + ) + + background_task.stop + end + end + end + + context 'when task run raises exception' do + let(:error) { RuntimeError.new('task error') } + let(:options) { { name: 'test_background_task' } } + let(:task) do + proc do + @task_run = true + raise error + end + end + + it 'stops gracefully' do + expect_condition { @task_run == true } + expect { background_task.stop }.not_to raise_error + expect(background_task).not_to be_running + end + + it 'reports the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + error, { extra: { reported_by: 'test_background_task' } } + ) + + background_task.stop + end + end + end +end +# rubocop: enable RSpec/InstanceVariable diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index d29447ee376..becfdced5fb 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -54,16 +54,16 @@ RSpec.describe ::Gitlab::BareRepositoryImport::Repository do end context 'hashed storage' do - let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" } let(:root_path) { TestEnv.repos_path } let(:repo_path) { File.join(root_path, "#{hashed_path}.git") } let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") } let(:raw_repository) { Gitlab::Git::Repository.new('default', "#{hashed_path}.git", nil, nil) } + let(:full_path) { 'to/repo' } before do raw_repository.create_repository - raw_repository.set_full_path(full_path: 'to/repo') + raw_repository.set_full_path(full_path: full_path) if full_path end after do @@ -95,16 +95,17 @@ RSpec.describe ::Gitlab::BareRepositoryImport::Repository do expect(subject).not_to be_processable end - it 'returns false when group and project name are missing' do - repository = Rugged::Repository.new(repo_path) - repository.config.delete('gitlab.fullpath') - - expect(subject).not_to be_processable - end - it 'returns true when group path and project name are present' do expect(subject).to be_processable end + + context 'group and project name are missing' do + let(:full_path) { nil } + + it 'returns false' do + expect(subject).not_to be_processable + end + end end describe '#project_full_path' do diff --git a/spec/lib/gitlab/batch_pop_queueing_spec.rb b/spec/lib/gitlab/batch_pop_queueing_spec.rb index 41efc5417e4..5af78ddabe7 100644 --- a/spec/lib/gitlab/batch_pop_queueing_spec.rb +++ b/spec/lib/gitlab/batch_pop_queueing_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Gitlab::BatchPopQueueing do context 'when the queue key does not exist in Redis' do before do - allow(queue).to receive(:enqueue) { } + allow(queue).to receive(:enqueue) {} end it 'yields empty array' do diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb index 906c02d54db..8d5702a6b27 100644 --- a/spec/lib/gitlab/chat_name_token_spec.rb +++ b/spec/lib/gitlab/chat_name_token_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::ChatNameToken do context 'when using unknown token' do - let(:token) { } + let(:token) {} subject { described_class.new(token).get } diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 27c2b005a93..30359a7170f 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -210,8 +210,8 @@ RSpec.describe Gitlab::Ci::Ansi2html do let(:section_start_time) { Time.new(2017, 9, 20).utc } let(:section_duration) { 3.seconds } let(:section_end_time) { section_start_time + section_duration } - let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} - let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} + let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K" } + let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K" } let(:section_start_html) do '<div class="section-start"' \ " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{class_name(section_name)}\"" \ @@ -258,13 +258,13 @@ RSpec.describe Gitlab::Ci::Ansi2html do it_behaves_like 'a legit section' context 'section name includes $' do - let(:section_name) { 'my_$ection'} + let(:section_name) { 'my_$ection' } it_behaves_like 'forbidden char in section_name' end context 'section name includes <' do - let(:section_name) { '<a_tag>'} + let(:section_name) { '<a_tag>' } it_behaves_like 'forbidden char in section_name' end diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb index f9d23ff97bc..4b3b049176f 100644 --- a/spec/lib/gitlab/ci/ansi2json_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json_spec.rb @@ -78,8 +78,8 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:section_duration) { 63.seconds } let(:section_start_time) { Time.new(2019, 9, 17).utc } let(:section_end_time) { section_start_time + section_duration } - let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} - let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} + let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K" } + let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K" } it 'marks the first line of the section as header' do expect(convert_json("Hello#{section_start}world!")).to eq([ @@ -258,8 +258,8 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:nested_section_duration) { 2.seconds } let(:nested_section_start_time) { Time.new(2019, 9, 17).utc } let(:nested_section_end_time) { nested_section_start_time + nested_section_duration } - let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K"} - let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K"} + let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K" } + let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K" } it 'adds multiple sections to the lines inside the nested section' do trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world" @@ -342,7 +342,7 @@ RSpec.describe Gitlab::Ci::Ansi2json do end context 'with section options' do - let(:option_section_start) { "section_start:#{section_start_time.to_i}:#{section_name}[collapsed=true,unused_option=123]\r\033[0K"} + let(:option_section_start) { "section_start:#{section_start_time.to_i}:#{section_name}[collapsed=true,unused_option=123]\r\033[0K" } it 'provides section options when set' do trace = "#{option_section_start}hello#{section_end}" @@ -463,8 +463,8 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:section_duration) { 63.seconds } let(:section_start_time) { Time.new(2019, 9, 17).utc } let(:section_end_time) { section_start_time + section_duration } - let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} - let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} + let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K" } + let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K" } context 'with split section body' do let(:pre_text) { "#{section_start}this is a header\nand " } diff --git a/spec/lib/gitlab/ci/artifacts/logger_spec.rb b/spec/lib/gitlab/ci/artifacts/logger_spec.rb new file mode 100644 index 00000000000..7753cb0d25e --- /dev/null +++ b/spec/lib/gitlab/ci/artifacts/logger_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Artifacts::Logger do + before do + Gitlab::ApplicationContext.push(feature_category: 'test', caller_id: 'caller') + end + + describe '.log_created' do + it 'logs information about created artifact' do + artifact = create(:ci_job_artifact, :archive) + + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Artifact created', + job_artifact_id: artifact.id, + size: artifact.size, + type: artifact.file_type, + build_id: artifact.job_id, + project_id: artifact.project_id, + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + + described_class.log_created(artifact) + end + end + + describe '.log_deleted' do + it 'logs information about deleted artifacts' do + artifact_1 = create(:ci_job_artifact, :archive, :expired) + artifact_2 = create(:ci_job_artifact, :archive) + artifacts = [artifact_1, artifact_2] + method = 'Foo#method' + + artifacts.each do |artifact| + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Artifact deleted', + job_artifact_id: artifact.id, + expire_at: artifact.expire_at, + size: artifact.size, + type: artifact.file_type, + build_id: artifact.job_id, + project_id: artifact.project_id, + method: method, + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + end + + described_class.log_deleted(artifacts, method) + end + end +end diff --git a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb index 0ce76285b03..39e440f09e1 100644 --- a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb @@ -5,6 +5,25 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do let(:metrics) { described_class.new } + describe '.build_completed_report_type_counter' do + context 'when incrementing by more than one' do + let(:sast_counter) { described_class.send(:build_completed_report_type_counter, :sast) } + let(:dast_counter) { described_class.send(:build_completed_report_type_counter, :dast) } + + it 'increments a single counter' do + [dast_counter, sast_counter].each do |counter| + counter.increment(status: 'success') + counter.increment(status: 'success') + counter.increment(status: 'failed') + + expect(counter.get(status: 'success')).to eq 2.0 + expect(counter.get(status: 'failed')).to eq 1.0 + expect(counter.values.count).to eq 2 + end + end + end + end + describe '#increment_destroyed_artifacts' do context 'when incrementing by more than one' do let(:counter) { metrics.send(:destroyed_artifacts_counter) } diff --git a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb b/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb new file mode 100644 index 00000000000..2c236ba3726 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Artifacts::Adapters::ZipStream do + let(:file_name) { 'single_file.zip' } + let(:fixture_path) { "lib/gitlab/ci/build/artifacts/adapters/zip_stream/#{file_name}" } + let(:stream) { File.open(expand_fixture_path(fixture_path), 'rb') } + + describe '#initialize' do + it 'initializes when stream is passed' do + expect { described_class.new(stream) }.not_to raise_error + end + + context 'when stream is not passed' do + let(:stream) { nil } + + it 'raises an error' do + expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError) + end + end + end + + describe '#each_blob' do + let(:adapter) { described_class.new(stream) } + + context 'when stream is a zip file' do + it 'iterates file content when zip file contains one file' do + expect { |b| adapter.each_blob(&b) } + .to yield_with_args("file 1 content\n") + end + + context 'when zip file contains multiple files' do + let(:file_name) { 'multiple_files.zip' } + + it 'iterates content of all files' do + expect { |b| adapter.each_blob(&b) } + .to yield_successive_args("file 1 content\n", "file 2 content\n") + end + end + + context 'when zip file includes files in a directory' do + let(:file_name) { 'with_directory.zip' } + + it 'iterates contents from files only' do + expect { |b| adapter.each_blob(&b) } + .to yield_successive_args("file 1 content\n", "file 2 content\n") + end + end + + context 'when zip contains a file which decompresses beyond the size limit' do + let(:file_name) { '200_mb_decompressed.zip' } + + it 'does not read the file' do + expect { |b| adapter.each_blob(&b) }.not_to yield_control + end + end + + context 'when the zip contains too many files' do + let(:file_name) { '100_files.zip' } + + it 'stops processing when the limit is reached' do + expect { |b| adapter.each_blob(&b) } + .to yield_control.exactly(described_class::MAX_FILES_PROCESSED).times + end + end + + context 'when stream is a zipbomb' do + let(:file_name) { 'zipbomb.zip' } + + it 'does not read the file' do + expect { |b| adapter.each_blob(&b) }.not_to yield_control + end + end + end + + context 'when stream is not a zip file' do + let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') } + + it 'does not yield any data' do + expect { |b| adapter.each_blob(&b) }.not_to yield_control + expect { adapter.each_blob { |b| b } }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index c8ace28108b..7b35c9ba483 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -67,6 +67,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do subject { |example| path(example).children } it { is_expected.to all(be_an_instance_of(described_class)) } + it do is_expected.to contain_exactly entry('path/dir_1/file_1'), entry('path/dir_1/file_b'), @@ -79,6 +80,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do it { is_expected.to all(be_file) } it { is_expected.to all(be_an_instance_of(described_class)) } + it do is_expected.to contain_exactly entry('path/dir_1/file_1'), entry('path/dir_1/file_b') @@ -99,6 +101,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do it { is_expected.to all(be_directory) } it { is_expected.to all(be_an_instance_of(described_class)) } + it do is_expected.to contain_exactly entry('path/dir_1/subdir/'), entry('path/') diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb index 94c14cfa479..baabab73ea2 100644 --- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do end context 'kubernetes namespace does not exist' do - let(:namespace_builder) { double(execute: kubernetes_namespace)} + let(:namespace_builder) { double(execute: kubernetes_namespace) } before do allow(Clusters::KubernetesNamespaceFinder).to receive(:new) diff --git a/spec/lib/gitlab/ci/build/releaser_spec.rb b/spec/lib/gitlab/ci/build/releaser_spec.rb index 435f70e9ac5..ffa7073818a 100644 --- a/spec/lib/gitlab/ci/build/releaser_spec.rb +++ b/spec/lib/gitlab/ci/build/releaser_spec.rb @@ -13,6 +13,7 @@ RSpec.describe Gitlab::Ci::Build::Releaser do name: 'Release $CI_COMMIT_SHA', description: 'Created using the release-cli $EXTRA_DESCRIPTION', tag_name: 'release-$CI_COMMIT_SHA', + tag_message: 'Annotated tag message', ref: '$CI_COMMIT_SHA', milestones: %w[m1 m2 m3], released_at: '2020-07-15T08:00:00Z', @@ -27,7 +28,7 @@ RSpec.describe Gitlab::Ci::Build::Releaser do end it 'generates the script' do - expect(subject).to eq(['release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "release-$CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" --assets-link "{\"name\":\"asset1\",\"url\":\"https://example.com/assets/1\",\"link_type\":\"other\",\"filepath\":\"/pretty/asset/1\"}" --assets-link "{\"name\":\"asset2\",\"url\":\"https://example.com/assets/2\"}"']) + expect(subject).to eq(['release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "release-$CI_COMMIT_SHA" --tag-message "Annotated tag message" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" --assets-link "{\"name\":\"asset1\",\"url\":\"https://example.com/assets/1\",\"link_type\":\"other\",\"filepath\":\"/pretty/asset/1\"}" --assets-link "{\"name\":\"asset2\",\"url\":\"https://example.com/assets/2\"}"']) end end @@ -39,6 +40,7 @@ RSpec.describe Gitlab::Ci::Build::Releaser do :name | 'Release $CI_COMMIT_SHA' | 'release-cli create --name "Release $CI_COMMIT_SHA"' :description | 'Release-cli $EXTRA_DESCRIPTION' | 'release-cli create --description "Release-cli $EXTRA_DESCRIPTION"' :tag_name | 'release-$CI_COMMIT_SHA' | 'release-cli create --tag-name "release-$CI_COMMIT_SHA"' + :tag_message | 'Annotated tag message' | 'release-cli create --tag-message "Annotated tag message"' :ref | '$CI_COMMIT_SHA' | 'release-cli create --ref "$CI_COMMIT_SHA"' :milestones | %w[m1 m2 m3] | 'release-cli create --milestone "m1" --milestone "m2" --milestone "m3"' :released_at | '2020-07-15T08:00:00Z' | 'release-cli create --released-at "2020-07-15T08:00:00Z"' diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index 3892b88598a..234ba68d627 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -4,7 +4,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do describe '#satisfied_by?' do - subject { described_class.new(globs).satisfied_by?(pipeline, context) } + let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) } + + subject(:satisfied_by) { described_class.new(globs).satisfied_by?(pipeline, context) } context 'a glob matching rule' do using RSpec::Parameterized::TableSyntax @@ -18,11 +20,9 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do # rubocop:disable Layout/LineLength where(:case_name, :globs, :files, :satisfied) do - 'exact top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true 'exact top-level match' | { paths: ['Dockerfile'] } | { 'Dockerfile' => '', 'Gemfile' => '' } | true 'exact top-level no match' | { paths: ['Dockerfile'] } | { 'Gemfile' => '' } | false 'pattern top-level match' | { paths: ['Docker*'] } | { 'Dockerfile' => '', 'Gemfile' => '' } | true - 'pattern top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false 'pattern top-level no match' | { paths: ['Docker*'] } | { 'Gemfile' => '' } | false 'exact nested match' | { paths: ['project/build.properties'] } | { 'project/build.properties' => '' } | true 'exact nested no match' | { paths: ['project/build.properties'] } | { 'project/README.md' => '' } | false @@ -92,5 +92,97 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do it { is_expected.to be_truthy } end end + + context 'when using compare_to' do + let_it_be(:project) do + create(:project, :custom_repo, + files: { 'README.md' => 'readme' }) + end + + let_it_be(:user) { project.owner } + + before_all do + project.repository.add_branch(user, 'feature_1', 'master') + + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + project.repository.add_tag(user, 'tag_1', 'feature_1') + + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_1' + ) + project.repository.add_branch(user, 'feature_2', 'feature_1') + + project.repository.update_file( + user, 'file2.txt', 'file 2 updated', message: 'Update file2.txt', branch_name: 'feature_2' + ) + end + + context 'when compare_to is branch or tag' do + using RSpec::Parameterized::TableSyntax + + where(:pipeline_ref, :compare_to, :paths, :ff, :result) do + 'feature_1' | 'master' | ['file1.txt'] | true | true + 'feature_1' | 'master' | ['README.md'] | true | false + 'feature_1' | 'master' | ['xyz.md'] | true | false + 'feature_2' | 'master' | ['file1.txt'] | true | true + 'feature_2' | 'master' | ['file2.txt'] | true | true + 'feature_2' | 'feature_1' | ['file1.txt'] | true | false + 'feature_2' | 'feature_1' | ['file1.txt'] | false | true + 'feature_2' | 'feature_1' | ['file2.txt'] | true | true + 'feature_1' | 'tag_1' | ['file1.txt'] | true | false + 'feature_1' | 'tag_1' | ['file1.txt'] | false | true + 'feature_1' | 'tag_1' | ['file2.txt'] | true | true + 'feature_2' | 'tag_1' | ['file2.txt'] | true | true + end + + with_them do + let(:globs) { { paths: paths, compare_to: compare_to } } + + let(:pipeline) do + build(:ci_pipeline, project: project, ref: pipeline_ref, sha: project.commit(pipeline_ref).sha) + end + + before do + stub_feature_flags(ci_rules_changes_compare: ff) + end + + it { is_expected.to eq(result) } + end + end + + context 'when compare_to is a sha' do + let(:globs) { { paths: ['file2.txt'], compare_to: project.commit('tag_1').sha } } + + let(:pipeline) do + build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) + end + + it { is_expected.to be_truthy } + end + + context 'when compare_to is not a valid ref' do + let(:globs) { { paths: ['file1.txt'], compare_to: 'xyz' } } + + let(:pipeline) do + build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) + end + + it 'raises ParseError' do + expect { satisfied_by }.to raise_error( + ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' + ) + end + + context 'when the FF ci_rules_changes_compare is disabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it { is_expected.to be_truthy } + end + end + end end end 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 index 81bce989833..31c7437cfe0 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb @@ -51,14 +51,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If do 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 diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 0fa6d4f8804..6121c28070f 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'support/helpers/stubbed_feature' -require 'support/helpers/stub_feature_flags' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Image do - include StubFeatureFlags - before do stub_feature_flags(ci_docker_image_pull_policy: true) diff --git a/spec/lib/gitlab/ci/config/entry/imageable_spec.rb b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb new file mode 100644 index 00000000000..88f8e260611 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Imageable do + let(:node_class) do + Class.new(::Gitlab::Config::Entry::Node) do + include ::Gitlab::Ci::Config::Entry::Imageable + + validations do + validates :config, allowed_keys: ::Gitlab::Ci::Config::Entry::Imageable::IMAGEABLE_ALLOWED_KEYS + end + + def self.name + 'node' + end + + def value + if string? + { name: @config } + elsif hash? + { + name: @config[:name] + }.compact + else + {} + end + end + end + end + + subject(:entry) { node_class.new(config) } + + before do + entry.compose! + end + + context 'when entry value is correct' do + let(:config) { 'image:1.0' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + let(:config) { ['image:1.0'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors.first) + .to match /config should be a hash or a string/ + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unexpected key is specified' do + let(:config) { { name: 'image:1.0', non_existing: 'test' } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors.first) + .to match /config contains unknown keys: non_existing/ + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 5b9337ede34..714b0a3b6aa 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -212,7 +212,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do let(:unspecified) { double('unspecified', 'specified?' => false) } let(:default) { double('default', '[]' => unspecified) } let(:workflow) { double('workflow', 'has_rules?' => false) } - let(:variables) { } + let(:variables) {} let(:deps) do double('deps', diff --git a/spec/lib/gitlab/ci/config/entry/release_spec.rb b/spec/lib/gitlab/ci/config/entry/release_spec.rb index e5155f91be4..7b6b31ca748 100644 --- a/spec/lib/gitlab/ci/config/entry/release_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/release_spec.rb @@ -128,25 +128,25 @@ RSpec.describe Gitlab::Ci::Config::Entry::Release do end context "when 'ref' is a short commit SHA" do - let(:ref) { 'b3235930'} + let(:ref) { 'b3235930' } it_behaves_like 'a valid entry' end context "when 'ref' is a branch name" do - let(:ref) { 'fix/123-branch-name'} + let(:ref) { 'fix/123-branch-name' } it_behaves_like 'a valid entry' end context "when 'ref' is a semantic versioning tag" do - let(:ref) { 'v1.2.3'} + let(:ref) { 'v1.2.3' } it_behaves_like 'a valid entry' end context "when 'ref' is a semantic versioning tag rc" do - let(:ref) { 'v1.2.3-rc'} + let(:ref) { 'v1.2.3-rc' } it_behaves_like 'a valid entry' end @@ -188,6 +188,30 @@ RSpec.describe Gitlab::Ci::Config::Entry::Release do end end + context "when value includes 'tag_message' keyword" do + let(:config) do + { + tag_name: 'v0.06', + description: "./release_changelog.txt", + tag_message: "Annotated tag message" + } + end + + it_behaves_like 'a valid entry' + end + + context "when 'tag_message' is nil" do + let(:config) do + { + tag_name: 'v0.06', + description: "./release_changelog.txt", + tag_message: nil + } + end + + it_behaves_like 'a valid entry' + end + context 'when entry value is not correct' do describe '#errors' do context 'when value of attribute is invalid' do @@ -231,6 +255,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Release do it_behaves_like 'reports error', 'release milestones should be an array of strings or a string' end + + context 'when `tag_message` is not a string' do + let(:config) { { tag_message: 100 } } + + it_behaves_like 'reports error', 'release tag message should be a string' + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 051cccb4833..45aa859a356 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do :dotenv | 'build.dotenv' :terraform | 'tfplan.json' :accessibility | 'gl-accessibility.json' + :cyclonedx | 'gl-sbom.cdx.zip' end with_them do diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 55ad119ea21..1f8543227c9 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -155,7 +155,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], only: { refs: %w(branches tags) }, - job_variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, after_script: [], ignore: false, @@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], - job_variables: { 'VAR' => 'job' }, + job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, ignore: false, after_script: ['make clean'], diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb index 295561b3c4d..64f0a64074c 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do let(:factory) do @@ -119,6 +119,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do end end end + + context 'with paths and compare_to' do + let(:config) { { paths: %w[app/ lib/], compare_to: 'branch1' } } + + it { is_expected.to be_valid } + + context 'when compare_to is not a string' do + let(:config) { { paths: %w[app/ lib/], compare_to: 1 } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be a string/) + end + end + end end describe '#value' do @@ -137,5 +154,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do it { is_expected.to eq(config) } end + + context 'with paths and compare_to' do + let(:config) do + { paths: ['app/', 'lib/'], compare_to: 'branch1' } + end + + it { is_expected.to eq(config) } + end end end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 93f4a66bfb6..c85fe366da6 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -2,7 +2,6 @@ require 'fast_spec_helper' require 'gitlab_chronic_duration' -require 'support/helpers/stub_feature_flags' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do @@ -418,6 +417,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it { is_expected.to eq(config) } end + + context 'when using changes with paths and compare_to' do + let(:config) { { changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb], compare_to: 'branch1' } } } + + it { is_expected.to eq(config) } + end end context 'when default value has been provided' do diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 3c000fd09ed..821ab442d61 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'support/helpers/stubbed_feature' -require 'support/helpers/stub_feature_flags' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Service do - include StubFeatureFlags - before do stub_feature_flags(ci_docker_image_pull_policy: true) entry.compose! diff --git a/spec/lib/gitlab/ci/config/entry/tags_spec.rb b/spec/lib/gitlab/ci/config/entry/tags_spec.rb index e05d4ae52b2..24efd08c855 100644 --- a/spec/lib/gitlab/ci/config/entry/tags_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/tags_spec.rb @@ -34,7 +34,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Tags do end context 'when tags limit is reached' do - let(:config) { Array.new(50) {|i| "tag-#{i}" } } + let(:config) { Array.new(50) { |i| "tag-#{i}" } } it 'reports error' do expect(entry.errors) diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index 280bebe1a7c..1306d61d99c 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::File::Base do - let(:variables) { } + let(:variables) {} let(:context_params) { { sha: 'HEAD', variables: variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } @@ -100,7 +100,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base do describe '#to_hash' do context 'with includes' do let(:location) { 'some/file/config.yml' } - let(:content) { 'include: { template: Bash.gitlab-ci.yml }'} + let(:content) { 'include: { template: Bash.gitlab-ci.yml }' } before do allow_any_instance_of(test_class) 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 0e78498c98e..f5b36ebfa45 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -167,7 +167,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local do describe '#to_hash' do context 'properly includes another local file in the same repository' do let(:location) { 'some/file/config.yml' } - let(:content) { 'include: { local: another-config.yml }'} + let(:content) { 'include: { local: another-config.yml }' } let(:another_location) { 'another-config.yml' } let(:another_content) { 'rspec: JOB' } 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 3e1c4df4e32..45dfea636f3 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::File::Remote do include StubRequests - let(:variables) {Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } + let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } let(:context_params) { { sha: '12345', variables: variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:params) { { remote: location } } diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 354392eb42e..96ca5d98a6e 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -232,7 +232,7 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do context 'when parallel config does not matches a factory' do let(:variables_config) { {} } - let(:parallel_config) { } + let(:parallel_config) {} it 'does not alter the job config' do is_expected.to match(config) diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 5eb04d969eb..055114769ea 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -872,4 +872,21 @@ RSpec.describe Gitlab::Ci::Config do end end end + + describe '#workflow_rules' do + subject(:workflow_rules) { config.workflow_rules } + + let(:yml) do + <<-EOS + workflow: + rules: + - if: $CI_COMMIT_REF_NAME == "master" + + rspec: + script: exit 0 + EOS + end + + it { is_expected.to eq([{ if: '$CI_COMMIT_REF_NAME == "master"' }]) } + end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 4017accb462..33474865a93 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -178,7 +178,7 @@ RSpec.describe Gitlab::Ci::CronParser do end context 'when time crosses a Daylight Savings boundary' do - let(:cron) { '* 0 1 12 *'} + let(:cron) { '* 0 1 12 *' } # Note this previously only failed if the time zone is set # to a zone that observes Daylight Savings diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb new file mode 100644 index 00000000000..c99cfa94aa6 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do + subject(:parse_source) { described_class.parse_source(properties) } + + context 'when properties are nil' do + let(:properties) { nil } + + it { is_expected.to be_nil } + end + + context 'when report does not have gitlab properties' do + let(:properties) { ['name' => 'foo', 'value' => 'bar'] } + + it { is_expected.to be_nil } + end + + context 'when schema_version is missing' do + let(:properties) do + [ + { 'name' => 'gitlab:dependency_scanning:dependency_file', 'value' => 'package-lock.json' }, + { 'name' => 'gitlab:dependency_scanning:package_manager_name', 'value' => 'npm' }, + { 'name' => 'gitlab:dependency_scanning:language', 'value' => 'JavaScript' } + ] + end + + it { is_expected.to be_nil } + end + + context 'when schema version is unsupported' do + let(:properties) do + [ + { 'name' => 'gitlab:meta:schema_version', 'value' => '2' }, + { 'name' => 'gitlab:dependency_scanning:dependency_file', 'value' => 'package-lock.json' }, + { 'name' => 'gitlab:dependency_scanning:package_manager_name', 'value' => 'npm' }, + { 'name' => 'gitlab:dependency_scanning:language', 'value' => 'JavaScript' } + ] + end + + it { is_expected.to be_nil } + end + + context 'when no dependency_scanning properties are present' do + let(:properties) do + [ + { 'name' => 'gitlab:meta:schema_version', 'value' => '1' } + ] + end + + it 'does not call dependency_scanning parser' do + expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:parse_source) + + parse_source + end + end + + context 'when dependency_scanning properties are present' do + let(:properties) do + [ + { 'name' => 'gitlab:meta:schema_version', 'value' => '1' }, + { 'name' => 'gitlab:dependency_scanning:category', 'value' => 'development' }, + { 'name' => 'gitlab:dependency_scanning:input_file:path', 'value' => 'package-lock.json' }, + { 'name' => 'gitlab:dependency_scanning:source_file:path', 'value' => 'package.json' }, + { 'name' => 'gitlab:dependency_scanning:package_manager:name', 'value' => 'npm' }, + { 'name' => 'gitlab:dependency_scanning:language:name', 'value' => 'JavaScript' }, + { 'name' => 'gitlab:dependency_scanning:unsupported_property', 'value' => 'Should be ignored' } + ] + end + + let(:expected_input) do + { + 'category' => 'development', + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + } + end + + it 'passes only supported properties to the dependency scanning parser' do + expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).to receive(:source).with(expected_input) + + parse_source + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb new file mode 100644 index 00000000000..431fe9f3591 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do + let(:report) { instance_double('Gitlab::Ci::Reports::Sbom::Report') } + let(:report_data) { base_report_data } + let(:raw_report_data) { report_data.to_json } + let(:report_valid?) { true } + let(:validator_errors) { [] } + let(:properties_parser) { class_double('Gitlab::Ci::Parsers::Sbom::CyclonedxProperties') } + + let(:base_report_data) do + { + 'bomFormat' => 'CycloneDX', + 'specVersion' => '1.4', + 'version' => 1 + } + end + + subject(:parse!) { described_class.new.parse!(raw_report_data, report) } + + before do + allow_next_instance_of(Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator) do |validator| + allow(validator).to receive(:valid?).and_return(report_valid?) + allow(validator).to receive(:errors).and_return(validator_errors) + end + + allow(properties_parser).to receive(:parse_source) + stub_const('Gitlab::Ci::Parsers::Sbom::CyclonedxProperties', properties_parser) + end + + context 'when report JSON is invalid' do + let(:raw_report_data) { '{ ' } + + it 'handles errors and adds them to the report' do + expect(report).to receive(:add_error).with(a_string_including("Report JSON is invalid:")) + + expect { parse! }.not_to raise_error + end + end + + context 'when report uses an unsupported spec version' do + let(:report_data) { base_report_data.merge({ 'specVersion' => '1.3' }) } + + it 'reports unsupported version as an error' do + expect(report).to receive(:add_error).with("Unsupported CycloneDX spec version. Must be one of: 1.4") + + parse! + end + end + + context 'when report does not conform to the CycloneDX schema' do + let(:report_valid?) { false } + let(:validator_errors) { %w[error1 error2] } + + it 'reports all errors returned by the validator' do + expect(report).to receive(:add_error).with("error1") + expect(report).to receive(:add_error).with("error2") + + parse! + end + end + + context 'when cyclonedx report has no components' do + it 'skips component processing' do + expect(report).not_to receive(:add_component) + + parse! + end + end + + context 'when report has components' do + let(:report_data) { base_report_data.merge({ 'components' => components }) } + let(:components) do + [ + { + "name" => "activesupport", + "version" => "5.1.4", + "purl" => "pkg:gem/activesupport@5.1.4", + "type" => "library", + "bom-ref" => "pkg:gem/activesupport@5.1.4" + }, + { + "name" => "byebug", + "version" => "10.0.0", + "purl" => "pkg:gem/byebug@10.0.0", + "type" => "library", + "bom-ref" => "pkg:gem/byebug@10.0.0" + }, + { + "name" => "minimal-component", + "type" => "library" + }, + { + # Should be skipped + "name" => "unrecognized-type", + "type" => "unknown" + } + ] + end + + it 'adds each component, ignoring unused attributes' do + expect(report).to receive(:add_component) + .with({ "name" => "activesupport", "version" => "5.1.4", "type" => "library" }) + expect(report).to receive(:add_component) + .with({ "name" => "byebug", "version" => "10.0.0", "type" => "library" }) + expect(report).to receive(:add_component) + .with({ "name" => "minimal-component", "type" => "library" }) + + parse! + end + end + + context 'when report has metadata properties' do + let(:report_data) { base_report_data.merge({ 'metadata' => { 'properties' => properties } }) } + + let(:properties) do + [ + { 'name' => 'gitlab:meta:schema_version', 'value' => '1' }, + { 'name' => 'gitlab:dependency_scanning:category', 'value' => 'development' }, + { 'name' => 'gitlab:dependency_scanning:input_file:path', 'value' => 'package-lock.json' }, + { 'name' => 'gitlab:dependency_scanning:source_file:path', 'value' => 'package.json' }, + { 'name' => 'gitlab:dependency_scanning:package_manager:name', 'value' => 'npm' }, + { 'name' => 'gitlab:dependency_scanning:language:name', 'value' => 'JavaScript' } + ] + end + + it 'passes them to the properties parser' do + expect(properties_parser).to receive(:parse_source).with(properties) + + parse! + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb new file mode 100644 index 00000000000..30114b17cac --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do + subject { described_class.source(property_data) } + + context 'when all property data is present' do + let(:property_data) do + { + 'category' => 'development', + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + } + end + + it 'returns expected source data' do + is_expected.to eq({ + 'type' => :dependency_scanning, + 'data' => property_data, + 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + }) + end + end + + context 'when required properties are missing' do + let(:property_data) do + { + 'category' => 'development', + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + } + end + + it { is_expected.to be_nil } + end +end diff --git a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb new file mode 100644 index 00000000000..c54a3268bbe --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator do + # Reports should be valid or invalid according to the specification at + # https://cyclonedx.org/docs/1.4/json/ + + subject(:validator) { described_class.new(report_data) } + + let_it_be(:required_attributes) do + { + "bomFormat" => "CycloneDX", + "specVersion" => "1.4", + "version" => 1 + } + end + + context "with minimally valid report" do + let_it_be(:report_data) { required_attributes } + + it { is_expected.to be_valid } + end + + context "when report has components" do + let(:report_data) { required_attributes.merge({ "components" => components }) } + + context "with minimally valid components" do + let(:components) do + [ + { + "type" => "library", + "name" => "activesupport" + }, + { + "type" => "library", + "name" => "byebug" + } + ] + end + + it { is_expected.to be_valid } + end + + context "when components have versions" do + let(:components) do + [ + { + "type" => "library", + "name" => "activesupport", + "version" => "5.1.4" + }, + { + "type" => "library", + "name" => "byebug", + "version" => "10.0.0" + } + ] + end + + it { is_expected.to be_valid } + end + + context "when components are not valid" do + let(:components) do + [ + { "type" => "foo" }, + { "name" => "activesupport" } + ] + end + + it { is_expected.not_to be_valid } + + it "outputs errors for each validation failure" do + expect(validator.errors).to match_array([ + "property '/components/0' is missing required keys: name", + "property '/components/0/type' is not one of: [\"application\", \"framework\"," \ + " \"library\", \"container\", \"operating-system\", \"device\", \"firmware\", \"file\"]", + "property '/components/1' is missing required keys: type" + ]) + end + end + end + + context "when report has metadata" do + let(:metadata) do + { + "timestamp" => "2022-02-23T08:02:39Z", + "tools" => [{ "vendor" => "GitLab", "name" => "Gemnasium", "version" => "2.34.0" }], + "authors" => [{ "name" => "GitLab", "email" => "support@gitlab.com" }] + } + end + + let(:report_data) { required_attributes.merge({ "metadata" => metadata }) } + + it { is_expected.to be_valid } + + context "when metadata has properties" do + before do + metadata.merge!({ "properties" => properties }) + end + + context "when properties are valid" do + let(:properties) do + [ + { "name" => "gitlab:dependency_scanning:input_file", "value" => "Gemfile.lock" }, + { "name" => "gitlab:dependency_scanning:package_manager", "value" => "bundler" } + ] + end + + it { is_expected.to be_valid } + end + + context "when properties are invalid" do + let(:properties) do + [ + { "name" => ["gitlab:meta:schema_version"], "value" => 1 } + ] + end + + it { is_expected.not_to be_valid } + + it "outputs errors for each validation failure" do + expect(validator.errors).to match_array([ + "property '/metadata/properties/0/name' is not of type: string", + "property '/metadata/properties/0/value' is not of type: string" + ]) + end + end + end + 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 d06077d69b6..7828aa99f6a 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 @@ -6,6 +6,10 @@ 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(:deprecated_schema_version_message) {} + let(:missing_schema_version_message) do + "Report version not provided, dast report type supports versions: #{supported_dast_versions}" + end let(:scanner) do { @@ -24,7 +28,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 'when a schema JSON file exists for a particular report type version' do + context 'when all files under schema path are explicitly listed' 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/ @@ -38,7 +42,7 @@ 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} is in the constant" do + it "#{report_type} #{version}" do expect(described_class::SUPPORTED_VERSIONS[report_type]).to include(version) end end @@ -64,11 +68,54 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do describe '#valid?' do subject { validator.valid? } + context 'when given a supported MAJOR.MINOR schema version' do + let(:report_type) { :dast } + let(:report_version) do + latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") + (latest_vendored_version[0...2] << "34").join(".") + end + + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } + end + + it { is_expected.to be_truthy } + end + + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } + end + + it { is_expected.to be_falsey } + + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: 'schema_validation_fails', + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) + + subject + end + end + end + context 'when given a supported schema version' do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - context 'when the report is valid' do + context 'and the report is valid' do let(:report_data) do { 'version' => report_version, @@ -79,7 +126,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_truthy } end - context 'when the report is invalid' do + context 'and the report is invalid' do let(:report_data) do { 'version' => report_version @@ -118,7 +165,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end - context 'when the report passes schema validation' do + context 'and the report passes schema validation' do let(:report_data) do { 'version' => '10.0.0', @@ -143,34 +190,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end end - 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 - - let(:report_data) do - { - 'version' => 'V2.7.0' - } - end - - it { is_expected.to be_falsey } + context 'and the report does not pass schema validation' do + let(:report_data) do + { + 'version' => 'V2.7.0' + } end - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end - - let(:report_data) do - { - 'version' => 'V2.7.0' - } - end - - it { is_expected.to be_truthy } - end + it { is_expected.to be_falsey } end end @@ -178,20 +205,40 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: true) + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } end - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end + it { is_expected.to be_falsey } + + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: 'using_unsupported_schema_version', + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) - it { is_expected.to be_falsey } + subject + end + end + + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } + end + + context 'and scanner information is empty' do + let(:scanner) { {} } it 'logs related information' do expect(Gitlab::AppLogger).to receive(:info).with( @@ -199,79 +246,26 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do security_report_type: report_type, security_report_version: report_version, project_id: project.id, + security_report_failure: 'schema_validation_fails', + security_report_scanner_id: nil, + security_report_scanner_version: nil + ) + + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, security_report_failure: 'using_unsupported_schema_version', - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' + security_report_scanner_id: nil, + security_report_scanner_version: nil ) subject end end - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - context 'when scanner information is empty' do - let(:scanner) { {} } - - it 'logs related information' do - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'schema_validation_fails', - security_report_scanner_id: nil, - security_report_scanner_version: nil - ) - - expect(Gitlab::AppLogger).to receive(:info).with( - message: "security report schema validation problem", - security_report_type: report_type, - security_report_version: report_version, - project_id: project.id, - security_report_failure: 'using_unsupported_schema_version', - security_report_scanner_id: nil, - security_report_scanner_version: nil - ) - - subject - end - end - - it { is_expected.to be_falsey } - end - end - - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end - - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_truthy } - end - - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - it { is_expected.to be_truthy } - end + it { is_expected.to be_falsey } end end @@ -284,19 +278,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } 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 @@ -307,7 +289,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 'when the report is valid' do + context 'and the report is valid' do let(:report_data) do { 'version' => report_version, @@ -318,34 +300,20 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_empty } end - context 'when the report is invalid' do + context 'and the report is invalid' do let(:report_data) do { 'version' => report_version } end - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: project) - end - - let(:expected_errors) do - [ - 'root is missing required keys: vulnerabilities' - ] - end - - it { is_expected.to match_array(expected_errors) } + let(:expected_errors) do + [ + 'root is missing required keys: vulnerabilities' + ] end - 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 + it { is_expected.to match_array(expected_errors) } end end @@ -363,7 +331,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end - context 'when the report passes schema validation' do + context 'and the report passes schema validation' do let(:report_data) do { 'version' => '10.0.0', @@ -374,119 +342,77 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_empty } end - 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 - - let(:report_data) do - { - 'version' => 'V2.7.0' - } - end - - let(:expected_errors) do - [ - "property '/version' does not match pattern: ^[0-9]+\\.[0-9]+\\.[0-9]+$", - "root is missing required keys: vulnerabilities" - ] - end - - it { is_expected.to match_array(expected_errors) } + context 'and the report does not pass schema validation' do + let(:report_data) do + { + 'version' => 'V2.7.0' + } end - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end - - let(:report_data) do - { - 'version' => 'V2.7.0' - } - end - - it { is_expected.to be_empty } + let(:expected_errors) do + [ + "property '/version' does not match pattern: ^[0-9]+\\.[0-9]+\\.[0-9]+$", + "root is missing required keys: vulnerabilities" + ] end + + it { is_expected.to match_array(expected_errors) } end end context 'when given an unsupported schema version' do let(:report_type) { :dast } let(:report_version) { "12.37.0" } + let(:expected_unsupported_message) do + "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\ + "#{supported_dast_versions}. GitLab will attempt to validate this report against the earliest supported "\ + "versions of this report type, to show all the errors but will not ingest the report" + end - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: true) + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } end - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - let(:expected_errors) do - [ - "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) } + let(:expected_errors) do + [ + expected_unsupported_message + ] end - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - let(:expected_errors) do - [ - "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 - - it { is_expected.to match_array(expected_errors) } - end + it { is_expected.to match_array(expected_errors) } end - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } end - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } + let(:expected_errors) do + [ + expected_unsupported_message, + "root is missing required keys: vulnerabilities" + ] end - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - it { is_expected.to be_empty } - end + it { is_expected.to match_array(expected_errors) } end end context 'when not given a schema version' do let(:report_type) { :dast } let(:report_version) { nil } + let(:expected_missing_version_message) do + "Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\ + "will attempt to validate this report against the earliest supported versions of this report type, to show all "\ + "the errors but will not ingest the report" + end + let(:report_data) do { 'vulnerabilities' => [] @@ -496,19 +422,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ "root is missing required keys: version", - "Report version not provided, dast report type supports versions: #{supported_dast_versions}" + expected_missing_version_message ] 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 @@ -519,7 +437,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 'when the report is valid' do + context 'and the report is valid' do let(:report_data) do { 'version' => report_version, @@ -530,7 +448,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_empty } end - context 'when the report is invalid' do + context 'and the report is invalid' do let(:report_data) do { 'version' => report_version @@ -550,9 +468,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } + let(:expected_deprecation_message) do + "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this "\ + "report type are: #{supported_dast_versions}. GitLab will attempt to parse and ingest this report if valid." + end + let(:expected_deprecation_warnings) do [ - "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}" + expected_deprecation_message ] end @@ -560,7 +483,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end - context 'when the report passes schema validation' do + context 'and the report passes schema validation' do let(:report_data) do { 'version' => report_version, @@ -571,7 +494,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to match_array(expected_deprecation_warnings) } end - context 'when the report does not pass schema validation' do + context 'and the report does not pass schema validation' do let(:report_data) do { 'version' => 'V2.7.0' @@ -600,11 +523,27 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do describe '#warnings' do subject { validator.warnings } - context 'when given a supported schema version' do + context 'when given a supported MAJOR.MINOR schema version' do let(:report_type) { :dast } - let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } + let(:report_version) do + latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") + (latest_vendored_version[0...2] << "34").join(".") + end + + let(:latest_patch_version) do + ::Security::ReportSchemaVersionMatcher.new( + report_declared_version: report_version, + supported_versions: described_class::SUPPORTED_VERSIONS[report_type] + ).call + end + + let(:message) do + "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\ + " any vendored schema version. Validation will be attempted against version"\ + " #{latest_patch_version}" + end - context 'when the report is valid' do + context 'and the report is valid' do let(:report_data) do { 'version' => report_version, @@ -612,37 +551,57 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do } end - it { is_expected.to be_empty } + it { is_expected.to match_array([message]) } end - context 'when the report is invalid' do + context 'and the report is invalid' do let(:report_data) do { 'version' => report_version } end - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: project) - end + it { is_expected.to match_array([message]) } + + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: 'schema_validation_fails', + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) - it { is_expected.to be_empty } + subject end + end + end - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end + context 'when given a supported schema version' do + let(:report_type) { :dast } + let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } - let(:expected_warnings) do - [ - 'root is missing required keys: vulnerabilities' - ] - end + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } + end + + it { is_expected.to be_empty } + end - it { is_expected.to match_array(expected_warnings) } + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } end + + it { is_expected.to be_empty } end end @@ -660,7 +619,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end - context 'when the report passes schema validation' do + context 'and the report passes schema validation' do let(:report_data) do { 'vulnerabilities' => [] @@ -670,35 +629,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do it { is_expected.to be_empty } end - context 'when the report does not pass schema validation' do + context 'and the report does not pass schema validation' do let(:report_data) do { 'version' => 'V2.7.0' } end - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: true) - end - - it { is_expected.to be_empty } - end - - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end - - let(:expected_warnings) do - [ - "property '/version' does not match pattern: ^[0-9]+\\.[0-9]+\\.[0-9]+$", - "root is missing required keys: vulnerabilities" - ] - end - - it { is_expected.to match_array(expected_warnings) } - end + it { is_expected.to be_empty } end end @@ -706,71 +644,25 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:report_type) { :dast } let(:report_version) { "12.37.0" } - context 'when enforce_security_report_validation is enabled' do - before do - stub_feature_flags(enforce_security_report_validation: true) - end - - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - it { is_expected.to be_empty } + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } end - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - it { is_expected.to be_empty } - end + it { is_expected.to be_empty } end - context 'when enforce_security_report_validation is disabled' do - before do - stub_feature_flags(enforce_security_report_validation: false) - end - - context 'when the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - - let(:expected_warnings) do - [ - "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) } + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } end - context 'when the report is invalid' do - let(:report_data) do - { - 'version' => report_version - } - end - - let(:expected_warnings) do - [ - "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 - - it { is_expected.to match_array(expected_warnings) } - end + it { is_expected.to be_empty } end end @@ -784,21 +676,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do 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/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 0d78ce3440a..de43e759193 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -282,7 +282,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do subject { command.ambiguous_ref? } context 'when ref is not ambiguous' do - it { is_expected. to eq(false) } + it { is_expected.to eq(false) } end context 'when ref is ambiguous' do @@ -291,7 +291,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do project.repository.add_branch(project.creator, 'ref', 'master') end - it { is_expected. to eq(true) } + it { is_expected.to eq(true) } end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb index cbf92f8fa83..be5d3a96126 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do end context 'when the corresponding environment does not exist' do - let!(:environment) { } + let!(:environment) {} it 'does not create a deployment record' do expect { subject }.not_to change { Deployment.count } diff --git a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb index e30a78546af..eb5a37f19f4 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do before do allow(step).to receive(:workflow_rules_result) .and_return( - double(pass?: true, variables: { 'VAR1' => 'val2' }) + double(pass?: true, variables: { 'VAR1' => 'val2', 'VAR2' => 3 }) ) step.perform! @@ -65,7 +65,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do end it 'saves workflow_rules_result' do - expect(command.workflow_rules_result.variables).to eq({ 'VAR1' => 'val2' }) + expect(command.workflow_rules_result.variables).to eq({ 'VAR1' => 'val2', 'VAR2' => 3 }) end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb index fabfbd779f3..5ee96b0baa8 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::SeedBlock do let(:project) { create(:project, :repository) } let(:user) { create(:user, developer_projects: [project]) } - let(:seeds_block) { } + let(:seeds_block) {} let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index 687bb82a8ef..f7774e199fb 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user, developer_projects: [project]) } - let(:seeds_block) { } + let(:seeds_block) {} let(:command) { initialize_command } let(:pipeline) { build(:ci_pipeline, project: project) } @@ -205,6 +205,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end end + describe '#rule_variables' do + let(:config) do + { + variables: { VAR1: 11 }, + workflow: { + rules: [{ if: '$CI_PIPELINE_SOURCE', + variables: { SYMBOL: :symbol, STRING: "string", INTEGER: 1 } }, + { when: 'always' }] + }, + rspec: { script: 'rake' } + } + end + + let(:rspec_variables) { command.pipeline_seed.stages[0].statuses[0].variables.to_hash } + + it 'correctly parses rule variables' do + run_chain + + expect(rspec_variables['SYMBOL']).to eq("symbol") + expect(rspec_variables['STRING']).to eq("string") + expect(rspec_variables['INTEGER']).to eq("1") + end + end + context 'N+1 queries' do it 'avoids N+1 queries when calculating variables of jobs', :use_sql_query_cache do warm_up_pipeline, warm_up_command = prepare_pipeline1 diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index eeac0c85a77..fb1a360a4b7 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -148,6 +148,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do expect(::Gitlab::HTTP).to receive(:post) do |_url, params| payload = Gitlab::Json.parse(params[:body]) + expect(payload['total_builds_count']).to eq(0) + builds = payload['builds'] expect(builds.count).to eq(2) expect(builds[0]['services']).to be_nil @@ -160,6 +162,23 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do perform! end + + context "with existing jobs from other project's alive pipelines" do + before do + create(:ci_pipeline, :with_job, user: user) + create(:ci_pipeline, :with_job) + end + + it 'returns the expected total_builds_count' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['total_builds_count']).to eq(1) + end + + perform! + end + end end context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do @@ -243,7 +262,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end context 'when save_incompleted is false' do - let(:save_incompleted) { false} + let(:save_incompleted) { false } it 'adds errors to the pipeline without dropping it' do 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 83742699d3d..47f172922a5 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -160,14 +160,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches 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 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 aad33106647..9e7ea3e4ea4 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 @@ -160,14 +160,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches 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 diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index bbd11a00149..acaec07f95b 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -179,24 +179,16 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do .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 + where(:expression, :result) do + '$teststring =~ "abcde"' | true + '$teststring =~ $teststring' | true + '$teststring =~ $pattern1' | true + '$teststring =~ $pattern2' | 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 diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb index 8f727749ee2..a742c619584 100644 --- a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } - let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2)} + let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2) } let(:command) do double(:command, diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 040f3ab5830..75f6a773c2d 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -97,15 +97,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }], + job_variables: [{ key: 'VAR1', value: 'var 1' }, + { key: 'VAR2', value: 'var 2' }], rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } end it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }]) end end @@ -114,13 +114,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VARIABLE', value: 'value', public: true }], + job_variables: [{ key: 'VARIABLE', value: 'value' }], tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] } end it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } - it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value', public: true }]) } + it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } end context 'with cache:key' do @@ -257,19 +257,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], - job_variables: [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }], + yaml_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + job_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], root_variables_inheritance: root_variables_inheritance } end context 'when the pipeline has variables' do let(:root_variables) do - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var pipeline 2', public: true }, - { key: 'VAR3', value: 'var pipeline 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var pipeline 2' }, + { key: 'VAR3', value: 'var pipeline 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] end context 'when root_variables_inheritance is true' do @@ -277,10 +277,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }, - { key: 'VAR4', value: 'new var pipeline 4', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] ) end end @@ -290,8 +290,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns job variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -301,9 +301,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }] + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] ) end end @@ -314,8 +314,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns seed yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2', public: true }, - { key: 'VAR3', value: 'var 3', public: true }]) + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }]) end end end @@ -324,8 +324,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:attributes) do { name: 'rspec', ref: 'master', - yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }], - job_variables: [{ key: 'VAR1', value: 'var 1', public: true }], + yaml_variables: [{ key: 'VAR1', value: 'var 1' }], + job_variables: [{ key: 'VAR1', value: 'var 1' }], root_variables_inheritance: root_variables_inheritance, rules: rules } end @@ -338,14 +338,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'new var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'new var 2' }) end end context 'when the rules use root variables' do let(:root_variables) do - [{ key: 'VAR2', value: 'var pipeline 2', public: true }] + [{ key: 'VAR2', value: 'var pipeline 2' }] end let(:rules) do @@ -353,15 +353,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1', public: true }, - { key: 'VAR2', value: 'overridden var 2', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'overridden var 2' }) end context 'when the root_variables_inheritance is false' do let(:root_variables_inheritance) { false } it 'does not recalculate the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1', public: true }) + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) end end end @@ -769,7 +769,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do with_them do it { is_expected.not_to be_included } - it 'correctly populates when:' do + it 'still correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end @@ -958,6 +958,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do expect(seed_build.attributes).to include(when: 'never') end end + + context 'with invalid rules raising error' do + let(:rule_set) do + [ + { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } + ] + end + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + + it 'returns an error' do + expect(seed_build.errors).to contain_exactly( + 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' + ) + end + end end end diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb new file mode 100644 index 00000000000..672117c311f --- /dev/null +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Sbom::Component do + let(:attributes) do + { + 'type' => 'library', + 'name' => 'component-name', + 'version' => 'v0.0.1' + } + end + + subject { described_class.new(attributes) } + + it 'has correct attributes' do + expect(subject).to have_attributes( + component_type: 'library', + name: 'component-name', + version: 'v0.0.1' + ) + end +end diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb new file mode 100644 index 00000000000..d7a285ab13c --- /dev/null +++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Sbom::Report do + subject(:report) { described_class.new } + + describe '#add_error' do + it 'appends errors to a list' do + report.add_error('error1') + report.add_error('error2') + + expect(report.errors).to match_array(%w[error1 error2]) + end + end + + describe '#set_source' do + let_it_be(:source) do + { + 'type' => :dependency_scanning, + 'data' => { + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + }, + 'fingerprint' => 'c01df1dc736c1148717e053edbde56cb3a55d3e31f87cea955945b6f67c17d42' + } + end + + it 'stores the source' do + report.set_source(source) + + expect(report.source).to be_a(Gitlab::Ci::Reports::Sbom::Source) + end + end + + describe '#add_component' do + let_it_be(:components) do + [ + { 'type' => 'library', 'name' => 'component1', 'version' => 'v0.0.1' }, + { 'type' => 'library', 'name' => 'component2', 'version' => 'v0.0.2' }, + { 'type' => 'library', 'name' => 'component2' } + ] + end + + it 'appends components to a list' do + components.each { |component| report.add_component(component) } + + expect(report.components.size).to eq(3) + expect(report.components).to all(be_a(Gitlab::Ci::Reports::Sbom::Component)) + end + end +end diff --git a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb new file mode 100644 index 00000000000..97d8d7abb33 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Sbom::Reports do + subject(:reports_list) { described_class.new } + + describe '#add_report' do + let(:rep1) { Gitlab::Ci::Reports::Sbom::Report.new } + let(:rep2) { Gitlab::Ci::Reports::Sbom::Report.new } + + it 'appends the report to the report list' do + reports_list.add_report(rep1) + reports_list.add_report(rep2) + + expect(reports_list.reports.length).to eq(2) + expect(reports_list.reports.first).to eq(rep1) + expect(reports_list.reports.last).to eq(rep2) + end + end +end diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb new file mode 100644 index 00000000000..2d6434534a0 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Sbom::Source do + let(:attributes) do + { + 'type' => :dependency_scanning, + 'data' => { + 'category' => 'development', + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + }, + 'fingerprint' => '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + } + end + + subject { described_class.new(attributes) } + + it 'has correct attributes' do + expect(subject).to have_attributes( + source_type: attributes['type'], + data: attributes['data'], + fingerprint: attributes['fingerprint'] + ) + end +end diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb index 79eee642552..e240edc4a12 100644 --- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb @@ -57,7 +57,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: 'dast') } let(:vulnerabilities_allowed) { 0 } let(:severity_levels) { %w(critical high) } - let(:vulnerability_states) { %w(newly_detected)} + let(:vulnerability_states) { %w(newly_detected) } subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) } diff --git a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb index 44e66fd9028..6f75e2c55e8 100644 --- a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb @@ -60,7 +60,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do end describe '#added' do - let(:new_location) {build(:ci_reports_security_locations_sast, :dynamic) } + let(:new_location) { build(:ci_reports_security_locations_sast, :dynamic) } let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) } let(:vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:critical], location: new_location, **vul_params) } let(:low_vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:low], location: new_location, **vul_params) } diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 1d6b39a7831..4a1f77bed65 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -91,7 +91,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do subject { test_suite.with_attachment! } context 'when test cases do not contain an attachment' do - let(:test_case) { build(:report_test_case, :failed)} + let(:test_case) { build(:report_test_case, :failed) } before do test_suite.add_test_case(test_case) @@ -103,7 +103,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do end context 'when test cases contain an attachment' do - let(:test_case_with_attachment) { build(:report_test_case, :failed_with_attachment)} + let(:test_case_with_attachment) { build(:report_test_case, :failed_with_attachment) } before do test_suite.add_test_case(test_case_with_attachment) diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index 576eb02ad83..ad1e9b12b8a 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::RunnerReleases do subject { described_class.instance } - let(:runner_releases_url) { 'the release API URL' } + let(:runner_releases_url) { 'http://testurl.com/runner_public_releases' } def releases subject.releases @@ -18,7 +18,7 @@ RSpec.describe Gitlab::Ci::RunnerReleases do before do subject.reset_backoff! - stub_application_setting(public_runner_releases_url: runner_releases_url) + allow(subject).to receive(:runner_releases_url).and_return(runner_releases_url) end describe 'caching behavior', :use_clean_rails_memory_store_caching do @@ -77,7 +77,8 @@ RSpec.describe Gitlab::Ci::RunnerReleases do allow(Gitlab::HTTP).to receive(:get).with(runner_releases_url, anything) do http_call_timestamp_offsets << Time.now.utc - start_time - raise Net::OpenTimeout if opts&.dig(:raise_timeout) + err_class = opts&.dig(:raise_error) + raise err_class if err_class mock_http_response(response) end @@ -113,12 +114,13 @@ RSpec.describe Gitlab::Ci::RunnerReleases do end context 'when request results in timeout' do - let(:response) { } + let(:response) {} let(:expected_releases) { nil } let(:expected_releases_by_minor) { nil } it_behaves_like 'requests that follow cache status', 5.seconds - it_behaves_like 'a service implementing exponential backoff', raise_timeout: true + it_behaves_like 'a service implementing exponential backoff', raise_error: Net::OpenTimeout + it_behaves_like 'a service implementing exponential backoff', raise_error: Errno::ETIMEDOUT end context 'when response is nil' do diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index f2507a24b10..55c3834bfa7 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -5,36 +5,35 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do using RSpec::Parameterized::TableSyntax - describe '#check_runner_upgrade_status' do - subject(:result) { described_class.instance.check_runner_upgrade_status(runner_version) } + subject(:instance) { described_class.new(gitlab_version, runner_releases) } + + describe '#check_runner_upgrade_suggestion' do + subject(:result) { instance.check_runner_upgrade_suggestion(runner_version) } let(:gitlab_version) { '14.1.1' } let(:parsed_runner_version) { ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true) } - - before do - allow(described_class.instance).to receive(:gitlab_version) - .and_return(::Gitlab::VersionInfo.parse(gitlab_version)) - end + let(:runner_releases) { instance_double(Gitlab::Ci::RunnerReleases) } context 'with failing Gitlab::Ci::RunnerReleases request' do let(:runner_version) { '14.1.123' } - let(:runner_releases_double) { instance_double(Gitlab::Ci::RunnerReleases) } before do - allow(Gitlab::Ci::RunnerReleases).to receive(:instance).and_return(runner_releases_double) - allow(runner_releases_double).to receive(:releases).and_return(nil) + allow(runner_releases).to receive(:releases).and_return(nil) end it 'returns :error' do - is_expected.to eq({ error: parsed_runner_version }) + is_expected.to eq([parsed_runner_version, :error]) end end context 'with available_runner_releases configured' do - before do - url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url + let(:runner_releases) { Gitlab::Ci::RunnerReleases.instance } + let(:runner_releases_url) do + ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url + end - WebMock.stub_request(:get, url).to_return( + before do + WebMock.stub_request(:get, runner_releases_url).to_return( body: available_runner_releases.map { |v| { name: v } }.to_json, status: 200, headers: { 'Content-Type' => 'application/json' } @@ -53,7 +52,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:runner_version) { 'v14.0.1' } it 'returns :not_available' do - is_expected.to eq({ not_available: parsed_runner_version }) + is_expected.to eq([parsed_runner_version, :not_available]) end end end @@ -68,7 +67,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:runner_version) { nil } it 'returns :invalid_version' do - is_expected.to match({ invalid_version: anything }) + is_expected.to match([anything, :invalid_version]) end end @@ -76,7 +75,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:runner_version) { 'junk' } it 'returns :invalid_version' do - is_expected.to match({ invalid_version: anything }) + is_expected.to match([anything, :invalid_version]) end end @@ -87,7 +86,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:runner_version) { 'v14.2.0' } it 'returns :not_available' do - is_expected.to eq({ not_available: parsed_runner_version }) + is_expected.to eq([parsed_runner_version, :not_available]) end end end @@ -96,7 +95,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:gitlab_version) { '14.0.1' } context 'with valid params' do - where(:runner_version, :expected_result, :expected_suggested_version) do + where(:runner_version, :expected_status, :expected_suggested_version) do 'v15.0.0' | :not_available | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available 'v14.1.0-rc3' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes 'v14.1.0~beta.1574.gf6ea9389' | :recommended | '14.1.1' # suffixes are correctly handled @@ -116,7 +115,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do end with_them do - it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) } + it { is_expected.to eq([Gitlab::VersionInfo.parse(expected_suggested_version), expected_status]) } end end end @@ -125,7 +124,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:gitlab_version) { '13.9.0' } context 'with valid params' do - where(:runner_version, :expected_result, :expected_suggested_version) do + where(:runner_version, :expected_status, :expected_suggested_version) do 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible 'v13.10.1' | :not_available | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available @@ -136,7 +135,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do end with_them do - it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) } + it { is_expected.to eq([Gitlab::VersionInfo.parse(expected_suggested_version), expected_status]) } end end end @@ -152,7 +151,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do let(:runner_version) { '14.11.0~beta.29.gd0c550e3' } it 'recommends 15.1.0 since 14.11 is an unknown release and 15.1.0 is available' do - is_expected.to eq({ recommended: Gitlab::VersionInfo.new(15, 1, 0) }) + is_expected.to eq([Gitlab::VersionInfo.new(15, 1, 0), :recommended]) end end end diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb index 30e6ad234a0..37524afc83d 100644 --- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb @@ -29,15 +29,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do end it { expect(subject).to have_details } - it { expect(subject.details_path).to include "jobs/#{bridge.id}" } - - context 'with ci_retry_downstream_pipeline ff disabled' do - before do - stub_feature_flags(ci_retry_downstream_pipeline: false) - end - - it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" } - end + it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" } end context 'when user does not have access to read downstream pipeline' do diff --git a/spec/lib/gitlab/ci/status/build/canceled_spec.rb b/spec/lib/gitlab/ci/status/build/canceled_spec.rb index e30a2211c8f..519b970ca5e 100644 --- a/spec/lib/gitlab/ci/status/build/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/build/canceled_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Canceled do end describe '.matches?' do - subject {described_class.matches?(build, user) } + subject { described_class.matches?(build, user) } context 'when build is canceled' do let(:build) { create(:ci_build, :canceled) } diff --git a/spec/lib/gitlab/ci/status/build/created_spec.rb b/spec/lib/gitlab/ci/status/build/created_spec.rb index 49468674140..9738b3c1f36 100644 --- a/spec/lib/gitlab/ci/status/build/created_spec.rb +++ b/spec/lib/gitlab/ci/status/build/created_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Created do end describe '.matches?' do - subject {described_class.matches?(build, user) } + subject { described_class.matches?(build, user) } context 'when build is created' do let(:build) { create(:ci_build, :created) } diff --git a/spec/lib/gitlab/ci/status/build/manual_spec.rb b/spec/lib/gitlab/ci/status/build/manual_spec.rb index 150705c1e36..a1152cb77e3 100644 --- a/spec/lib/gitlab/ci/status/build/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/build/manual_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Manual do end describe '.matches?' do - subject {described_class.matches?(build, user) } + subject { described_class.matches?(build, user) } context 'when build is manual' do let(:build) { create(:ci_build, :manual) } diff --git a/spec/lib/gitlab/ci/status/build/pending_spec.rb b/spec/lib/gitlab/ci/status/build/pending_spec.rb index 7b695d33877..b7dda9ce9c9 100644 --- a/spec/lib/gitlab/ci/status/build/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/build/pending_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Pending do end describe '.matches?' do - subject {described_class.matches?(build, user) } + subject { described_class.matches?(build, user) } context 'when build is pending' do let(:build) { create(:ci_build, :pending) } diff --git a/spec/lib/gitlab/ci/status/build/skipped_spec.rb b/spec/lib/gitlab/ci/status/build/skipped_spec.rb index 0b998a52a57..4437ac0089f 100644 --- a/spec/lib/gitlab/ci/status/build/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/build/skipped_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Skipped do end describe '.matches?' do - subject {described_class.matches?(build, user) } + subject { described_class.matches?(build, user) } context 'when build is skipped' do let(:build) { create(:ci_build, :skipped) } diff --git a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb index 91a9724d043..26087fd771c 100644 --- a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb +++ b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource do end describe '.matches?' do - subject {described_class.matches?(processable, user) } + subject { described_class.matches?(processable, user) } context 'when processable is waiting for resource' do let(:processable) { create(:ci_build, :waiting_for_resource) } 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 index 0f97bc06a4e..5ff179b6fee 100644 --- 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 @@ -36,9 +36,10 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do let(:merge_request) { create(:merge_request, :simple, source_project: project) } let(:pipeline) { service.execute(merge_request).payload } - it 'has no jobs' do + it 'creates a pipeline with the expected jobs' do expect(pipeline).to be_merge_request_event - expect(build_names).to be_empty + expect(pipeline.errors.full_messages).to be_empty + expect(build_names).to match_array(%w(kics-iac-sast)) end end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 78d3982a79f..1a909f52ec3 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -44,7 +44,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do context 'when the project is set for deployment to AWS' do let(:platform_value) { 'ECS' } - let(:review_prod_build_names) { build_names.select {|n| n.include?('review') || n.include?('production')} } + let(:review_prod_build_names) { build_names.select { |n| n.include?('review') || n.include?('production') } } before do create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_value) diff --git a/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb b/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb index 1cd88034166..be29543676f 100644 --- a/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb +++ b/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Ci::Trace::RemoteChecksum do end context 'when the response does not include :content_md5' do - let(:metadata) {{}} + let(:metadata) { {} } it 'raises an exception' do expect { subject }.to raise_error KeyError, /content_md5/ @@ -55,7 +55,7 @@ RSpec.describe Gitlab::Ci::Trace::RemoteChecksum do end context 'when the response include :content_md5' do - let(:metadata) {{ content_md5: base64checksum }} + let(:metadata) { { content_md5: base64checksum } } it { is_expected.to eq(checksum) } end diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 8ec0846bdca..6ab2089cce8 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Variables::Builder do + include Ci::TemplateHelpers let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, namespace: group) } let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) } @@ -92,6 +93,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder do value: project.pages_url }, { key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url }, + { key: 'CI_TEMPLATE_REGISTRY_HOST', + value: template_registry_host }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s }, { key: 'CI_PIPELINE_SOURCE', diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 26c560565e0..8ac03301322 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -302,6 +302,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do .append(key: 'CI_BUILD_ID', value: '1') .append(key: 'RAW_VAR', value: '$TEST1', raw: true) .append(key: 'TEST1', value: 'test-3') + .append(key: 'FILEVAR1', value: 'file value 1', file: true) end context 'table tests' do @@ -311,28 +312,23 @@ RSpec.describe Gitlab::Ci::Variables::Collection do { "empty value": { value: '', - result: '', - keep_undefined: false + result: '' }, "simple expansions": { value: 'key$TEST1-$CI_BUILD_ID', - result: 'keytest-3-1', - keep_undefined: false + result: 'keytest-3-1' }, "complex expansion": { value: 'key${TEST1}-${CI_JOB_NAME}', - result: 'keytest-3-test-1', - keep_undefined: false + result: 'keytest-3-test-1' }, "complex expansions with raw variable": { value: 'key${RAW_VAR}-${CI_JOB_NAME}', - result: 'key$TEST1-test-1', - keep_undefined: false + result: 'key$TEST1-test-1' }, "missing variable not keeping original": { value: 'key${MISSING_VAR}-${CI_JOB_NAME}', - result: 'key-test-1', - keep_undefined: false + result: 'key-test-1' }, "missing variable keeping original": { value: 'key${MISSING_VAR}-${CI_JOB_NAME}', @@ -341,14 +337,24 @@ RSpec.describe Gitlab::Ci::Variables::Collection do }, "escaped characters are kept intact": { value: 'key-$TEST1-%%HOME%%-$${HOME}', - result: 'key-test-3-%%HOME%%-$${HOME}', - keep_undefined: false + result: 'key-test-3-%%HOME%%-$${HOME}' + }, + "file variable with expand_file_vars: true": { + value: 'key-$FILEVAR1-$TEST1', + result: 'key-file value 1-test-3' + }, + "file variable with expand_file_vars: false": { + value: 'key-$FILEVAR1-$TEST1', + result: 'key-$FILEVAR1-test-3', + expand_file_vars: false } } end with_them do - subject { collection.expand_value(value, keep_undefined: keep_undefined) } + let(:options) { { keep_undefined: keep_undefined, expand_file_vars: expand_file_vars }.compact } + + subject(:result) { collection.expand_value(value, **options) } it 'matches expected expansion' do is_expected.to eq(result) diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb index f13b334c10e..2a1cdaeb3a7 100644 --- a/spec/lib/gitlab/ci/variables/helpers_spec.rb +++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb @@ -15,21 +15,27 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do end let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.merge_variables(current_variables, new_variables) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when new variables is a hash' do let(:new_variables) do { 'key2' => 'value22', 'key3' => 'value3' } end - it { is_expected.to eq(result) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + it { is_expected.to match_array(result) } end context 'when new variables is a hash with symbol keys' do @@ -37,67 +43,72 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do { key2: 'value22', key3: 'value3' } end - it { is_expected.to eq(result) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] + end + + it { is_expected.to match_array(result) } end context 'when new variables is nil' do let(:new_variables) {} let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end - describe '.transform_to_yaml_variables' do - let(:variables) do - { 'key1' => 'value1', 'key2' => 'value2' } - end + describe '.transform_to_array' do + subject { described_class.transform_to_array(variables) } - let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] - end + context 'when values are strings' do + let(:variables) do + { 'key1' => 'value1', 'key2' => 'value2' } + end - subject { described_class.transform_to_yaml_variables(variables) } + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } + end context 'when variables is nil' do let(:variables) {} - it { is_expected.to eq([]) } - end - end - - describe '.transform_from_yaml_variables' do - let(:variables) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value2', public: true }] + it { is_expected.to match_array([]) } end - let(:result) do - { 'key1' => 'value1', 'key2' => 'value2' } - end + context 'when values are hashes' do + let(:variables) do + { 'key1' => { value: 'value1', description: 'var 1' }, 'key2' => { value: 'value2' } } + end - subject { described_class.transform_from_yaml_variables(variables) } + let(:result) do + [{ key: 'key1', value: 'value1', description: 'var 1' }, + { key: 'key2', value: 'value2' }] + end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } - context 'when variables is nil' do - let(:variables) {} + context 'when a value data has `key` as a key' do + let(:variables) do + { 'key1' => { value: 'value1', key: 'new_key1' }, 'key2' => { value: 'value2' } } + end - it { is_expected.to eq({}) } - end + let(:result) do + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }] + end - context 'when variables is a hash' do - let(:variables) do - { key1: 'value1', 'key2' => 'value2' } + it { is_expected.to match_array(result) } end - - it { is_expected.to eq(result) } end end @@ -115,35 +126,35 @@ RSpec.describe Gitlab::Ci::Variables::Helpers do let(:inheritance) { true } let(:result) do - [{ key: 'key1', value: 'value1', public: true }, - { key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end subject { described_class.inherit_yaml_variables(from: from, to: to, inheritance: inheritance) } - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } context 'when inheritance is false' do let(:inheritance) { false } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end context 'when inheritance is array' do let(:inheritance) { ['key2'] } let(:result) do - [{ key: 'key2', value: 'value22', public: true }, - { key: 'key3', value: 'value3', public: true }] + [{ key: 'key2', value: 'value22' }, + { key: 'key3', value: 'value3' }] end - it { is_expected.to eq(result) } + it { is_expected.to match_array(result) } end end end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 8416501e949..f7a0905d9da 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -72,8 +72,8 @@ module Gitlab it 'returns calculated variables with root and job variables' do is_expected.to match_array([ - { key: 'VAR1', value: 'value 11', public: true }, - { key: 'VAR2', value: 'value 2', public: true } + { key: 'VAR1', value: 'value 11' }, + { key: 'VAR2', value: 'value 2' } ]) end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 22bc6b0db59..3477fe837b4 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -448,7 +448,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -490,7 +490,7 @@ module Gitlab it 'parses the root:variables as #root_variables' do expect(subject.root_variables) - .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true }) + .to contain_exactly({ key: 'SUPPORTED', value: 'parsed' }) end end @@ -1098,8 +1098,8 @@ module Gitlab it 'returns job variables' do expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } + { key: 'VAR1', value: 'value1' }, + { key: 'VAR2', value: 'value2' } ) expect(root_variables_inheritance).to eq(true) end @@ -1203,21 +1203,21 @@ module Gitlab expect(config_processor.builds[0]).to include( name: 'test1', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'test1 var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[1]).to include( name: 'test2', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }, - { key: 'VAR2', value: 'test2 var 2', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }, + { key: 'VAR2', value: 'test2 var 2' }] ) expect(config_processor.builds[2]).to include( name: 'test3', options: { script: ['test'] }, - job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }] + job_variables: [{ key: 'VAR1', value: 'base var 1' }] ) expect(config_processor.builds[3]).to include( @@ -1425,7 +1425,7 @@ module Gitlab it 'returns the parallel config' do build_options = builds.map { |build| build[:options] } parallel_config = { - matrix: parallel[:matrix].map { |var| var.transform_values { |v| Array(v).flatten }}, + matrix: parallel[:matrix].map { |var| var.transform_values { |v| Array(v).flatten } }, total: build_options.size } @@ -1766,6 +1766,7 @@ module Gitlab script: ["make changelog | tee release_changelog.txt"], release: { tag_name: "$CI_COMMIT_TAG", + tag_message: "Annotated tag message", name: "Release $CI_TAG_NAME", description: "./release_changelog.txt", ref: 'b3235930aa443112e639f941c69c578912189bdd', @@ -1956,7 +1957,7 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } context 'no dependencies' do - let(:dependencies) { } + let(:dependencies) {} it { is_expected.to be_valid } end @@ -2012,8 +2013,8 @@ module Gitlab end describe "Job Needs" do - let(:needs) { } - let(:dependencies) { } + let(:needs) {} + let(:dependencies) {} let(:config) do { @@ -2893,7 +2894,7 @@ module Gitlab end end - describe 'Rules' do + describe 'Job rules' do context 'changes' do let(:config) do <<~YAML @@ -2938,6 +2939,49 @@ module Gitlab end end + describe 'Workflow rules' do + context 'changes' do + let(:config) do + <<~YAML + workflow: + rules: + - changes: [README.md] + + rspec: + script: exit 0 + YAML + end + + it 'returns pipeline with correct rules' do + expect(processor.builds.size).to eq(1) + expect(processor.workflow_rules).to eq( + [{ changes: { paths: ["README.md"] } }] + ) + end + + context 'with paths' do + let(:config) do + <<~YAML + workflow: + rules: + - changes: + paths: [README.md] + + rspec: + script: exit 0 + YAML + end + + it 'returns pipeline with correct rules' do + expect(processor.builds.size).to eq(1) + expect(processor.workflow_rules).to eq( + [{ changes: { paths: ["README.md"] } }] + ) + end + end + end + end + describe '#execute' do subject { Gitlab::Ci::YamlProcessor.new(content).execute } diff --git a/spec/lib/gitlab/composer/cache_spec.rb b/spec/lib/gitlab/composer/cache_spec.rb index 071771960c6..a4d632da848 100644 --- a/spec/lib/gitlab/composer/cache_spec.rb +++ b/spec/lib/gitlab/composer/cache_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Gitlab::Composer::Cache do cache_file = Packages::Composer::CacheFile.last freeze_time do - expect { subject }.to change { cache_file.reload.delete_at}.from(nil).to(1.day.from_now) + expect { subject }.to change { cache_file.reload.delete_at }.from(nil).to(1.day.from_now) end end end diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 7173ea43450..0e7d7f1efda 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::CycleAnalytics::StageSummary do + include CycleAnalyticsHelpers + let_it_be(:project) { create(:project, :repository) } let(:options) { { from: 1.day.ago } } diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 9cee0802e87..2c239d5868a 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::DataBuilder::Build do let!(:tag_names) { %w(tag-1 tag-2) } - let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) } + let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n) }) } let(:user) { create(:user, :public_email) } let(:build) { create(:ci_build, :running, runner: runner, user: user) } @@ -33,6 +33,7 @@ RSpec.describe Gitlab::DataBuilder::Build do it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.full_name) } it { expect(data[:pipeline_id]).to eq(build.pipeline.id) } + it { expect(data[:user]).to eq( { @@ -43,6 +44,7 @@ RSpec.describe Gitlab::DataBuilder::Build do email: user.email }) } + it { expect(data[:commit][:id]).to eq(build.pipeline.id) } it { expect(data[:runner][:id]).to eq(build.runner.id) } it { expect(data[:runner][:tags]).to match_array(tag_names) } diff --git a/spec/lib/gitlab/data_builder/issuable_spec.rb b/spec/lib/gitlab/data_builder/issuable_spec.rb index c1ae65c160f..f0802f335f4 100644 --- a/spec/lib/gitlab/data_builder/issuable_spec.rb +++ b/spec/lib/gitlab/data_builder/issuable_spec.rb @@ -113,6 +113,7 @@ RSpec.describe Gitlab::DataBuilder::Issuable do expect(data[:object_attributes]['assignee_id']).to eq(user.id) expect(data[:assignees].first).to eq(user.hook_attrs) expect(data).not_to have_key(:assignee) + expect(data).not_to have_key(:reviewers) end end @@ -126,5 +127,25 @@ RSpec.describe Gitlab::DataBuilder::Issuable do expect(data).not_to have_key(:assignee) end end + + context 'merge_request is assigned reviewers' do + let(:merge_request) { create(:merge_request, reviewers: [user]) } + let(:data) { described_class.new(merge_request).build(user: user) } + + it 'returns correct hook data' do + expect(data[:object_attributes]['reviewer_ids']).to match_array([user.id]) + expect(data[:reviewers].first).to eq(user.hook_attrs) + end + end + + context 'when merge_request does not have reviewers and assignees' do + let(:merge_request) { create(:merge_request) } + let(:data) { described_class.new(merge_request).build(user: user) } + + it 'returns correct hook data' do + expect(data).not_to have_key(:assignees) + expect(data).not_to have_key(:reviewers) + end + end end end diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 469812c80fc..86a1539a836 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -54,7 +54,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do context 'build with runner' do let_it_be(:tag_names) { %w(tag-1 tag-2) } - let_it_be(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) } + let_it_be(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n) }) } let_it_be(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) } it 'has runner attributes', :aggregate_failures do diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index 7eb81a880bf..a3dd4e49e83 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -67,6 +67,7 @@ RSpec.describe Gitlab::DataBuilder::Push do it { expect(data[:project_id]).to eq(15) } it { expect(data[:commits].size).to eq(1) } it { expect(data[:total_commits_count]).to eq(1) } + it 'contains project data' do expect(data[:project]).to be_a(Hash) expect(data[:project][:id]).to eq(15) diff --git a/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb b/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb new file mode 100644 index 00000000000..adb0f45706d --- /dev/null +++ b/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do + include ExclusiveLeaseHelpers + + describe '#perform' do + subject { described_class.new(async_index) } + + let(:async_index) { create(:postgres_async_index, :with_drop) } + + let(:index_model) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex } + + let(:model) { Gitlab::Database.database_base_models[Gitlab::Database::PRIMARY_DATABASE_NAME] } + let(:connection) { model.connection } + + let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) } + let(:lease_key) { "gitlab/database/async_indexes/index_destructor/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" } + let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION } + + before do + connection.add_index(async_index.table_name, 'id', name: async_index.name) + end + + around do |example| + Gitlab::Database::SharedModel.using_connection(connection) do + example.run + end + end + + context 'when the index does not exist' do + before do + connection.execute(async_index.definition) + end + + it 'skips index destruction' do + expect(connection).not_to receive(:execute).with(/DROP INDEX/) + + subject.perform + end + end + + it 'creates the index while controlling lock timeout' do + allow(connection).to receive(:execute).and_call_original + expect(connection).to receive(:execute).with("SET lock_timeout TO '60000ms'").and_call_original + expect(connection).to receive(:execute).with(async_index.definition).and_call_original + expect(connection).to receive(:execute) + .with("RESET idle_in_transaction_session_timeout; RESET lock_timeout") + .and_call_original + + subject.perform + end + + it 'removes the index preparation record from postgres_async_indexes' do + expect(async_index).to receive(:destroy).and_call_original + + expect { subject.perform }.to change { index_model.count }.by(-1) + end + + it 'skips logic if not able to acquire exclusive lease' do + expect(lease).to receive(:try_obtain).ordered.and_return(false) + expect(connection).not_to receive(:execute).with(/DROP INDEX/) + expect(async_index).not_to receive(:destroy) + + expect { subject.perform }.not_to change { index_model.count } + end + end +end diff --git a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb index 9ba3dad72b3..52f5e37eff2 100644 --- a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb @@ -142,4 +142,42 @@ RSpec.describe Gitlab::Database::AsyncIndexes::MigrationHelpers do end end end + + describe '#prepare_async_index_removal' do + before do + connection.create_table(table_name) + connection.add_index(table_name, 'id', name: index_name) + end + + it 'creates the record for the async index removal' do + expect do + migration.prepare_async_index_removal(table_name, 'id', name: index_name) + end.to change { index_model.where(name: index_name).count }.by(1) + + record = index_model.find_by(name: index_name) + + expect(record.table_name).to eq(table_name) + expect(record.definition).to match(/DROP INDEX CONCURRENTLY "#{index_name}"/) + end + + context 'when the index does not exist' do + it 'does not create the record' do + connection.remove_index(table_name, 'id', name: index_name) + + expect do + migration.prepare_async_index_removal(table_name, 'id', name: index_name) + end.not_to change { index_model.where(name: index_name).count } + end + end + + context 'when the record already exists' do + it 'does attempt to create the record' do + create(:postgres_async_index, table_name: table_name, name: index_name) + + expect do + migration.prepare_async_index_removal(table_name, 'id', name: index_name) + end.not_to change { index_model.where(name: index_name).count } + end + end + end end diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb index 223730f87c0..806d57af4b3 100644 --- a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb +++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb @@ -16,4 +16,21 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model it { is_expected.to validate_presence_of(:definition) } it { is_expected.to validate_length_of(:definition).is_at_most(definition_limit) } end + + describe 'scopes' do + let!(:async_index_creation) { create(:postgres_async_index) } + let!(:async_index_destruction) { create(:postgres_async_index, :with_drop) } + + describe '.to_create' do + subject { described_class.to_create } + + it { is_expected.to contain_exactly(async_index_creation) } + end + + describe '.to_drop' do + subject { described_class.to_drop } + + it { is_expected.to contain_exactly(async_index_destruction) } + end + end end diff --git a/spec/lib/gitlab/database/async_indexes_spec.rb b/spec/lib/gitlab/database/async_indexes_spec.rb index 74e30ea2c4e..8a5509f892f 100644 --- a/spec/lib/gitlab/database/async_indexes_spec.rb +++ b/spec/lib/gitlab/database/async_indexes_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes do end it 'takes 2 pending indexes and creates those' do - Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.order(:id).limit(2).each do |index| + Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.to_create.order(:id).limit(2).each do |index| creator = double('index creator') expect(Gitlab::Database::AsyncIndexes::IndexCreator).to receive(:new).with(index).and_return(creator) expect(creator).to receive(:perform) @@ -20,4 +20,22 @@ RSpec.describe Gitlab::Database::AsyncIndexes do subject end end + + describe '.drop_pending_indexes!' do + subject { described_class.drop_pending_indexes! } + + before do + create_list(:postgres_async_index, 4, :with_drop) + end + + it 'takes 2 pending indexes and destroys those' do + Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.to_drop.order(:id).limit(2).each do |index| + destructor = double('index destructor') + expect(Gitlab::Database::AsyncIndexes::IndexDestructor).to receive(:new).with(index).and_return(destructor) + expect(destructor).to receive(:perform) + end + + subject + end + end end diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb index a7b3670da7c..32746a46308 100644 --- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb @@ -304,6 +304,13 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d it { expect(subject).to be_falsey } end + + context 'when the batch_size is 1' do + let(:job) { create(:batched_background_migration_job, :failed, batch_size: 1) } + let(:exception) { ActiveRecord::StatementTimeout.new } + + it { expect(subject).to be_falsey } + end end describe '#time_efficiency' do @@ -415,10 +422,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d end context 'when batch size is already 1' do - let!(:job) { create(:batched_background_migration_job, :failed, batch_size: 1) } + let!(:job) { create(:batched_background_migration_job, :failed, batch_size: 1, attempts: 3) } - it 'raises an exception' do - expect { job.split_and_retry! }.to raise_error 'Job cannot be split further' + it 'keeps the same batch size' do + job.split_and_retry! + + expect(job.reload.batch_size).to eq 1 + end + + it 'resets the number of attempts' do + job.split_and_retry! + + expect(job.attempts).to eq 0 end end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb index b8ff78be333..4ef2e7f936b 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb @@ -15,8 +15,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end before do - allow(Gitlab::Database::BackgroundMigration::HealthStatus).to receive(:evaluate) - .and_return(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal) + normal_signal = instance_double(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal, stop?: false) + allow(Gitlab::Database::BackgroundMigration::HealthStatus).to receive(:evaluate).and_return([normal_signal]) end describe '#run_migration_job' do @@ -77,14 +77,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end it 'puts migration on hold on stop signal' do - expect(health_status).to receive(:evaluate).and_return(stop_signal) + expect(health_status).to receive(:evaluate).and_return([stop_signal]) expect { runner.run_migration_job(migration) }.to change { migration.on_hold? } .from(false).to(true) end it 'optimizes migration on normal signal' do - expect(health_status).to receive(:evaluate).and_return(normal_signal) + expect(health_status).to receive(:evaluate).and_return([normal_signal]) expect(migration).to receive(:optimize!) @@ -92,7 +92,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end it 'optimizes migration on no signal' do - expect(health_status).to receive(:evaluate).and_return(not_available_signal) + expect(health_status).to receive(:evaluate).and_return([not_available_signal]) expect(migration).to receive(:optimize!) @@ -100,7 +100,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end it 'optimizes migration on unknown signal' do - expect(health_status).to receive(:evaluate).and_return(unknown_signal) + expect(health_status).to receive(:evaluate).and_return([unknown_signal]) expect(migration).to receive(:optimize!) 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 55f607c0cb0..06c2bc32db3 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -307,7 +307,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end describe '#batch_class' do - let(:batch_class) { Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy} + let(:batch_class) { Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy } let(:batched_migration) { build(:batched_background_migration) } it 'returns the class of the batch strategy for the migration' do @@ -617,6 +617,49 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m end end + describe '#progress' do + subject { migration.progress } + + context 'when the migration is finished' do + let(:migration) do + create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record| + create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 1) + end + end + + it 'returns 100' do + expect(subject).to be 100 + end + end + + context 'when the migration does not have jobs' do + let(:migration) { create(:batched_background_migration, :active) } + + it 'returns zero' do + expect(subject).to be 0 + end + end + + context 'when the `total_tuple_count` is zero' do + let(:migration) { create(:batched_background_migration, :active, total_tuple_count: 0) } + let!(:batched_job) { create(:batched_background_migration_job, :succeeded, batched_migration: migration) } + + it 'returns nil' do + expect(subject).to be nil + end + end + + context 'when migration has completed jobs' do + let(:migration) { create(:batched_background_migration, :active, total_tuple_count: 100) } + + let!(:batched_job) { create(:batched_background_migration_job, :succeeded, batched_migration: migration, batch_size: 8) } + + it 'calculates the progress' do + expect(subject).to be 8 + end + end + end + describe '.for_configuration' do let!(:attributes) do { 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 83c0275a870..983f482d464 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 @@ -38,10 +38,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' batch_column: 'id', sub_batch_size: 1, pause_ms: pause_ms, + job_arguments: active_migration.job_arguments, connection: connection) .and_return(job_instance) - expect(job_instance).to receive(:perform).with('id', 'other_id') + expect(job_instance).to receive(:perform).with(no_args) perform end @@ -49,7 +50,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' it 'updates the tracking record in the database' do test_metrics = { 'my_metrics' => 'some value' } - expect(job_instance).to receive(:perform).with('id', 'other_id') + expect(job_instance).to receive(:perform).with(no_args) expect(job_instance).to receive(:batch_metrics).and_return(test_metrics) freeze_time do @@ -78,7 +79,7 @@ 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).with('id', 'other_id') + expect(job_instance).to receive(:perform).with(no_args) expect(job_instance).to receive(:batch_metrics).and_return(updated_metrics) freeze_time do @@ -97,7 +98,7 @@ 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('id', 'other_id') + expect(job_instance).to receive(:perform).with(no_args) freeze_time do perform @@ -110,7 +111,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end it 'tracks metrics of the execution' do - expect(job_instance).to receive(:perform).with('id', 'other_id') + expect(job_instance).to receive(:perform).with(no_args) expect(metrics_tracker).to receive(:track).with(job_record) perform @@ -120,7 +121,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' 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('id', 'other_id').and_raise(error_class) + expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class) freeze_time do expect { perform }.to raise_error(error_class) @@ -133,7 +134,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end it 'tracks metrics of the execution' do - expect(job_instance).to receive(:perform).with('id', 'other_id').and_raise(error_class) + expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class) expect(metrics_tracker).to receive(:track).with(job_record) expect { perform }.to raise_error(error_class) @@ -147,6 +148,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' context 'when the batched background migration does not inherit from BatchedMigrationJob' do let(:job_class) { Class.new } + let(:job_instance) { job_class.new } it 'runs the job with the correct arguments' do expect(job_class).to receive(:new).with(no_args).and_return(job_instance) diff --git a/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb b/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb index 21204814f17..db4383a79d4 100644 --- a/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb +++ b/spec/lib/gitlab/database/background_migration/health_status/indicators/autovacuum_active_on_table_spec.rb @@ -20,9 +20,9 @@ RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus::Indicators:: swapout_view_for_table(:postgres_autovacuum_activity) end - let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(tables) } let(:tables) { [table] } let(:table) { 'users' } + let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(connection, tables) } context 'without autovacuum activity' do it 'returns Normal signal' do diff --git a/spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb b/spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb new file mode 100644 index 00000000000..650f11e3cd5 --- /dev/null +++ b/spec/lib/gitlab/database/background_migration/health_status/indicators/write_ahead_log_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog do + let(:connection) { Gitlab::Database.database_base_models[:main].connection } + + around do |example| + Gitlab::Database::SharedModel.using_connection(connection) do + example.run + end + end + + describe '#evaluate' do + let(:tables) { [table] } + let(:table) { 'users' } + let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(connection, tables) } + + subject(:evaluate) { described_class.new(context).evaluate } + + it 'remembers the indicator class' do + expect(evaluate.indicator_class).to eq(described_class) + end + + it 'returns NoSignal signal in case the feature flag is disabled' do + stub_feature_flags(batched_migrations_health_status_wal: false) + + expect(evaluate).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::NotAvailable) + expect(evaluate.reason).to include('indicator disabled') + end + + it 'returns NoSignal signal when WAL archive queue can not be calculated' do + expect(connection).to receive(:execute).and_return([{ 'pending_wal_count' => nil }]) + + expect(evaluate).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::NotAvailable) + expect(evaluate.reason).to include('WAL archive queue can not be calculated') + end + + it 'uses primary database' do + expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary).and_yield + + evaluate + end + + context 'when WAL archive queue size is below the limit' do + it 'returns Normal signal' do + expect(connection).to receive(:execute).and_return([{ 'pending_wal_count' => 1 }]) + expect(evaluate).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal) + expect(evaluate.reason).to include('WAL archive queue is within limit') + end + end + + context 'when WAL archive queue size is above the limit' do + it 'returns Stop signal' do + expect(connection).to receive(:execute).and_return([{ 'pending_wal_count' => 420 }]) + expect(evaluate).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Stop) + expect(evaluate.reason).to include('WAL archive queue is too big') + end + end + end +end diff --git a/spec/lib/gitlab/database/background_migration/health_status_spec.rb b/spec/lib/gitlab/database/background_migration/health_status_spec.rb index 6d0430dcbbb..8bc04d80fa1 100644 --- a/spec/lib/gitlab/database/background_migration/health_status_spec.rb +++ b/spec/lib/gitlab/database/background_migration/health_status_spec.rb @@ -12,30 +12,47 @@ RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do end describe '.evaluate' do - subject(:evaluate) { described_class.evaluate(migration, indicator_class) } + subject(:evaluate) { described_class.evaluate(migration, [autovacuum_indicator_class]) } let(:migration) { build(:batched_background_migration, :active) } - let(:health_status) { 'Gitlab::Database::BackgroundMigration::HealthStatus' } - let(:indicator_class) { class_double("#{health_status}::Indicators::AutovacuumActiveOnTable") } - let(:indicator) { instance_double("#{health_status}::Indicators::AutovacuumActiveOnTable") } + let(:health_status) { Gitlab::Database::BackgroundMigration::HealthStatus } + let(:autovacuum_indicator_class) { health_status::Indicators::AutovacuumActiveOnTable } + let(:wal_indicator_class) { health_status::Indicators::WriteAheadLog } + let(:autovacuum_indicator) { instance_double(autovacuum_indicator_class) } + let(:wal_indicator) { instance_double(wal_indicator_class) } before do - allow(indicator_class).to receive(:new).with(migration.health_context).and_return(indicator) + allow(autovacuum_indicator_class).to receive(:new).with(migration.health_context).and_return(autovacuum_indicator) end - it 'returns a signal' do + context 'with default indicators' do + subject(:evaluate) { described_class.evaluate(migration) } + + it 'returns a collection of signals' do + normal_signal = instance_double("#{health_status}::Signals::Normal", log_info?: false) + not_available_signal = instance_double("#{health_status}::Signals::NotAvailable", log_info?: false) + + expect(autovacuum_indicator).to receive(:evaluate).and_return(normal_signal) + expect(wal_indicator_class).to receive(:new).with(migration.health_context).and_return(wal_indicator) + expect(wal_indicator).to receive(:evaluate).and_return(not_available_signal) + + expect(evaluate).to contain_exactly(normal_signal, not_available_signal) + end + end + + it 'returns a collection of signals' do signal = instance_double("#{health_status}::Signals::Normal", log_info?: false) - expect(indicator).to receive(:evaluate).and_return(signal) + expect(autovacuum_indicator).to receive(:evaluate).and_return(signal) - expect(evaluate).to eq(signal) + expect(evaluate).to contain_exactly(signal) end it 'logs interesting signals' do signal = instance_double("#{health_status}::Signals::Stop", log_info?: true) - expect(indicator).to receive(:evaluate).and_return(signal) + expect(autovacuum_indicator).to receive(:evaluate).and_return(signal) expect(described_class).to receive(:log_signal).with(signal, migration) evaluate @@ -44,7 +61,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do it 'does not log signals of no interest' do signal = instance_double("#{health_status}::Signals::Normal", log_info?: false) - expect(indicator).to receive(:evaluate).and_return(signal) + expect(autovacuum_indicator).to receive(:evaluate).and_return(signal) expect(described_class).not_to receive(:log_signal) evaluate @@ -54,7 +71,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do let(:error) { RuntimeError.new('everything broken') } before do - expect(indicator).to receive(:evaluate).and_raise(error) + expect(autovacuum_indicator).to receive(:evaluate).and_raise(error) end it 'does not fail' do @@ -62,8 +79,10 @@ RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do end it 'returns Unknown signal' do - expect(evaluate).to be_an_instance_of(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Unknown) - expect(evaluate.reason).to eq("unexpected error: everything broken (RuntimeError)") + signal = evaluate.first + + expect(signal).to be_an_instance_of(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Unknown) + expect(signal.reason).to eq("unexpected error: everything broken (RuntimeError)") end it 'reports the exception to error tracking' do diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb index 08b4d50f83b..fa519cffd6b 100644 --- a/spec/lib/gitlab/database/bulk_update_spec.rb +++ b/spec/lib/gitlab/database/bulk_update_spec.rb @@ -91,7 +91,8 @@ RSpec.describe Gitlab::Database::BulkUpdate do .to eq(['MR a', 'Issue a', 'Issue b']) end - context 'validates prepared_statements support', :reestablished_active_record_base do + context 'validates prepared_statements support', :reestablished_active_record_base, + :suppress_gitlab_schemas_validate_connection do using RSpec::Parameterized::TableSyntax where(:prepared_statements) do diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb index 34eb64997c1..9c09253b24c 100644 --- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb @@ -358,7 +358,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do end it 'returns true for deeply wrapped/nested errors' do - top = twice_wrapped_exception(ActionView::Template::Error, ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished) + top = twice_wrapped_exception( + ActionView::Template::Error, + ActiveRecord::StatementInvalid, + ActiveRecord::ConnectionNotEstablished + ) expect(lb.connection_error?(top)).to eq(true) end @@ -404,7 +408,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do end describe '#select_up_to_date_host' do - let(:location) { 'AB/12345'} + let(:location) { 'AB/12345' } let(:hosts) { lb.host_list.hosts } let(:set_host) { request_cache[described_class::CACHE_KEY] } @@ -455,7 +459,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do end it 'does not modify connection class pool' do - expect { with_replica_pool(5) { } }.not_to change { ActiveRecord::Base.connection_pool } + expect { with_replica_pool(5) {} }.not_to change { ActiveRecord::Base.connection_pool } end def with_replica_pool(*args) diff --git a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb index b768d4ecea3..a1c141af537 100644 --- a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb @@ -30,6 +30,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do expect(app).to receive(:call).with(env).and_return(10) + allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original + expect(ActiveSupport::Notifications) .to receive(:instrument) .with('web_transaction_completed.load_balancing') diff --git a/spec/lib/gitlab/database/load_balancing/session_spec.rb b/spec/lib/gitlab/database/load_balancing/session_spec.rb index 74512f76fd4..05b44579c62 100644 --- a/spec/lib/gitlab/database/load_balancing/session_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/session_spec.rb @@ -132,7 +132,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Session do it 'does not prevent using primary if an exception is raised' do instance = described_class.new - instance.ignore_writes { raise ArgumentError } rescue ArgumentError + begin + instance.ignore_writes { raise ArgumentError } + rescue ArgumentError + nil + end instance.write! expect(instance).to be_using_primary diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb index 31be3963565..8053bd57bba 100644 --- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_gitlab_redis_queues do let(:middleware) { described_class.new } let(:worker) { worker_class.new } - let(:location) {'0/D525E3A8' } + let(:location) { '0/D525E3A8' } let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } } let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations } } diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb index f3139bb1b4f..2ffb2c32c32 100644 --- a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb @@ -77,6 +77,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do let(:last_write_location) { 'foo' } before do + allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original + allow(sticking) .to receive(:last_write_location_for) .with(:user, 42) diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb index f320fe0276f..76dfaa74ae6 100644 --- a/spec/lib/gitlab/database/load_balancing_spec.rb +++ b/spec/lib/gitlab/database/load_balancing_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::LoadBalancing do +RSpec.describe Gitlab::Database::LoadBalancing, :suppress_gitlab_schemas_validate_connection do describe '.base_models' do it 'returns the models to apply load balancing to' do models = described_class.base_models diff --git a/spec/lib/gitlab/database/lock_writes_manager_spec.rb b/spec/lib/gitlab/database/lock_writes_manager_spec.rb new file mode 100644 index 00000000000..eb527d492cf --- /dev/null +++ b/spec/lib/gitlab/database/lock_writes_manager_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::LockWritesManager do + let(:connection) { ApplicationRecord.connection } + let(:test_table) { '_test_table' } + let(:logger) { instance_double(Logger) } + + subject(:lock_writes_manager) do + described_class.new( + table_name: test_table, + connection: connection, + database_name: 'main', + logger: logger + ) + end + + before do + allow(logger).to receive(:info) + + connection.execute(<<~SQL) + CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0); + + INSERT INTO #{test_table} (id, value) + VALUES (1, 1), (2, 2), (3, 3) + SQL + end + + describe '#lock_writes' do + it 'prevents any writes on the table' do + subject.lock_writes + + expect do + connection.execute("delete from #{test_table}") + end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/) + end + + it 'prevents truncating the table' do + subject.lock_writes + + expect do + connection.execute("truncate #{test_table}") + end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/) + end + + it 'adds 3 triggers to the ci schema tables on the main database' do + expect do + subject.lock_writes + end.to change { + number_of_triggers_on(connection, test_table) + }.by(3) # Triggers to block INSERT / UPDATE / DELETE + # Triggers on TRUNCATE are not added to the information_schema.triggers + # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us + end + + it 'logs the write locking' do + expect(logger).to receive(:info).with("Database: 'main', Table: '_test_table': Lock Writes") + + subject.lock_writes + end + + it 'retries again if it receives a statement_timeout a few number of times' do + error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout" + call_count = 0 + allow(connection).to receive(:execute) do |statement| + if statement.include?("CREATE TRIGGER") + call_count += 1 + raise(ActiveRecord::QueryCanceled, error_message) if call_count.even? + end + end + subject.lock_writes + end + + it 'raises the exception if it happened many times' do + error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout" + allow(connection).to receive(:execute) do |statement| + if statement.include?("CREATE TRIGGER") + raise(ActiveRecord::QueryCanceled, error_message) + end + end + + expect do + subject.lock_writes + end.to raise_error(ActiveRecord::QueryCanceled) + end + end + + describe '#unlock_writes' do + before do + subject.lock_writes + end + + it 'allows writing on the table again' do + subject.unlock_writes + + expect do + connection.execute("delete from #{test_table}") + end.not_to raise_error + end + + it 'removes the write protection triggers from the gitlab_main tables on the ci database' do + expect do + subject.unlock_writes + end.to change { + number_of_triggers_on(connection, test_table) + }.by(-3) # Triggers to block INSERT / UPDATE / DELETE + # Triggers on TRUNCATE are not added to the information_schema.triggers + # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us + end + + it 'logs the write unlocking' do + expect(logger).to receive(:info).with("Database: 'main', Table: '_test_table': Allow Writes") + + subject.unlock_writes + end + end + + def number_of_triggers_on(connection, table_name) + connection + .select_value("SELECT count(*) FROM information_schema.triggers WHERE event_object_table=$1", nil, [table_name]) + end +end diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb index 87a3e0f81e4..ff99f681b0c 100644 --- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb @@ -84,4 +84,32 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do end end end + + describe '.definitions' do + subject(:definitions) { described_class.definitions } + + it 'contains at least all parent tables that have triggers' do + all_definition_parent_tables = definitions.map { |d| d.to_table }.to_set + + triggers_query = <<~SQL + SELECT event_object_table, trigger_name + FROM information_schema.triggers + WHERE trigger_name LIKE '%_loose_fk_trigger' + GROUP BY event_object_table, trigger_name + SQL + + all_triggers = ApplicationRecord.connection.execute(triggers_query) + + all_triggers.each do |trigger| + table = trigger['event_object_table'] + trigger_name = trigger['trigger_name'] + error_message = <<~END + Missing a loose foreign key definition for parent table: #{table} with trigger: #{trigger_name}. + Loose foreign key definitions must be added before triggers are added and triggers must be removed before removing the loose foreign key definition. + Read more at https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html ." + END + expect(all_definition_parent_tables).to include(table), error_message + end + 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 1009ec354c3..e43cfe0814e 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 @@ -5,6 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_analyzers: false, stub_feature_flags: false do let(:schema_class) { Class.new(Gitlab::Database::Migration[1.0]).include(described_class) } + # We keep only the GitlabSchemasValidateConnection analyzer running + around do |example| + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed(false) do + example.run + end + end + describe '#restrict_gitlab_migration' do it 'invalid schema raises exception' do expect { schema_class.restrict_gitlab_migration gitlab_schema: :gitlab_non_exisiting } diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb index 5c054795697..2055dc33d48 100644 --- a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb @@ -266,7 +266,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } } it 'sets the migration class name in the logs' do - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} buffer.rewind expect(buffer.read).to include("\"class\":\"#{model.class}\"") @@ -280,7 +280,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: raise_on_exhaustion) - model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) { } + model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) {} end end @@ -289,7 +289,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} end it 'defaults to disallowing subtransactions' do @@ -297,7 +297,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do expect(Gitlab::Database::WithLockRetries).to receive(:new).with(hash_including(allow_savepoints: false)).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} end context 'when in transaction' do @@ -323,7 +323,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do end it 'raises an error' do - expect { model.with_lock_retries(env: env, logger: in_memory_logger) { } }.to raise_error /can not be run inside an already open transaction/ + expect { model.with_lock_retries(env: env, logger: in_memory_logger) {} }.to raise_error /can not be run inside an already open transaction/ end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 3ccc3a17862..dd5ad40d8ef 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end describe 'overridden dynamic model helpers' do - let(:test_table) { '__test_batching_table' } + let(:test_table) { '_test_batching_table' } before do model.connection.execute(<<~SQL) @@ -1022,6 +1022,40 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(Project.sum(:star_count)).to eq(2 * Project.count) end end + + context 'when the table is write-locked' do + let(:test_table) { '_test_table' } + let(:lock_writes_manager) do + Gitlab::Database::LockWritesManager.new( + table_name: test_table, + connection: model.connection, + database_name: 'main' + ) + end + + before do + model.connection.execute(<<~SQL) + CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0); + + INSERT INTO #{test_table} (id, value) + VALUES (1, 1), (2, 2), (3, 3) + SQL + + lock_writes_manager.lock_writes + end + + it 'disables the write-lock trigger function' do + expect do + model.update_column_in_batches(test_table, :value, Arel.sql('1+1'), disable_lock_writes: true) + end.not_to raise_error + end + + it 'raises an error if it does not disable the trigger function' do + expect do + model.update_column_in_batches(test_table, :value, Arel.sql('1+1'), disable_lock_writes: false) + end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/) + end + end end context 'when running inside the transaction' do @@ -1080,6 +1114,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'renames a column concurrently' do + expect(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection).to receive(:with_suppressed).and_yield + expect(model).to receive(:check_trigger_permissions!).with(:users) expect(model).to receive(:install_rename_triggers) @@ -1112,6 +1148,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do let(:connection) { ActiveRecord::Migration.connection } before do + expect(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection).to receive(:with_suppressed).and_yield expect(Gitlab::Database::UnidirectionalCopyTrigger).to receive(:on_table) .with(:users, connection: connection).and_return(copy_trigger) end @@ -1119,6 +1156,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'copies the value to the new column using the type_cast_function', :aggregate_failures do expect(model).to receive(:copy_indexes).with(:users, :id, :new) expect(model).to receive(:add_not_null_constraint).with(:users, :new) + expect(model).to receive(:execute).with("SELECT set_config('lock_writes.users', 'false', true)") expect(model).to receive(:execute).with("UPDATE \"users\" SET \"new\" = cast_to_jsonb_with_default(\"users\".\"id\") WHERE \"users\".\"id\" >= #{user.id}") expect(copy_trigger).to receive(:create).with(:id, :new, trigger_name: nil) @@ -1165,6 +1203,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'copies the default to the new column' do + expect(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection).to receive(:with_suppressed).and_yield + expect(model).to receive(:change_column_default) .with(:users, :new, old_column.default) @@ -1176,6 +1216,34 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + context 'when the table in the other database is write-locked' do + let(:test_table) { '_test_table' } + let(:lock_writes_manager) do + Gitlab::Database::LockWritesManager.new( + table_name: test_table, + connection: model.connection, + database_name: 'main' + ) + end + + before do + model.connection.execute(<<~SQL) + CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0); + + INSERT INTO #{test_table} (id, value) + VALUES (1, 1), (2, 2), (3, 3) + SQL + + lock_writes_manager.lock_writes + end + + it 'does not raise an error when renaming the column' do + expect do + model.rename_column_concurrently(test_table, :value, :new_value) + end.not_to raise_error + end + end + context 'when the column to be renamed does not exist' do before do allow(model).to receive(:columns).and_return([]) @@ -1246,6 +1314,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'reverses the operations of cleanup_concurrent_column_rename' do + expect(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection).to receive(:with_suppressed).and_yield + expect(model).to receive(:check_trigger_permissions!).with(:users) expect(model).to receive(:install_rename_triggers) @@ -1302,6 +1372,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'copies the default to the old column' do + expect(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection).to receive(:with_suppressed).and_yield + expect(model).to receive(:change_column_default) .with(:users, :old, new_column.default) @@ -2438,7 +2510,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } } it 'sets the migration class name in the logs' do - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} buffer.rewind expect(buffer.read).to include("\"class\":\"#{model.class}\"") @@ -2452,7 +2524,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: raise_on_exhaustion) - model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) { } + model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) {} end end @@ -2461,7 +2533,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} end it 'defaults to allowing subtransactions' do @@ -2470,7 +2542,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(Gitlab::Database::WithLockRetries).to receive(:new).with(hash_including(allow_savepoints: true)).and_return(with_lock_retries) expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false) - model.with_lock_retries(env: env, logger: in_memory_logger) { } + model.with_lock_retries(env: env, logger: in_memory_logger) {} 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 c423340a572..f21f1ac5e52 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -37,12 +37,6 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do freeze_time { example.run } end - before do - User.class_eval do - include EachBatch - end - end - it 'returns the final expected delay' do Sidekiq::Testing.fake! do final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2) 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 5bfb2516ba1..a2f6e6b43ed 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 @@ -15,12 +15,25 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d describe '#queue_batched_background_migration' do let(:pgclass_info) { instance_double('Gitlab::Database::PgClass', cardinality_estimate: 42) } + let(:job_class) do + Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do + def self.name + 'MyJobClass' + end + end + end before do allow(Gitlab::Database::PgClass).to receive(:for_table).and_call_original expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!) allow(migration).to receive(:transaction_open?).and_return(false) + + stub_const("Gitlab::Database::BackgroundMigration::BatchedMigration::JOB_CLASS_MODULE", '') + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |batched_migration| + allow(batched_migration).to receive(:job_class) + .and_return(job_class) + end end context 'when such migration already exists' do @@ -42,7 +55,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d expect do migration.queue_batched_background_migration( - 'MyJobClass', + job_class.name, :projects, :id, [:id], [:id_convert_to_bigint], @@ -62,7 +75,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d expect do migration.queue_batched_background_migration( - 'MyJobClass', + job_class.name, :projects, :id, job_interval: 5.minutes, @@ -97,7 +110,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d it 'sets the job interval to the minimum value' do expect do - migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: minimum_delay - 1.minute) + migration.queue_batched_background_migration(job_class.name, :events, :id, job_interval: minimum_delay - 1.minute) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last @@ -107,26 +120,76 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end context 'when additional arguments are passed to the method' do - it 'saves the arguments on the database record' do - expect do - migration.queue_batched_background_migration( - 'MyJobClass', - :projects, - :id, - 'my', - 'arguments', - job_interval: 5.minutes, - batch_max_value: 1000) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + context 'when the job class provides job_arguments_count' do + context 'when defined job arguments for the job class does not match provided arguments' do + it 'raises an error' do + expect do + migration.queue_batched_background_migration( + job_class.name, + :projects, + :id, + 'my', + 'arguments', + job_interval: 2.minutes) + end.to raise_error(RuntimeError, /Wrong number of job arguments for MyJobClass \(given 2, expected 0\)/) + end + end - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( - job_class_name: 'MyJobClass', - table_name: 'projects', - column_name: 'id', - interval: 300, - min_value: 1, - max_value: 1000, - job_arguments: %w[my arguments]) + context 'when defined job arguments for the job class match provided arguments' do + let(:job_class) do + Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do + def self.name + 'MyJobClass' + end + + job_arguments :foo, :bar + end + end + + it 'saves the arguments on the database record' do + expect do + migration.queue_batched_background_migration( + job_class.name, + :projects, + :id, + 'my', + 'arguments', + job_interval: 5.minutes, + batch_max_value: 1000) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( + job_class_name: 'MyJobClass', + table_name: 'projects', + column_name: 'id', + interval: 300, + min_value: 1, + max_value: 1000, + job_arguments: %w[my arguments]) + end + end + end + + context 'when the job class does not provide job_arguments_count' do + let(:job_class) do + Class.new do + def self.name + 'MyJobClass' + end + end + end + + it 'does not raise an error' do + expect do + migration.queue_batched_background_migration( + job_class.name, + :projects, + :id, + 'my', + 'arguments', + job_interval: 2.minutes) + end.not_to raise_error + end end end @@ -138,7 +201,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d it 'creates the record with the current max value' do expect do - migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + migration.queue_batched_background_migration(job_class.name, :events, :id, job_interval: 5.minutes) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last @@ -148,7 +211,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d it 'creates the record with an active status' do expect do - migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + migration.queue_batched_background_migration(job_class.name, :events, :id, job_interval: 5.minutes) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_active @@ -158,7 +221,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d context 'when the database is empty' do it 'sets the max value to the min value' do expect do - migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + migration.queue_batched_background_migration(job_class.name, :events, :id, job_interval: 5.minutes) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last @@ -168,7 +231,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d it 'creates the record with a finished status' do expect do - migration.queue_batched_background_migration('MyJobClass', :projects, :id, job_interval: 5.minutes) + migration.queue_batched_background_migration(job_class.name, :projects, :id, job_interval: 5.minutes) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_finished @@ -181,7 +244,7 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d expect(migration).to receive(:gitlab_schema_from_context).and_return(:gitlab_ci) expect do - migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + migration.queue_batched_background_migration(job_class.name, :events, :id, job_interval: 5.minutes) end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb index c31244060ec..3540a120b8f 100644 --- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb +++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb @@ -122,7 +122,11 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do it 'records observations for all migrations' do subject.observe(version: migration_version, name: migration_name, connection: connection) {} - subject.observe(version: migration_version_2, name: migration_name_2, connection: connection) { raise 'something went wrong' } rescue nil + begin + subject.observe(version: migration_version_2, name: migration_name_2, connection: connection) { raise 'something went wrong' } + rescue StandardError + nil + end expect { load_observation(result_dir, migration_name) }.not_to raise_error expect { load_observation(result_dir, migration_name_2) }.not_to raise_error diff --git a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb index 50ad77caaf1..6092d985ce8 100644 --- a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb +++ b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb @@ -83,10 +83,10 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do context 'with transactions disabled' do let(:migration) { double('migration', enable_lock_retries?: false) } - let(:receiver) { double('receiver', use_transaction?: false)} + let(:receiver) { double('receiver', use_transaction?: false) } it 'calls super method' do - p = proc { } + p = proc {} expect(receiver).to receive(:ddl_transaction).with(migration, &p) @@ -95,11 +95,11 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do end context 'with transactions enabled, but lock retries disabled' do - let(:receiver) { double('receiver', use_transaction?: true)} + let(:receiver) { double('receiver', use_transaction?: true) } let(:migration) { double('migration', enable_lock_retries?: false) } it 'calls super method' do - p = proc { } + p = proc {} expect(receiver).to receive(:ddl_transaction).with(migration, &p) @@ -108,12 +108,12 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do end context 'with transactions enabled and lock retries enabled' do - let(:receiver) { double('receiver', use_transaction?: true)} + let(:receiver) { double('receiver', use_transaction?: true) } let(:migration) { double('migration', migration_connection: connection, enable_lock_retries?: true) } let(:connection) { ActiveRecord::Base.connection } it 'calls super method' do - p = proc { } + p = proc {} expect(receiver).not_to receive(:ddl_transaction) expect_next_instance_of(Gitlab::Database::WithLockRetries) do |retries| diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb index e7f68e3e4a8..a37247ba0c6 100644 --- a/spec/lib/gitlab/database/migrations/runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/runner_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Gitlab::Database::Migrations::Runner do allow(described_class).to receive(:migration_context).and_return(ctx) - names_this_branch = (applied_migrations_this_branch + pending_migrations).map { |m| "db/migrate/#{m.version}_#{m.name}.rb"} + names_this_branch = (applied_migrations_this_branch + pending_migrations).map { |m| "db/migrate/#{m.version}_#{m.name}.rb" } allow(described_class).to receive(:migration_file_names_this_branch).and_return(names_this_branch) end diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb index f1f72d71e1a..9451a6bd34a 100644 --- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez let(:connection) { ApplicationRecord.connection } - let(:table_name) { "_test_column_copying"} + let(:table_name) { "_test_column_copying" } before do connection.execute(<<~SQL) @@ -50,18 +50,16 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez context 'with jobs to run' do let(:migration_name) { 'TestBackgroundMigration' } - before do - migration.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 + migration.queue_batched_background_migration(migration_name, table_name, :id, + job_interval: 5.minutes, + batch_size: 100) + 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 @@ -70,6 +68,9 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez context 'with multiple jobs to run' do it 'runs all jobs created within the last 3 hours' do old_migration = define_background_migration(migration_name) + migration.queue_batched_background_migration(migration_name, table_name, :id, + job_interval: 5.minutes, + batch_size: 100) travel 4.hours diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb index d8b06ee1a5d..04b9fba5b2f 100644 --- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb +++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb @@ -48,61 +48,43 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do end describe '#validate_and_fix' do - context 'feature flag is disabled' do - before do - stub_feature_flags(fix_sliding_list_partitioning: false) - end + it 'does not call change_column_default if the partitioning in a valid state' do + expect(strategy.model.connection).not_to receive(:change_column_default) - it 'does not try to fix the default partition value' do - connection.change_column_default(model.table_name, strategy.partitioning_key, 3) - expect(strategy.model.connection).not_to receive(:change_column_default) - strategy.validate_and_fix - end + strategy.validate_and_fix end - context 'feature flag is enabled' do - before do - stub_feature_flags(fix_sliding_list_partitioning: true) - end - - it 'does not call change_column_default if the partitioning in a valid state' do - expect(strategy.model.connection).not_to receive(:change_column_default) - - strategy.validate_and_fix - end - - it 'calls change_column_default on partition_key with the most default partition number' do - connection.change_column_default(model.table_name, strategy.partitioning_key, 1) + it 'calls change_column_default on partition_key with the most default partition number' do + connection.change_column_default(model.table_name, strategy.partitioning_key, 1) - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Fixed default value of sliding_list_strategy partitioning_key', - connection_name: 'main', - old_value: 1, - new_value: 2, - table_name: table_name, - column: strategy.partitioning_key - ) + expect(Gitlab::AppLogger).to receive(:warn).with( + message: 'Fixed default value of sliding_list_strategy partitioning_key', + connection_name: 'main', + old_value: 1, + new_value: 2, + table_name: table_name, + column: strategy.partitioning_key + ) - expect(strategy.model.connection).to receive(:change_column_default).with( - model.table_name, strategy.partitioning_key, 2 - ).and_call_original + expect(strategy.model.connection).to receive(:change_column_default).with( + model.table_name, strategy.partitioning_key, 2 + ).and_call_original - strategy.validate_and_fix - end + strategy.validate_and_fix + end - it 'does not change the default column if it has been changed in the meanwhile by another process' do - expect(strategy).to receive(:current_default_value).and_return(1, 2) + it 'does not change the default column if it has been changed in the meanwhile by another process' do + expect(strategy).to receive(:current_default_value).and_return(1, 2) - expect(strategy.model.connection).not_to receive(:change_column_default) + expect(strategy.model.connection).not_to receive(:change_column_default) - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Table partitions or partition key default value have been changed by another process', - table_name: table_name, - default_value: 2 - ) + expect(Gitlab::AppLogger).to receive(:warn).with( + message: 'Table partitions or partition key default value have been changed by another process', + table_name: table_name, + default_value: 2 + ) - strategy.validate_and_fix - end + strategy.validate_and_fix end end diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb index 7c69f639aab..36c8b0811fe 100644 --- a/spec/lib/gitlab/database/partitioning_spec.rb +++ b/spec/lib/gitlab/database/partitioning_spec.rb @@ -89,7 +89,7 @@ RSpec.describe Gitlab::Database::Partitioning do end it 'manages partitions for each given model' do - expect { described_class.sync_partitions(models)} + expect { described_class.sync_partitions(models) } .to change { find_partitions(table_names.first).size }.from(0) .and change { find_partitions(table_names.last).size }.from(0) 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 index 5e8afc0102e..ddf5793049d 100644 --- 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 @@ -5,6 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection, query_analyzers: false do let(:analyzer) { described_class } + # We keep only the GitlabSchemasValidateConnection analyzer running + around do |example| + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed(false) do + example.run + end + end + context 'properly observes all queries', :request_store do using RSpec::Parameterized::TableSyntax @@ -61,6 +68,24 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection end end + context "when analyzer is enabled for tests", :query_analyzers do + before do + skip_if_multiple_databases_not_setup + end + + it "throws an error when trying to access a table that belongs to the gitlab_main schema from the ci database" do + expect do + Ci::ApplicationRecord.connection.execute("select * from users limit 1") + end.to raise_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError) + end + + it "throws an error when trying to access a table that belongs to the gitlab_ci schema from the main database" do + expect do + ApplicationRecord.connection.execute("select * from ci_builds limit 1") + end.to raise_error(Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection::CrossSchemaAccessError) + end + end + def process_sql(model, sql) Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do # Skip load balancer and retrieve connection assigned to model diff --git a/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb b/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb index 34670696787..1bccdda3be1 100644 --- a/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb +++ b/spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Database::Reindexing::GrafanaNotifier do include Database::DatabaseHelpers let(:api_key) { "foo" } - let(:api_url) { "http://bar"} + let(:api_url) { "http://bar" } let(:additional_tag) { "some-tag" } let(:action) { create(:reindex_action) } diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index 976b9896dfa..495e953f993 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -46,6 +46,27 @@ RSpec.describe Gitlab::Database::Reindexing do end end + context 'when async index destruction is enabled' do + it 'executes async index destruction prior to any reindexing actions' do + stub_feature_flags(database_async_index_destruction: true) + + expect(Gitlab::Database::AsyncIndexes).to receive(:drop_pending_indexes!).ordered.exactly(databases_count).times + expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times + + described_class.invoke + end + end + + context 'when async index destruction is disabled' do + it 'does not execute async index destruction' do + stub_feature_flags(database_async_index_destruction: false) + + expect(Gitlab::Database::AsyncIndexes).not_to receive(:drop_pending_indexes!) + + described_class.invoke + end + end + context 'calls automatic reindexing' do it 'uses all candidate indexes' do expect(described_class).to receive(:automatic_reindexing).exactly(databases_count).times diff --git a/spec/lib/gitlab/database/shared_model_spec.rb b/spec/lib/gitlab/database/shared_model_spec.rb index c88edc17817..7e0ba3397d1 100644 --- a/spec/lib/gitlab/database/shared_model_spec.rb +++ b/spec/lib/gitlab/database/shared_model_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::Database::SharedModel do shared_model = shared_model_class.new - expect(shared_model.connection_db_config). to eq(described_class.connection_db_config) + expect(shared_model.connection_db_config).to eq(described_class.connection_db_config) end end end diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb index 6c32fb3ca17..836332524a9 100644 --- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb @@ -232,14 +232,14 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do expect(connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original expect(connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original - subject.run { } + subject.run {} end it 'calls `sleep` after the first iteration fails, using the configured sleep time' do expect(subject).to receive(:run_block_with_lock_timeout).and_raise(ActiveRecord::LockWaitTimeout).twice expect(subject).to receive(:sleep).with(0.025) - subject.run { } + subject.run {} end end end diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 6b35ccafabc..797a01c482d 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -248,14 +248,14 @@ RSpec.describe Gitlab::Database::WithLockRetries do expect(connection).to receive(:execute).with("SET LOCAL lock_timeout TO '15ms'").and_call_original expect(connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1", "TRANSACTION").and_call_original - subject.run { } + subject.run {} end it 'calls `sleep` after the first iteration fails, using the configured sleep time' do expect(subject).to receive(:run_block_with_lock_timeout).and_raise(ActiveRecord::LockWaitTimeout).twice expect(subject).to receive(:sleep).with(0.025) - subject.run { } + subject.run {} end end @@ -265,13 +265,13 @@ RSpec.describe Gitlab::Database::WithLockRetries do it 'prevents running inside already open transaction' do allow(connection).to receive(:transaction_open?).and_return(true) - expect { subject.run { } }.to raise_error(/should not run inside already open transaction/) + expect { subject.run {} }.to raise_error(/should not run inside already open transaction/) end it 'does not raise the error if not inside open transaction' do allow(connection).to receive(:transaction_open?).and_return(false) - expect { subject.run { } }.not_to raise_error + expect { subject.run {} }.not_to raise_error end end end diff --git a/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb b/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb index fdf16069381..1150de880b5 100644 --- a/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb +++ b/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Gitlab::DatabaseImporters::CommonMetrics::Importer do end context 'if ID is missing' do - let(:query_identifier) { } + let(:query_identifier) {} it 'raises exception' do expect { subject.execute }.to raise_error(Gitlab::DatabaseImporters::CommonMetrics::Importer::MissingQueryId) diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index 5350dda5fb2..1d1ffc8c275 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -115,6 +115,10 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do .once .and_call_original + Gitlab::Redis::Cache.with do |redis| + expect(redis).to receive(:expire).with(cache.key, described_class::EXPIRATION) + end + 2.times { cache.write_if_empty } end @@ -259,8 +263,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do describe '#key' do subject { cache.key } + def options_hash(options_array) + OpenSSL::Digest::SHA256.hexdigest(options_array.join) + end + it 'returns cache key' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, true, true])}") end context 'when the `use_marker_ranges` feature flag is disabled' do @@ -269,7 +277,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end it 'returns the original version of the cache' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, false, true])}") end end @@ -279,7 +287,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end it 'returns the original version of the cache' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, true, false])}") end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 624160d2f48..c378ecb8134 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -117,7 +117,7 @@ RSpec.describe Gitlab::Diff::Highlight do it 'reports to Sentry if configured' do expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).and_call_original - expect { subject }. to raise_exception(RangeError) + expect { subject }.to raise_exception(RangeError) end end diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb index 42ab2d1d063..ad92d90e253 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_helper_spec.rb @@ -49,7 +49,7 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFileHelper do describe '#image_as_rich_text' do let(:img) { 'data:image/png;base64,some_image_here' } - let(:line_text) { " ![](#{img})"} + let(:line_text) { " ![](#{img})" } subject { dummy.image_as_rich_text(line_text) } diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb new file mode 100644 index 00000000000..e953733c997 --- /dev/null +++ b/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512 do + describe '.transform_secret' do + let(:plaintext_token) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' } + + it 'generates a PBKDF2+SHA512 hashed value in the correct format' do + expect(described_class.transform_secret(plaintext_token)) + .to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength + end + + context 'when hash_oauth_tokens is disabled' do + before do + stub_feature_flags(hash_oauth_tokens: false) + end + + it 'returns a plaintext token' do + expect(described_class.transform_secret(plaintext_token)).to eq(plaintext_token) + end + end + end + + describe 'STRETCHES' do + it 'is 20_000' do + expect(described_class::STRETCHES).to eq(20_000) + end + end + + describe 'SALT' do + it 'is empty' do + expect(described_class::SALT).to be_empty + end + end +end 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 9ff395070ea..585dce331ed 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do it_behaves_like :note_handler_shared_examples do let(:recipient) { sent_notification.recipient } - let(:update_commands_only) { fixture_file('emails/update_commands_only_reply.eml')} + let(:update_commands_only) { fixture_file('emails/update_commands_only_reply.eml') } let(:no_content) { fixture_file('emails/no_content_reply.eml') } let(:commands_in_reply) { fixture_file('emails/commands_in_reply.eml') } let(:with_quick_actions) { fixture_file('emails/valid_reply_with_quick_actions.eml') } @@ -54,7 +54,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do end context 'with a secondary verified email address' do - let(:verified_email) { 'alan@adventuretime.ooo'} + let(:verified_email) { 'alan@adventuretime.ooo' } let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub('jake@adventuretime.ooo', verified_email) } before do diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index d0aba70081b..08a7383700b 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -493,11 +493,19 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do end it 'does not create an issue' do - expect { receiver.execute rescue nil }.not_to change { Issue.count } + expect do + receiver.execute + rescue StandardError + nil + end.not_to change { Issue.count } end it 'does not send thank you email' do - expect { receiver.execute rescue nil }.not_to have_enqueued_job.on_queue('mailers') + expect do + receiver.execute + rescue StandardError + nil + end.not_to have_enqueued_job.on_queue('mailers') end end @@ -532,7 +540,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do end context 'service desk is disabled for the project' do - let(:group) { create(:group)} + let(:group) { create(:group) } let(:project) { create(:project, :public, group: group, path: 'test', service_desk_enabled: false) } it 'bounces the email' do @@ -540,7 +548,11 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do end it "doesn't create an issue" do - expect { receiver.execute rescue nil }.not_to change { Issue.count } + expect do + receiver.execute + rescue StandardError + nil + end.not_to change { Issue.count } end end end diff --git a/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb index b5c3415fe12..7a09feb5b64 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::AdminVerify do let(:series) { 0 } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe 'public methods' do it 'returns value for series', :aggregate_failures do diff --git a/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb index 35470ef3555..d5aec280ea6 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Create do let_it_be(:group) { build(:group) } let_it_be(:user) { build(:user) } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe "public methods" do where(series: [0, 1, 2]) diff --git a/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb index daeacef53f6..3ac2076bf35 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/team_short_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::TeamShort do let(:series) { 0 } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe 'public methods' do it 'returns value for series', :aggregate_failures do diff --git a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb index eca8ba1df00..3354b2ed5cf 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do let_it_be(:group) { build(:group) } let_it_be(:user) { build(:user) } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe "public methods" do where(series: [0, 1]) diff --git a/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb index ebad4672eb3..cf0a119ea80 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/trial_short_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::TrialShort do let(:series) { 0 } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe 'public methods' do it 'returns value for series', :aggregate_failures do diff --git a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb index 3e18b8e35b6..7f86c9a6c6f 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do let_it_be(:group) { build(:group) } let_it_be(:user) { build(:user) } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe "public methods" do where(series: [0, 1, 2]) diff --git a/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb index a7da2e9553d..7e6f62289d2 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Verify do let_it_be(:group) { build(:group) } let_it_be(:user) { build(:user) } - subject(:message) { described_class.new(group: group, user: user, series: series)} + subject(:message) { described_class.new(group: group, user: user, series: series) } describe "public methods" do context 'with series 0' do diff --git a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb index 81e2a410962..bcd59c34ea2 100644 --- a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb +++ b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb @@ -430,7 +430,7 @@ RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do it do is_expected - .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/api/#{project.id}") + .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/#{project.id}") end end end diff --git a/spec/lib/gitlab/error_tracking/logger_spec.rb b/spec/lib/gitlab/error_tracking/logger_spec.rb index 751ec10a1f0..1b722fc7896 100644 --- a/spec/lib/gitlab/error_tracking/logger_spec.rb +++ b/spec/lib/gitlab/error_tracking/logger_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::ErrorTracking::Logger do describe '.capture_exception' do let(:exception) { RuntimeError.new('boom') } let(:payload) { { foo: '123' } } - let(:log_entry) { { message: 'boom', context: payload }} + let(:log_entry) { { message: 'boom', context: payload } } it 'calls Gitlab::ErrorTracking::Logger.error with formatted log entry' do expect_next_instance_of(Gitlab::ErrorTracking::LogFormatter) do |log_formatter| diff --git a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb index d33f8393904..bc4526758c0 100644 --- a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb @@ -159,13 +159,13 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do context 'when processing via the default error handler' do context 'with Raven events' do - let(:event) { raven_event} + let(:event) { raven_event } include_examples 'Sidekiq arguments', args_in_job_hash: true end context 'with Sentry events' do - let(:event) { sentry_event} + let(:event) { sentry_event } include_examples 'Sidekiq arguments', args_in_job_hash: true end @@ -173,13 +173,13 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do context 'when processing via Gitlab::ErrorTracking' do context 'with Raven events' do - let(:event) { raven_event} + let(:event) { raven_event } include_examples 'Sidekiq arguments', args_in_job_hash: false end context 'with Sentry events' do - let(:event) { sentry_event} + let(:event) { sentry_event } include_examples 'Sidekiq arguments', args_in_job_hash: false end @@ -209,13 +209,13 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do end context 'with Raven events' do - let(:event) { raven_event} + let(:event) { raven_event } it_behaves_like 'handles jobstr fields' end context 'with Sentry events' do - let(:event) { sentry_event} + let(:event) { sentry_event } it_behaves_like 'handles jobstr fields' end @@ -233,13 +233,13 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do end context 'with Raven events' do - let(:event) { raven_event} + let(:event) { raven_event } it_behaves_like 'does nothing' end context 'with Sentry events' do - let(:event) { sentry_event} + let(:event) { sentry_event } it_behaves_like 'does nothing' end @@ -256,13 +256,13 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do end context 'with Raven events' do - let(:event) { raven_event} + let(:event) { raven_event } it_behaves_like 'does nothing' end context 'with Sentry events' do - let(:event) { sentry_event} + let(:event) { sentry_event } it_behaves_like 'does nothing' end diff --git a/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb index f74fbf1206f..1f30ac79488 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb @@ -52,6 +52,28 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers::SleepingLock, :clean_gitlab_redis_ end end + context 'when the lease is obtained already' do + let!(:lease) { stub_exclusive_lease_taken(key) } + + context 'when retries are not specified' do + it 'retries to obtain a lease and raises an error' do + expect(lease).to receive(:try_obtain).exactly(10).times + + expect { subject.obtain }.to raise_error('Failed to obtain a lock') + end + end + + context 'when specified retries are above the maximum attempts' do + let(:max_attempts) { 100 } + + it 'retries to obtain a lease and raises an error' do + expect(lease).to receive(:try_obtain).exactly(65).times + + expect { subject.obtain(max_attempts) }.to raise_error('Failed to obtain a lock') + end + end + end + context 'when the lease is held elsewhere' do let!(:lease) { stub_exclusive_lease_taken(key) } let(:max_attempts) { 7 } diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb index 8bf06bcebe2..f9db93a6167 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -9,12 +9,12 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d let(:unique_key) { SecureRandom.hex(10) } describe '#in_lock' do - subject { class_instance.in_lock(unique_key, **options) { } } + subject { class_instance.in_lock(unique_key, **options) {} } let(:options) { {} } context 'when unique key is not set' do - let(:unique_key) { } + let(:unique_key) {} it 'raises an error' do expect { subject }.to raise_error ArgumentError diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb index ea21bda12d3..d684beaaaca 100644 --- a/spec/lib/gitlab/file_markdown_link_builder_spec.rb +++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::FileMarkdownLinkBuilder do end describe 'markdown_link' do - let(:url) { "/uploads/#{filename}"} + let(:url) { "/uploads/#{filename}" } before do allow(custom_class).to receive(:secure_url).and_return(url) diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb index 2b1fcac9257..98fb154fb05 100644 --- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb +++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb @@ -9,6 +9,40 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do let_it_be(:form_builder) { described_class.new(:user, user, fake_action_view_base, {}) } + describe '#submit' do + context 'without pajamas_button enabled' do + subject(:submit_html) do + form_builder.submit('Save', class: 'gl-button btn-confirm custom-class', data: { test: true }) + end + + it 'renders a submit input' do + expected_html = <<~EOS + <input type="submit" name="commit" value="Save" class="gl-button btn-confirm custom-class" data-test="true" data-disable-with="Save" /> + EOS + + expect(html_strip_whitespace(submit_html)).to eq(html_strip_whitespace(expected_html)) + end + end + + context 'with pajamas_button enabled' do + subject(:submit_html) do + form_builder.submit('Save', pajamas_button: true, class: 'custom-class', data: { test: true }) + end + + it 'renders a submit button' do + expected_html = <<~EOS + <button class="gl-button btn btn-md btn-confirm custom-class" data-test="true" type="submit"> + <span class="gl-button-text"> + Save + </span> + </button> + EOS + + expect(html_strip_whitespace(submit_html)).to eq(html_strip_whitespace(expected_html)) + end + end + end + describe '#gitlab_ui_checkbox_component' do context 'when not using slots' do let(:optional_args) { {} } @@ -25,7 +59,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do it 'renders correct html' do expected_html = <<~EOS <div class="gl-form-checkbox custom-control custom-checkbox"> - <input name="user[view_diffs_file_by_file]" type="hidden" value="0" /> + <input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" /> <input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> <label class="custom-control-label" for="user_view_diffs_file_by_file"> <span>Show one file at a time on merge request's Changes tab</span> @@ -51,7 +85,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do it 'renders help text' do expected_html = <<~EOS <div class="gl-form-checkbox custom-control custom-checkbox"> - <input name="user[view_diffs_file_by_file]" type="hidden" value="1" /> + <input name="user[view_diffs_file_by_file]" type="hidden" value="1" autocomplete="off" /> <input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> <label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file"> <span>Show one file at a time on merge request's Changes tab</span> @@ -101,7 +135,7 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do it 'renders correct html' do expected_html = <<~EOS <div class="gl-form-checkbox custom-control custom-checkbox"> - <input name="user[view_diffs_file_by_file]" type="hidden" value="0" /> + <input name="user[view_diffs_file_by_file]" type="hidden" value="0" autocomplete="off" /> <input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" /> <label class="custom-control-label" for="user_view_diffs_file_by_file"> <span>Show one file at a time on merge request's Changes tab</span> @@ -195,6 +229,45 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do end end + describe '#gitlab_ui_datepicker' do + subject(:datepicker_html) do + form_builder.gitlab_ui_datepicker( + :expires_at, + **optional_args + ) + end + + let(:optional_args) { {} } + + context 'without optional arguments' do + it 'renders correct html' do + expected_html = <<~EOS + <input class="datepicker form-control gl-form-input" type="text" name="user[expires_at]" id="user_expires_at" /> + EOS + + expect(html_strip_whitespace(datepicker_html)).to eq(html_strip_whitespace(expected_html)) + end + end + + context 'with optional arguments' do + let(:optional_args) do + { + id: 'milk_gone_bad', + data: { action: 'throw' }, + value: '2022-08-01' + } + end + + it 'renders correct html' do + expected_html = <<~EOS + <input id="milk_gone_bad" data-action="throw" value="2022-08-01" class="datepicker form-control gl-form-input" type="text" name="user[expires_at]" /> + EOS + + expect(html_strip_whitespace(datepicker_html)).to eq(html_strip_whitespace(expected_html)) + end + end + end + private def html_strip_whitespace(html) diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index e514e128785..45d88f57c09 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Git::Blame do it 'only returns the range' do expect(result.size).to eq(range.size) - expect(result.map {|r| r[:line] }).to eq(['', 'This guide details how contribute to GitLab.', '']) + expect(result.map { |r| r[:line] }).to eq(['', 'This guide details how contribute to GitLab.', '']) end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index fb4510a78de..0da7aa7dad0 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -50,7 +50,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do end context 'utf-8 branch' do - let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb")} + let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb") } it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } end @@ -235,6 +235,7 @@ RSpec.describe Gitlab::Git::Blob, :seed_helper do it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } it { expect(blob.data).to eq('') } + it 'does not mark the blob as binary' do expect(blob).not_to be_binary_in_repo end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 97cd4777b4d..feaa1f6595c 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -2,8 +2,9 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Branch, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Branch do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } subject { repository.branches } @@ -54,14 +55,14 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do describe '#size' do subject { super().size } - it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } + it { is_expected.to eq(TestEnv::BRANCH_SHA.size) } end describe 'first branch' do let(:branch) { repository.branches.first } - it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) } - it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } + it { expect(branch.name).to eq(TestEnv::BRANCH_SHA.keys.min) } + it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA[TestEnv::BRANCH_SHA.keys.min]) } end describe 'master branch' do @@ -69,14 +70,10 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do repository.branches.find { |branch| branch.name == 'master' } end - it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) } + it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA['master']) } end context 'with active, stale and future branches' do - let(:repository) do - Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') - end - let(:user) { create(:user) } let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } @@ -88,10 +85,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do repository.create_branch('future-1', future_sha) end - after do - ensure_seeds - end - describe 'examine if the branch is active or stale' do let(:stale_branch) { repository.find_branch('stale-1') } let(:active_branch) { repository.find_branch('active-1') } @@ -117,8 +110,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do end end - it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } - def create_commit repository.multi_action( user, diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index da77d8ee5d6..95b49186d0f 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -222,6 +222,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 10 elements' do expect(subject.size).to eq(10) end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } end @@ -240,6 +241,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 10 elements' do expect(subject.size).to eq(10) end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } end @@ -259,6 +261,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 3 elements' do expect(subject.size).to eq(3) end + it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } end @@ -279,6 +282,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 3 elements' do expect(subject.size).to eq(3) end + it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") } it { is_expected.not_to include(SeedRepo::Commit::ID) } end @@ -299,6 +303,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 3 elements' do expect(subject.size).to eq(3) end + it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } it { is_expected.not_to include(SeedRepo::Commit::ID) } end @@ -570,13 +575,13 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do describe '#id' do subject { super().id } - it { is_expected.to eq(sample_commit_hash[:id])} + it { is_expected.to eq(sample_commit_hash[:id]) } end describe '#message' do subject { super().message } - it { is_expected.to eq(sample_commit_hash[:message])} + it { is_expected.to eq(sample_commit_hash[:message]) } end end @@ -648,6 +653,7 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do it 'has 2 element' do expect(subject.size).to eq(2) end + it { is_expected.to include("master") } it { is_expected.not_to include("feature") } end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 114b3d01952..0e3e92e03cf 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -520,7 +520,7 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do describe '#real_size' do subject { super().real_size } - it { is_expected.to eq('0')} + it { is_expected.to eq('0') } end describe '#line_count' do @@ -595,7 +595,7 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do end context 'multi-file collections' do - let(:iterator) { [{ diff: 'b' }, { diff: 'a' * 20480 }]} + let(:iterator) { [{ diff: 'b' }, { diff: 'a' * 20480 }] } it 'prunes diffs that are quite big' do diff = nil diff --git a/spec/lib/gitlab/git/raw_diff_change_spec.rb b/spec/lib/gitlab/git/raw_diff_change_spec.rb index f894ae1d98b..c55fcc729b6 100644 --- a/spec/lib/gitlab/git/raw_diff_change_spec.rb +++ b/spec/lib/gitlab/git/raw_diff_change_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Git::RawDiffChange do - let(:raw_change) { } + let(:raw_change) {} let(:change) { described_class.new(raw_change) } context 'bad input' do diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb deleted file mode 100644 index c7bc81573a6..00000000000 --- a/spec/lib/gitlab/git/remote_repository_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Git::RemoteRepository, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - - subject { described_class.new(repository) } - - describe '#empty?' do - using RSpec::Parameterized::TableSyntax - - where(:repository, :result) do - Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') | false - Gitlab::Git::Repository.new('default', 'does-not-exist.git', '', 'group/project') | true - end - - with_them do - it { expect(subject.empty?).to eq(result) } - end - end - - describe '#commit_id' do - it 'returns an OID if the revision exists' do - expect(subject.commit_id('v1.0.0')).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') - end - - it 'is nil when the revision does not exist' do - expect(subject.commit_id('does-not-exist')).to be_nil - end - end - - describe '#branch_exists?' do - using RSpec::Parameterized::TableSyntax - - where(:branch, :result) do - 'master' | true - 'does-not-exist' | false - end - - with_them do - it { expect(subject.branch_exists?(branch)).to eq(result) } - end - end - - describe '#same_repository?' do - using RSpec::Parameterized::TableSyntax - - where(:other_repository, :result) do - repository | true - Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '', 'group/project') | true - Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '', 'group/project') | false - Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '', 'group/project') | false - Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '', 'group/project') | false - end - - with_them do - it { expect(subject.same_repository?(other_repository)).to eq(result) } - end - end -end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e20d5b928c4..a1fb8b70bd7 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1252,8 +1252,8 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#raw_changes_between' do - let(:old_rev) { } - let(:new_rev) { } + let(:old_rev) {} + let(:new_rev) {} let(:changes) { repository.raw_changes_between(old_rev, new_rev) } context 'initial commit' do @@ -1837,6 +1837,47 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#find_tag' do + it 'returns a tag' do + tag = repository.find_tag('v1.0.0') + + expect(tag).to be_a_kind_of(Gitlab::Git::Tag) + expect(tag.name).to eq('v1.0.0') + end + + shared_examples 'a nonexistent tag' do + it 'returns nil' do + expect(repository.find_tag('this-is-garbage')).to be_nil + end + end + + context 'when asking for a non-existent tag' do + it_behaves_like 'a nonexistent tag' + end + + context 'when Gitaly returns Internal error' do + before do + expect(repository.gitaly_ref_client) + .to receive(:find_tag) + .and_raise(GRPC::Internal, "tag not found") + end + + it_behaves_like 'a nonexistent tag' + end + + context 'when Gitaly returns tag_not_found error' do + before do + expect(repository.gitaly_ref_client) + .to receive(:find_tag) + .and_raise(new_detailed_error(GRPC::Core::StatusCodes::NOT_FOUND, + "tag was not found", + Gitaly::FindTagError.new(tag_not_found: Gitaly::ReferenceNotFoundError.new))) + end + + it_behaves_like 'a nonexistent tag' + end + end + describe '#languages' do it 'returns exactly the expected results' do languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') @@ -2017,17 +2058,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#set_full_path' do before do - repository_rugged.config["gitlab.fullpath"] = repository_path + repository.set_full_path(full_path: repository_path) end context 'is given a path' do it 'writes it to disk' do repository.set_full_path(full_path: "not-the/real-path.git") - config = File.read(File.join(repository_path, "config")) - - expect(config).to include("[gitlab]") - expect(config).to include("fullpath = not-the/real-path.git") + expect(repository.full_path).to eq('not-the/real-path.git') end end @@ -2035,15 +2073,12 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'does not write it to disk' do repository.set_full_path(full_path: "") - config = File.read(File.join(repository_path, "config")) - - expect(config).to include("[gitlab]") - expect(config).to include("fullpath = #{repository_path}") + expect(repository.full_path).to eq(repository_path) end end context 'repository does not exist' do - it 'raises NoRepository and does not call Gitaly WriteConfig' do + it 'raises NoRepository and does not call SetFullPath' do repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project') expect(repository.gitaly_repository_client).not_to receive(:set_full_path) @@ -2055,6 +2090,18 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#full_path' do + let(:full_path) { 'some/path' } + + before do + repository.set_full_path(full_path: full_path) + end + + it 'returns the full path' do + expect(repository.full_path).to eq(full_path) + end + end + describe '#merge_to_ref' do let(:repository) { mutable_repository } let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } @@ -2468,7 +2515,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end describe '#rename' do - let(:project) { create(:project, :repository)} + let(:project) { create(:project, :repository) } let(:repository) { project.repository } it 'moves the repository' do diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb index b2603e099e6..03d1c125e36 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -58,35 +58,55 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do end end - context 'when not running puma with multiple threads' do - before do - allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false) + context 'when skip_rugged_auto_detect feature flag is enabled' do + context 'when not running puma with multiple threads' do + before do + allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false) + stub_feature_flags(feature_flag_name => nil) + stub_feature_flags(skip_rugged_auto_detect: true) + end + + it 'returns false' do + expect(subject.use_rugged?(repository, feature_flag_name)).to be false + end end + end - it 'returns true when gitaly matches disk' do - expect(subject.use_rugged?(repository, feature_flag_name)).to be true + context 'when skip_rugged_auto_detect feature flag is disabled' do + before do + stub_feature_flags(skip_rugged_auto_detect: false) end - it 'returns false when disk access fails' do - allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist") + context 'when not running puma with multiple threads' do + before do + allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(false) + end - expect(subject.use_rugged?(repository, feature_flag_name)).to be false - end + it 'returns true when gitaly matches disk' do + expect(subject.use_rugged?(repository, feature_flag_name)).to be true + end - it "returns false when gitaly doesn't match disk" do - allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file) + it 'returns false when disk access fails' do + allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist") - expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey + expect(subject.use_rugged?(repository, feature_flag_name)).to be false + end - File.delete(temp_gitaly_metadata_file) - end + it "returns false when gitaly doesn't match disk" do + allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file) + + expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey - it "doesn't lead to a second rpc call because gitaly client should use the cached value" do - expect(subject.use_rugged?(repository, feature_flag_name)).to be true + File.delete(temp_gitaly_metadata_file) + end - expect(Gitlab::GitalyClient).not_to receive(:filesystem_id) + it "doesn't lead to a second rpc call because gitaly client should use the cached value" do + expect(subject.use_rugged?(repository, feature_flag_name)).to be true - subject.use_rugged?(repository, feature_flag_name) + expect(Gitlab::GitalyClient).not_to receive(:filesystem_id) + + subject.use_rugged?(repository, feature_flag_name) + end end end end @@ -165,7 +185,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do context 'all features are enabled' do let(:feature_keys) { [:feature_key_1, :feature_key_2] } - it { is_expected.to be_truthy} + it { is_expected.to be_truthy } end context 'all features are not enabled' do diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index 4f56595d7d2..240cf6ed46f 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -2,12 +2,13 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Tag, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } +RSpec.describe Gitlab::Git::Tag do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } describe '#tags' do - describe 'first tag' do - let(:tag) { repository.tags.first } + describe 'unsigned tag' do + let(:tag) { repository.tags.detect { |t| t.name == 'v1.0.0' } } it { expect(tag.name).to eq("v1.0.0") } it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") } @@ -22,29 +23,13 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do it { expect(tag.tagger.timezone).to eq("+0200") } end - describe 'last tag' do - let(:tag) { repository.tags.last } - - it { expect(tag.name).to eq("v1.2.1") } - it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") } - it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") } - it { expect(tag.message).to eq("Version 1.2.1") } - it { expect(tag.has_signature?).to be_falsey } - it { expect(tag.signature_type).to eq(:NONE) } - it { expect(tag.signature).to be_nil } - it { expect(tag.tagger.name).to eq("Douwe Maan") } - it { expect(tag.tagger.email).to eq("douwe@selenight.nl") } - it { expect(tag.tagger.date).to eq(Google::Protobuf::Timestamp.new(seconds: 1427789449)) } - it { expect(tag.tagger.timezone).to eq("+0200") } - end - describe 'signed tag' do - let(:project) { create(:project, :repository) } - let(:tag) { project.repository.find_tag('v1.1.1') } + let(:tag) { repository.tags.detect { |t| t.name == 'v1.1.1' } } + it { expect(tag.name).to eq("v1.1.1") } it { expect(tag.target).to eq("8f03acbcd11c53d9c9468078f32a2622005a4841") } it { expect(tag.dereferenced_target.sha).to eq("189a6c924013fc3fe40d6f1ec1dc20214183bc97") } - it { expect(tag.message).to eq("x509 signed tag" + "\n" + X509Helpers::User1.signed_tag_signature.chomp) } + it { expect(tag.message).to eq("x509 signed tag\n" + X509Helpers::User1.signed_tag_signature.chomp) } it { expect(tag.has_signature?).to be_truthy } it { expect(tag.signature_type).to eq(:X509) } it { expect(tag.signature).not_to be_nil } @@ -54,11 +39,11 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do it { expect(tag.tagger.timezone).to eq("+0100") } end - it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } + it { expect(repository.tags.size).to be > 0 } end describe '.get_message' do - let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] } + let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8f03acbcd11c53d9c9468078f32a2622005a4841] } subject do tag_ids.map { |id| described_class.get_message(repository, id) } @@ -66,7 +51,7 @@ RSpec.describe Gitlab::Git::Tag, :seed_helper do it 'gets tag messages' do expect(subject[0]).to eq("Release\n") - expect(subject[1]).to eq("Version 1.1.0\n") + expect(subject[1]).to eq("x509 signed tag\n" + X509Helpers::User1.signed_tag_signature) end it 'gets messages in one batch', :request_store do diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 172d7a3f27b..b520de03929 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -2,10 +2,11 @@ require "spec_helper" -RSpec.describe Gitlab::Git::Tree, :seed_helper do +RSpec.describe Gitlab::Git::Tree do let_it_be(:user) { create(:user) } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } shared_examples :repo do subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, pagination_params) } @@ -105,10 +106,6 @@ RSpec.describe Gitlab::Git::Tree, :seed_helper do ).newrev end - after do - ensure_seeds - end - it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') } end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 5ee9cf05b3e..8577cad1011 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GitAccess do +RSpec.describe Gitlab::GitAccess, :aggregate_failures do include TermsHelper include GitHelpers include AdminModeHelper @@ -78,9 +78,7 @@ RSpec.describe Gitlab::GitAccess do let(:auth_result_type) { :ci } it "doesn't block http pull" do - aggregate_failures do - expect { pull_access_check }.not_to raise_error - end + expect { pull_access_check }.not_to raise_error end end end @@ -153,6 +151,15 @@ RSpec.describe Gitlab::GitAccess do it 'does not block pushes with "not found"' do expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) end + + it 'logs' do + expect(Gitlab::AppJsonLogger).to receive(:info).with( + message: 'Actor was :ci', + project_id: project.id + ).once + + pull_access_check + end end context 'when actor is DeployToken' do @@ -229,9 +236,9 @@ RSpec.describe Gitlab::GitAccess do end context 'key is expired' do - let(:actor) { create(:rsa_key_2048, :expired) } + let(:actor) { create(:deploy_key, :expired) } - it 'does not allow expired keys', :aggregate_failures do + it 'does not allow expired keys' 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 @@ -242,7 +249,7 @@ RSpec.describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: 4096) end - it 'does not allow keys which are too small', :aggregate_failures do + it 'does not allow keys which are too small' do expect(actor).not_to be_valid expect { pull_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.') expect { push_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.') @@ -254,7 +261,7 @@ RSpec.describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE) end - it 'does not allow keys which are too small', :aggregate_failures do + it 'does not allow keys which are too small' do expect(actor).not_to be_valid expect { pull_access_check }.to raise_forbidden(/Your SSH key type is forbidden/) expect { push_access_check }.to raise_forbidden(/Your SSH key type is forbidden/) @@ -263,7 +270,7 @@ RSpec.describe Gitlab::GitAccess do end it_behaves_like '#check with a key that is not valid' do - let(:actor) { build(:rsa_key_2048, user: user) } + let(:actor) { build(:deploy_key, user: user) } end it_behaves_like '#check with a key that is not valid' do @@ -736,6 +743,15 @@ RSpec.describe Gitlab::GitAccess do context 'pull code' do it { expect { pull_access_check }.not_to raise_error } + + it 'logs' do + expect(Gitlab::AppJsonLogger).to receive(:info).with( + message: 'Actor was :ci', + project_id: project.id + ).once + + pull_access_check + end end end end @@ -1163,13 +1179,13 @@ RSpec.describe Gitlab::GitAccess do -> { push_access_check }] end - it 'blocks access when the user did not accept terms', :aggregate_failures do + it 'blocks access when the user did not accept terms' do actions.each do |action| expect { action.call }.to raise_forbidden(/must accept the Terms of Service in order to perform this action/) end end - it 'allows access when the user accepted the terms', :aggregate_failures do + it 'allows access when the user accepted the terms' do accept_terms(user) actions.each do |action| diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 784d25f55c1..f359679a930 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -54,6 +54,7 @@ RSpec.describe Gitlab::Git do with_them do it { expect(described_class.shas_eql?(sha1, sha2)).to eq(result) } + it 'is commutative' do expect(described_class.shas_eql?(sha2, sha1)).to eq(result) end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index d5d1bef7bff..0d591fe6c43 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -340,7 +340,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do describe '#list_new_commits' do let(:revisions) { [revision] } let(:gitaly_commits) { create_list(:gitaly_commit, 3) } - let(:expected_commits) { gitaly_commits.map { |c| Gitlab::Git::Commit.new(repository, c) }} + let(:expected_commits) { gitaly_commits.map { |c| Gitlab::Git::Commit.new(repository, c) } } subject do client.list_new_commits(revisions) diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index e04895d975f..5d854f0c9d1 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -84,37 +84,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService do subject end - describe '#user_merge_to_ref' do - let(:first_parent_ref) { 'refs/heads/my-branch' } - let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } - let(:ref) { 'refs/merge-requests/x/merge' } - let(:message) { 'validación' } - let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - - let(:payload) do - { source_sha: source_sha, branch: 'branch', target_ref: ref, - message: message, first_parent_ref: first_parent_ref, allow_conflicts: true } - end - - it 'sends a user_merge_to_ref message' do - freeze_time do - expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_merge_to_ref) do |_, request, options| - expect(options).to be_kind_of(Hash) - expect(request.to_h).to eq( - payload.merge({ - repository: repository.gitaly_repository.to_h, - message: message.dup.force_encoding(Encoding::ASCII_8BIT), - user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, - timestamp: { nanos: 0, seconds: Time.current.to_i } - }) - ) - end.and_return(response) - - client.user_merge_to_ref(user, **payload) - end - end - end - context "when pre_receive_error is present" do let(:response) do Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "GitLab: something failed") @@ -131,6 +100,37 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end end + describe '#user_merge_to_ref' do + let(:first_parent_ref) { 'refs/heads/my-branch' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:ref) { 'refs/merge-requests/x/merge' } + let(:message) { 'validación' } + let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } + + let(:payload) do + { source_sha: source_sha, branch: 'branch', target_ref: ref, + message: message, first_parent_ref: first_parent_ref, allow_conflicts: true } + end + + it 'sends a user_merge_to_ref message' do + freeze_time do + expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_merge_to_ref) do |_, request, options| + expect(options).to be_kind_of(Hash) + expect(request.to_h).to eq( + payload.merge({ + repository: repository.gitaly_repository.to_h, + message: message.dup.force_encoding(Encoding::ASCII_8BIT), + user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, + timestamp: { nanos: 0, seconds: Time.current.to_i } + }) + ) + end.and_return(response) + + client.user_merge_to_ref(user, **payload) + end + end + end + describe '#user_delete_branch' do let(:branch_name) { 'my-branch' } let(:request) do @@ -551,7 +551,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end let(:expected_error) { Gitlab::Git::Repository::CreateTreeError } - let(:expected_error_message) { } + let(:expected_error_message) {} it_behaves_like '#user_cherry_pick with a gRPC error' end @@ -559,7 +559,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do context 'when a non-detailed gRPC error is raised' do let(:raised_error) { GRPC::Internal.new('non-detailed error') } let(:expected_error) { GRPC::Internal } - let(:expected_error_message) { } + let(:expected_error_message) {} it_behaves_like '#user_cherry_pick with a gRPC error' end @@ -813,4 +813,146 @@ RSpec.describe Gitlab::GitalyClient::OperationService do end end end + + describe '#add_tag' do + let(:tag_name) { 'some-tag' } + let(:tag_message) { nil } + let(:target) { 'master' } + + subject(:add_tag) do + client.add_tag(tag_name, user, target, tag_message) + end + + context 'without tag message' do + let(:tag_name) { 'lightweight-tag' } + + it 'creates a lightweight tag' do + tag = add_tag + expect(tag.name).to eq(tag_name) + expect(tag.message).to eq('') + end + end + + context 'with tag message' do + let(:tag_name) { 'annotated-tag' } + let(:tag_message) { "tag message" } + + it 'creates an annotated tag' do + tag = add_tag + expect(tag.name).to eq(tag_name) + expect(tag.message).to eq(tag_message) + end + end + + context 'with preexisting tag' do + let(:tag_name) { 'v1.0.0' } + + it 'raises a TagExistsError' do + expect { add_tag }.to raise_error(Gitlab::Git::Repository::TagExistsError) + end + end + + context 'with invalid target' do + let(:target) { 'refs/heads/does-not-exist' } + + it 'raises an InvalidRef error' do + expect { add_tag }.to raise_error(Gitlab::Git::Repository::InvalidRef) + end + end + + context 'with pre-receive error' do + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_tag) + .and_return(Gitaly::UserCreateTagResponse.new(pre_receive_error: "GitLab: something failed")) + end + + it 'raises a PreReceiveError' do + expect { add_tag }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") + end + end + + context 'with internal error' do + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_tag) + .and_raise(GRPC::Internal.new('undetailed internal error')) + end + + it 'raises an Internal error' do + expect { add_tag }.to raise_error do |error| + expect(error).to be_a(GRPC::Internal) + expect(error.details).to eq('undetailed internal error') + end + end + end + + context 'with structured errors' do + before do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_tag) + .and_raise(structured_error) + end + + context 'with ReferenceExistsError' do + let(:structured_error) do + new_detailed_error( + GRPC::Core::StatusCodes::ALREADY_EXISTS, + 'tag exists already', + Gitaly::UserCreateTagError.new( + reference_exists: Gitaly::ReferenceExistsError.new( + reference_name: tag_name, + oid: 'something' + ))) + end + + it 'raises a TagExistsError' do + expect { add_tag }.to raise_error(Gitlab::Git::Repository::TagExistsError) + end + end + + context 'with AccessCheckError' do + let(:structured_error) do + new_detailed_error( + GRPC::Core::StatusCodes::PERMISSION_DENIED, + "error creating tag", + Gitaly::UserCreateTagError.new( + access_check: Gitaly::AccessCheckError.new( + error_message: "You are not allowed to create this tag.", + protocol: "web", + user_id: "user-15", + changes: "df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/tags/v1.0.0\n" + ))) + end + + it 'raises a PreReceiveError' do + expect { add_tag }.to raise_error do |error| + expect(error).to be_a(Gitlab::Git::PreReceiveError) + expect(error.message).to eq("You are not allowed to create this tag.") + end + end + end + + context 'with CustomHookError' do + let(:structured_error) do + new_detailed_error( + GRPC::Core::StatusCodes::PERMISSION_DENIED, + "custom hook error", + Gitaly::UserCreateTagError.new( + custom_hook: Gitaly::CustomHookError.new( + stdout: "some stdout", + stderr: "GitLab: some custom hook error message", + hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE + ))) + end + + it 'raises a PreReceiveError' do + expect { add_tag }.to raise_error do |error| + expect(error).to be_a(Gitlab::Git::PreReceiveError) + expect(error.message).to eq("some custom hook error message") + end + end + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 566bdbacf4a..277276bb1d3 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -120,6 +120,28 @@ RSpec.describe Gitlab::GitalyClient::RefService do expect(client.find_tag('')).to be_nil end end + + context 'when Gitaly returns an Internal error' do + it 'raises an Internal error' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_tag) + .and_raise(GRPC::Internal.new('something went wrong')) + + expect { client.find_tag('v1.0.0') }.to raise_error(GRPC::Internal) + end + end + + context 'when Gitaly returns a tag_not_found error' do + it 'raises an UnknownRef error' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_tag) + .and_raise(new_detailed_error(GRPC::Core::StatusCodes::NOT_FOUND, + "tag was not found", + Gitaly::FindTagError.new(tag_not_found: Gitaly::ReferenceNotFoundError.new))) + + expect { client.find_tag('v1.0.0') }.to raise_error(Gitlab::Git::UnknownRef, 'tag does not exist: v1.0.0') + end + end end describe '#default_branch_name' do @@ -286,7 +308,7 @@ RSpec.describe Gitlab::GitalyClient::RefService do end context 'with a invalid format error' do - let(:invalid_refs) {['\invali.\d/1', '\.invali/d/2']} + let(:invalid_refs) { ['\invali.\d/1', '\.invali/d/2'] } let(:invalid_reference_format_error) do new_detailed_error( GRPC::Core::StatusCodes::INVALID_ARGUMENT, diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 39de9a65390..63d32cb906f 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -276,32 +276,12 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do end describe '#disconnect_alternates' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } - let(:pool_repository) { create(:pool_repository) } - let(:object_pool) { pool_repository.object_pool } - let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) } - - before do - object_pool_service.create(repository) # rubocop:disable Rails/SaveBang - object_pool_service.link_repository(repository) - end - - it 'deletes the alternates file' do - repository.disconnect_alternates - - alternates_file = File.join(repository_path, "objects", "info", "alternates") + it 'sends a disconnect_git_alternates message' do + expect_any_instance_of(Gitaly::ObjectPoolService::Stub) + .to receive(:disconnect_git_alternates) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) - expect(File.exist?(alternates_file)).to be_falsey - end - - context 'when called twice' do - it "doesn't raise an error" do - repository.disconnect_alternates - - expect { repository.disconnect_alternates }.not_to raise_error - end + client.disconnect_alternates end end @@ -351,4 +331,16 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do client.set_full_path(path) end end + + describe '#full_path' do + let(:path) { 'repo/path' } + + it 'sends a full_path message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:full_path) + .and_return(double(path: path)) + + expect(client.full_path).to eq(path) + end + end end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index c4d05e92633..2bd3910ad87 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -208,7 +208,7 @@ RSpec.describe Gitlab::GithubImport::Client do expect(client).to receive(:requests_remaining?).and_return(true) - client.with_rate_limit { } + client.with_rate_limit {} end it 'ignores rate limiting when disabled' do diff --git a/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb new file mode 100644 index 00000000000..41fe5fbdbbd --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::BaseImporter do + let(:project) { instance_double('Project') } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:issue_event) { instance_double('Gitlab::GithubImport::Representation::IssueEvent') } + let(:importer_class) { Class.new(described_class) } + let(:importer_instance) { importer_class.new(project, client) } + + describe '#execute' do + it { expect { importer_instance.execute(issue_event) }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb new file mode 100644 index 00000000000..2f6f727dc38 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do + subject(:importer) { described_class.new(project, client) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:assignee) { create(:user) } + let_it_be(:assigner) { create(:user) } + + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => event_type, + 'commit_id' => nil, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'assigner' => { 'id' => assigner.id, 'login' => assigner.username }, + 'assignee' => { 'id' => assignee.id, 'login' => assignee.username }, + 'issue' => { 'number' => issue.iid } + ) + end + + let(:note_attrs) do + { + noteable_id: issue.id, + noteable_type: Issue.name, + project_id: project.id, + author_id: assigner.id, + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_system_note_metadata_attrs) do + { + action: "assignee", + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + shared_examples 'new note' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issue.notes.count } + .from(0).to(1) + + expect(issue.notes.last) + .to have_attributes(expected_note_attrs) + end + + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } + .from(0).to(1) + + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) + ) + end + end + + describe '#execute' do + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) + allow(finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) + end + end + + context 'when importing an assigned event' do + let(:event_type) { 'assigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") } + + it_behaves_like 'new note' + end + + context 'when importing an unassigned event' do + let(:event_type) { 'unassigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assigner.username}") } + + it_behaves_like 'new note' + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb index b773598853d..e21672aa430 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb @@ -3,23 +3,25 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue) { create(:issue, project: project) } let!(:label) { create(:label, project: project) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => event_type, 'commit_id' => nil, 'label_title' => label.title, 'issue_db_id' => issue.id, - 'created_at' => '2022-04-26 18:30:53 UTC' + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } ) end @@ -43,6 +45,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end context 'when importing a labeled event' do diff --git a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb new file mode 100644 index 00000000000..2687627fc23 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do + subject(:importer) { described_class.new(project, client) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:issue) { create(:issue, project: project) } + let!(:milestone) { create(:milestone, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => user.id, 'login' => user.username }, + 'event' => event_type, + 'commit_id' => nil, + 'milestone_title' => milestone.title, + 'issue_db_id' => issue.id, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } + ) + end + + let(:event_attrs) do + { + user_id: user.id, + issue_id: issue.id, + milestone_id: milestone.id, + state: 'opened', + created_at: issue_event.created_at + }.stringify_keys + end + + shared_examples 'new event' do + it 'creates a new milestone event' do + expect { importer.execute(issue_event) }.to change { issue.resource_milestone_events.count } + .from(0).to(1) + expect(issue.resource_milestone_events.last) + .to have_attributes(expected_event_attrs) + end + end + + describe '#execute' do + before do + allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + end + + context 'when importing a milestoned event' do + let(:event_type) { 'milestoned' } + let(:expected_event_attrs) { event_attrs.merge(action: 'add') } + + it_behaves_like 'new event' + end + + context 'when importing demilestoned event' do + let(:event_type) { 'demilestoned' } + let(:expected_event_attrs) { event_attrs.merge(action: 'remove') } + + it_behaves_like 'new event' + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb index 116917d3e06..9a49d80a8bb 100644 --- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue) { create(:issue, project: project) } let(:commit_id) { nil } @@ -16,11 +17,11 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'closed', 'created_at' => '2022-04-26 18:30:53 UTC', 'commit_id' => commit_id, - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -45,6 +46,15 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do }.stringify_keys end + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + end + it 'creates expected event and state event' do importer.execute(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb index 118c482a7d9..68e001c7364 100644 --- a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb @@ -3,15 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_gitlab_redis_cache do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:sawyer_stub) { Struct.new(:iid, :issuable_type, keyword_init: true) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } - let(:issue) { create(:issue, project: project) } - let(:referenced_in) { build_stubbed(:issue, project: project) } + let(:issue_iid) { 999 } + let(:issue) { create(:issue, project: project, iid: issue_iid) } + let(:referenced_in) { build_stubbed(:issue, project: project, iid: issue_iid + 1) } let(:commit_id) { nil } let(:issue_event) do @@ -19,7 +20,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'cross-referenced', 'source' => { 'type' => 'issue', @@ -29,7 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g } }, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -38,7 +39,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g { system: true, noteable_type: Issue.name, - noteable_id: issue_event.issue_db_id, + noteable_id: issue.id, project_id: project.id, author_id: user.id, note: expected_note_body, @@ -47,12 +48,16 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g end context 'when referenced in other issue' do - let(:expected_note_body) { "mentioned in issue ##{issue.iid}" } + let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" } before do - other_issue_resource = sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'Issue') - Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) - .cache_database_id(referenced_in.iid) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do @@ -71,10 +76,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } before do - other_issue_resource = - sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'MergeRequest') - Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) - .cache_database_id(referenced_in.iid) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do @@ -87,7 +95,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g end context 'when referenced in out of project issue/pull_request' do - it 'creates expected note' do + it 'does not create expected note' do importer.execute(issue_event) expect(issue.notes.count).to eq 0 diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb index a8c3fbcb05d..316ea798965 100644 --- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -3,23 +3,24 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:issue) { create(:issue, project: project) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'renamed', 'commit_id' => nil, 'created_at' => '2022-04-26 18:30:53 UTC', 'old_title' => 'old title', 'new_title' => 'new title', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -45,6 +46,15 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do end describe '#execute' do + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + end + it 'creates expected note' do expect { importer.execute(issue_event) }.to change { issue.notes.count } .from(0).to(1) diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb index 81653b0ecdc..2461dbb9701 100644 --- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_failures do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } let(:issue) { create(:issue, project: project) } let(:issue_event) do @@ -15,10 +16,10 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'reopened', 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -42,6 +43,15 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail }.stringify_keys end + before do + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + end + it 'creates expected event and state event' do importer.execute(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb index da32a3b3766..33d5fbf13a0 100644 --- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab specific_importer = double(importer_class.name) # rubocop:disable RSpec/VerifiedDoubles expect(importer_class) - .to receive(:new).with(project, user.id) + .to receive(:new).with(project, client) .and_return(specific_importer) expect(specific_importer).to receive(:execute).with(issue_event) @@ -43,12 +43,6 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab describe '#execute' do before do - allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| - allow(finder).to receive(:author_id_for) - .with(issue_event, author_key: :actor) - .and_return(user.id, true) - end - issue_event.attributes[:issue_db_id] = issue.id end @@ -87,6 +81,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab Gitlab::GithubImport::Importer::Events::Renamed end + context "when it's milestoned issue event" do + let(:event_name) { 'milestoned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedMilestone + end + + context "when it's demilestoned issue event" do + let(:event_name) { 'demilestoned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedMilestone + end + context "when it's cross-referenced issue event" do let(:event_name) { 'cross-referenced' } @@ -94,6 +102,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab Gitlab::GithubImport::Importer::Events::CrossReferenced end + context "when it's assigned issue event" do + let(:event_name) { 'assigned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedAssignee + end + + context "when it's unassigned issue event" do + let(:event_name) { 'unassigned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedAssignee + end + context "when it's unknown issue event" do let(:event_name) { 'fake' } diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb new file mode 100644 index 00000000000..8d4c1b01e50 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do + subject(:importer) { described_class.new(project, client, parallel: parallel) } + + let(:project) { instance_double(Project, id: 4, import_source: 'foo/bar') } + let(:client) { instance_double(Gitlab::GithubImport::Client) } + + let(:parallel) { true } + let(:issue_event) do + struct = Struct.new( + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, + keyword_init: true + ) + struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') + end + + describe '#parallel?' do + context 'when running in parallel mode' do + it { expect(importer).to be_parallel } + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it { expect(importer).not_to be_parallel } + end + end + + describe '#execute' do + context 'when running in parallel mode' do + it 'imports events in parallel' do + expect(importer).to receive(:parallel_import) + + importer.execute + end + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it 'imports notes in sequence' do + expect(importer).to receive(:sequential_import) + + importer.execute + end + end + end + + describe '#sequential_import' do + let(:parallel) { false } + + it 'imports each event in sequence' do + event_importer = instance_double(Gitlab::GithubImport::Importer::IssueEventImporter) + + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::Importer::IssueEventImporter) + .to receive(:new) + .with( + an_instance_of(Gitlab::GithubImport::Representation::IssueEvent), + project, + client + ) + .and_return(event_importer) + + expect(event_importer).to receive(:execute) + + importer.sequential_import + end + end + + describe '#parallel_import' do + it 'imports each note in parallel' do + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::ImportIssueEventWorker).to receive(:bulk_perform_in).with( + 1.second, [ + [project.id, an_instance_of(Hash), an_instance_of(String)] + ], batch_size: 1000, batch_delay: 1.minute + ) + + waiter = importer.parallel_import + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + end + + describe '#importer_class' do + it { expect(importer.importer_class).to eq Gitlab::GithubImport::Importer::IssueEventImporter } + end + + describe '#representation_class' do + it { expect(importer.representation_class).to eq Gitlab::GithubImport::Representation::IssueEvent } + end + + describe '#sidekiq_worker_class' do + it { expect(importer.sidekiq_worker_class).to eq Gitlab::GithubImport::ImportIssueEventWorker } + end + + describe '#object_type' do + it { expect(importer.object_type).to eq :issue_event } + end + + describe '#collection_method' do + it { expect(importer.collection_method).to eq :repository_issue_events } + end + + describe '#id_for_already_imported_cache' do + it 'returns the ID of the given note' do + expect(importer.id_for_already_imported_cache(issue_event)).to eq(issue_event.id) + end + end + + describe '#collection_options' do + it { expect(importer.collection_options).to eq({}) } + end +end diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 570d26cdf2d..1692aac49f2 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache do + let_it_be(:work_item_type_id) { ::WorkItems::Type.default_issue_type.id } + let(:project) { create(:project) } let(:client) { double(:client) } let(:user) { create(:user) } @@ -25,7 +27,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'), created_at: created_at, updated_at: updated_at, - pull_request: false + pull_request: false, + work_item_type_id: work_item_type_id ) end @@ -116,6 +119,17 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi .and_return(milestone.id) end + it 'creates issues with a work item type id' do + allow(importer.user_finder) + .to receive(:author_id_for) + .with(issue) + .and_return([user.id, true]) + + issue_id = importer.create_issue + + expect(Issue.find(issue_id).work_item_type_id).to eq(work_item_type_id) + end + context 'when the issue author could be found' do it 'creates the issue with the found author as the issue author' do allow(importer.user_finder) @@ -136,7 +150,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi milestone_id: milestone.id, state_id: 1, created_at: created_at, - updated_at: updated_at + updated_at: updated_at, + work_item_type_id: work_item_type_id }, project.issues ) @@ -166,7 +181,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi milestone_id: milestone.id, state_id: 1, created_at: created_at, - updated_at: updated_at + updated_at: updated_at, + work_item_type_id: work_item_type_id }, project.issues ) diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb index 6dfd4424342..251829b83a0 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do let(:lfs_attributes) do { - oid: 'oid', + oid: 'a' * 64, size: 1, link: 'http://www.gitlab.com/lfs_objects/oid' } diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index c1b0f4df29a..c5846fa7a87 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -149,7 +149,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do expect(importer) .to receive(:update_repository) - importer.each_object_to_import { } + importer.each_object_to_import {} end end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb index 087faeffe02..bb1ee79ad93 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter describe '#each_object_to_import', :clean_gitlab_redis_cache do let(:issue_event) do - struct = Struct.new(:id, :event, :created_at, :issue_db_id, keyword_init: true) + struct = Struct.new(:id, :event, :created_at, :issue, keyword_init: true) struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') end @@ -81,7 +81,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter counter = 0 subject.each_object_to_import do |object| expect(object).to eq issue_event - expect(issue_event.issue_db_id).to eq issue.id + expect(issue_event.issue['number']).to eq issue.iid counter += 1 end expect(counter).to eq 1 diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb index 3afd006109b..d550f15e8c5 100644 --- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb +++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do let(:project) { double(:project, id: 4, group: nil) } let(:issue) do - double(:issue, issuable_type: MergeRequest, iid: 1) + double(:issue, issuable_type: MergeRequest, issuable_id: 1) end let(:finder) { described_class.new(project, issue) } diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb index 999f8ffb21e..738e7c88d7d 100644 --- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb @@ -243,7 +243,7 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do expect(repr_class) .to receive(:from_api_response) - .with(object) + .with(object, {}) .and_return(repr_instance) expect(importer) @@ -281,7 +281,7 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do allow(repr_class) .to receive(:from_api_response) - .with(object) + .with(object, {}) .and_return({ title: 'Foo' }) end diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb index 23da8276f64..d3a98035e73 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do expect(issue_event.source).to eq({ type: 'issue', id: 123456 }) end - it 'includes the issue_db_id' do - expect(issue_event.issue_db_id).to eq(100500) + it 'includes the issue data' do + expect(issue_event.issue).to eq({ number: 2, pull_request: pull_request }) end context 'when actor data present' do @@ -77,11 +77,66 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do end end + context 'when milestone data is present' do + it 'includes the milestone_title' do + expect(issue_event.milestone_title).to eq('milestone title') + end + end + + context 'when milestone data is empty' do + let(:with_milestone) { false } + + it 'does not return such info' do + expect(issue_event.milestone_title).to eq nil + end + end + + context 'when assignee and assigner data is present' do + it 'includes assignee and assigner details' do + expect(issue_event.assignee) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(issue_event.assignee.id).to eq(5) + expect(issue_event.assignee.login).to eq('tom') + + expect(issue_event.assigner) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(issue_event.assigner.id).to eq(6) + expect(issue_event.assigner.login).to eq('jerry') + end + end + + context 'when assignee and assigner data is empty' do + let(:with_assignee) { false } + + it 'does not return such info' do + expect(issue_event.assignee).to eq nil + expect(issue_event.assigner).to eq nil + end + end + it 'includes the created timestamp' do expect(issue_event.created_at).to eq('2022-04-26 18:30:53 UTC') end end + describe '#issuable_id' do + it 'returns issuable_id' do + expect(issue_event.issuable_id).to eq(2) + end + end + + describe '#issuable_type' do + context 'when event related to issue' do + it { expect(issue_event.issuable_type).to eq('Issue') } + end + + context 'when event related to pull request' do + let(:pull_request) { { url: FFaker::Internet.http_url } } + + it { expect(issue_event.issuable_type).to eq('MergeRequest') } + end + end + describe '#github_identifiers' do it 'returns a hash with needed identifiers' do expect(issue_event.github_identifiers).to eq({ id: 6501124486 }) @@ -92,8 +147,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do describe '.from_api_response' do let(:response) do event_resource = Struct.new( - :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, - :rename, :issue_db_id, :created_at, :performed_via_github_app, :source, + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -106,10 +161,13 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', commit_url: 'https://api.github.com/repos/octocat/Hello-World/commits'\ '/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + label: with_label ? { name: 'label title' } : nil, rename: with_rename ? { from: 'old title', to: 'new title' } : nil, + milestone: with_milestone ? { title: 'milestone title' } : nil, source: { type: 'issue', id: 123456 }, - issue_db_id: 100500, - label: with_label ? { name: 'label title' } : nil, + assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil, + assigner: with_assignee ? user_resource.new(id: 6, login: 'jerry') : nil, + issue: { 'number' => 2, 'pull_request' => pull_request }, created_at: '2022-04-26 18:30:53 UTC', performed_via_github_app: nil ) @@ -118,6 +176,9 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_actor) { true } let(:with_label) { true } let(:with_rename) { true } + let(:with_milestone) { true } + let(:with_assignee) { true } + let(:pull_request) { nil } it_behaves_like 'an IssueEvent' do let(:issue_event) { described_class.from_api_response(response) } @@ -139,8 +200,11 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 'label_title' => (with_label ? 'label title' : nil), 'old_title' => with_rename ? 'old title' : nil, 'new_title' => with_rename ? 'new title' : nil, + 'milestone_title' => (with_milestone ? 'milestone title' : nil), 'source' => { 'type' => 'issue', 'id' => 123456 }, - "issue_db_id" => 100500, + 'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil), + 'assigner' => (with_assignee ? { 'id' => 6, 'login' => 'jerry' } : nil), + 'issue' => { 'number' => 2, 'pull_request' => pull_request }, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil } @@ -149,6 +213,9 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_actor) { true } let(:with_label) { true } let(:with_rename) { true } + let(:with_milestone) { true } + let(:with_assignee) { true } + let(:pull_request) { nil } let(:issue_event) { described_class.from_json_hash(hash) } end diff --git a/spec/lib/gitlab/github_import/representation/issue_spec.rb b/spec/lib/gitlab/github_import/representation/issue_spec.rb index f3052efea70..5898518343a 100644 --- a/spec/lib/gitlab/github_import/representation/issue_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Representation::Issue do + let_it_be(:work_item_type_id) { ::WorkItems::Type.default_issue_type.id } + let(:created_at) { Time.new(2017, 1, 1, 12, 00) } let(:updated_at) { Time.new(2017, 1, 1, 12, 15) } @@ -60,6 +62,10 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do expect(issue.updated_at).to eq(updated_at) end + it 'includes the work_item_type_id' do + expect(issue.work_item_type_id).to eq(work_item_type_id) + end + it 'is not a pull request' do expect(issue.pull_request?).to eq(false) end @@ -84,8 +90,10 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do ) end + let(:additional_data) { { work_item_type_id: work_item_type_id } } + it_behaves_like 'an Issue' do - let(:issue) { described_class.from_api_response(response) } + let(:issue) { described_class.from_api_response(response, additional_data) } end it 'does not set the user if the response did not include a user' do @@ -93,7 +101,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do .to receive(:user) .and_return(nil) - issue = described_class.from_api_response(response) + issue = described_class.from_api_response(response, additional_data) expect(issue.author).to be_nil end @@ -113,7 +121,8 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do 'author' => { 'id' => 4, 'login' => 'alice' }, 'created_at' => created_at.to_s, 'updated_at' => updated_at.to_s, - 'pull_request' => false + 'pull_request' => false, + 'work_item_type_id' => work_item_type_id } end diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb index 8eb6eedd72d..d85e298785c 100644 --- a/spec/lib/gitlab/github_import/user_finder_spec.rb +++ b/spec/lib/gitlab/github_import/user_finder_spec.rb @@ -15,32 +15,64 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do let(:finder) { described_class.new(project, client) } describe '#author_id_for' do - it 'returns the user ID for the author of an object' do - user = double(:user, id: 4, login: 'kittens') - note = double(:note, author: user) + context 'with default author_key' do + it 'returns the user ID for the author of an object' do + user = double(:user, id: 4, login: 'kittens') + note = double(:note, author: user) - expect(finder).to receive(:user_id_for).with(user).and_return(42) + expect(finder).to receive(:user_id_for).with(user).and_return(42) - expect(finder.author_id_for(note)).to eq([42, true]) - end + expect(finder.author_id_for(note)).to eq([42, true]) + end - it 'returns the ID of the project creator if no user ID could be found' do - user = double(:user, id: 4, login: 'kittens') - note = double(:note, author: user) + it 'returns the ID of the project creator if no user ID could be found' do + user = double(:user, id: 4, login: 'kittens') + note = double(:note, author: user) - expect(finder).to receive(:user_id_for).with(user).and_return(nil) + expect(finder).to receive(:user_id_for).with(user).and_return(nil) - expect(finder.author_id_for(note)).to eq([project.creator_id, false]) - end + expect(finder.author_id_for(note)).to eq([project.creator_id, false]) + end + + it 'returns the ID of the ghost user when the object has no user' do + note = double(:note, author: nil) - it 'returns the ID of the ghost user when the object has no user' do - note = double(:note, author: nil) + expect(finder.author_id_for(note)).to eq([User.ghost.id, true]) + end - expect(finder.author_id_for(note)).to eq([User.ghost.id, true]) + it 'returns the ID of the ghost user when the given object is nil' do + expect(finder.author_id_for(nil)).to eq([User.ghost.id, true]) + end end - it 'returns the ID of the ghost user when the given object is nil' do - expect(finder.author_id_for(nil)).to eq([User.ghost.id, true]) + context 'with a non-default author_key' do + let(:user) { double(:user, id: 4, login: 'kittens') } + + shared_examples 'user ID finder' do |author_key| + it 'returns the user ID for an object' do + expect(finder).to receive(:user_id_for).with(user).and_return(42) + + expect(finder.author_id_for(issue_event, author_key: author_key)).to eq([42, true]) + end + end + + context 'when the author_key parameter is :actor' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', actor: user) } + + it_behaves_like 'user ID finder', :actor + end + + context 'when the author_key parameter is :assignee' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assignee: user) } + + it_behaves_like 'user ID finder', :assignee + end + + context 'when the author_key parameter is :assigner' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assigner: user) } + + it_behaves_like 'user ID finder', :assigner + end end end diff --git a/spec/lib/gitlab/global_id/deprecations_spec.rb b/spec/lib/gitlab/global_id/deprecations_spec.rb index 22a4766c0a0..3824473c95b 100644 --- a/spec/lib/gitlab/global_id/deprecations_spec.rb +++ b/spec/lib/gitlab/global_id/deprecations_spec.rb @@ -1,12 +1,21 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'graphql' +require_relative '../../../../app/graphql/types/base_scalar' +require_relative '../../../../app/graphql/types/global_id_type' +require_relative '../../../support/helpers/global_id_deprecation_helpers' RSpec.describe Gitlab::GlobalId::Deprecations do include GlobalIDDeprecationHelpers - let_it_be(:deprecation_1) { described_class::Deprecation.new(old_model_name: 'Foo::Model', new_model_name: 'Bar', milestone: '9.0') } - let_it_be(:deprecation_2) { described_class::Deprecation.new(old_model_name: 'Baz', new_model_name: 'Qux::Model', milestone: '10.0') } + let(:deprecation_1) do + described_class::NameDeprecation.new(old_name: 'Foo::Model', new_name: 'Bar', milestone: '9.0') + end + + let(:deprecation_2) do + described_class::NameDeprecation.new(old_name: 'Baz', new_name: 'Qux::Model', milestone: '10.0') + end before do stub_global_id_deprecations(deprecation_1, deprecation_2) diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 72c6c8efb5e..e64555f1079 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -218,7 +218,7 @@ RSpec.describe Gitlab::Gpg do expect(Retriable).to receive(:sleep).at_least(:twice) expect(FileUtils).to receive(:remove_entry).with(tmp_dir).at_least(:twice).and_raise('Deletion failed') - expect { described_class.using_tmp_keychain { } }.to raise_error(described_class::CleanupError) + expect { described_class.using_tmp_keychain {} }.to raise_error(described_class::CleanupError) end it 'does not attempt multiple times when the deletion succeeds' do @@ -226,7 +226,7 @@ RSpec.describe Gitlab::Gpg do expect(FileUtils).to receive(:remove_entry).with(tmp_dir).once.and_raise('Deletion failed') expect(FileUtils).to receive(:remove_entry).with(tmp_dir).and_call_original - expect { described_class.using_tmp_keychain { } }.not_to raise_error + expect { described_class.using_tmp_keychain {} }.not_to raise_error expect(File.exist?(tmp_dir)).to be false end diff --git a/spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb new file mode 100644 index 00000000000..d2022a28a90 --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GrapeLogging::Loggers::TokenLogger do + subject { described_class.new } + + describe ".parameters" do + let(:token_id) { 1 } + let(:token_type) { "PersonalAccessToken" } + + describe 'when no token information is available' do + let(:mock_request) { instance_double(ActionDispatch::Request, 'env', env: {}) } + + it 'returns an empty hash' do + expect(subject.parameters(mock_request, nil)).to eq({}) + end + end + + describe 'when token information is available' do + let(:mock_request) do + instance_double(ActionDispatch::Request, 'env', + env: { + 'gitlab.api.token' => { 'token_id': token_id, 'token_type': token_type } + } + ) + end + + it 'adds the token information to log parameters' do + expect(subject.parameters(mock_request, nil)).to eq( { 'token_id': 1, 'token_type': "PersonalAccessToken" }) + end + end + end +end diff --git a/spec/lib/gitlab/graphql/deprecation_spec.rb b/spec/lib/gitlab/graphql/deprecation_spec.rb index 2931e28a6ee..c9b47219198 100644 --- a/spec/lib/gitlab/graphql/deprecation_spec.rb +++ b/spec/lib/gitlab/graphql/deprecation_spec.rb @@ -6,30 +6,57 @@ require 'active_model' RSpec.describe ::Gitlab::Graphql::Deprecation do let(:options) { {} } - subject(:deprecation) { described_class.parse(options) } + subject(:deprecation) { described_class.new(**options) } describe '.parse' do - context 'with nil' do - let(:options) { nil } + subject(:parsed_deprecation) { described_class.parse(**options) } - it 'parses to nil' do - expect(deprecation).to be_nil + context 'with no arguments' do + it 'returns nil' do + expect(parsed_deprecation).to be_nil end end - context 'with empty options' do - let(:options) { {} } + context 'with an incomplete `deprecated` argument' do + let(:options) { { deprecated: {} } } - it 'parses to an empty deprecation' do - expect(deprecation).to eq(described_class.new) + it 'parses as an invalid deprecation' do + expect(parsed_deprecation).not_to be_valid + expect(parsed_deprecation).to eq(described_class.new) end end - context 'with defined options' do - let(:options) { { reason: :renamed, milestone: '10.10' } } + context 'with a `deprecated` argument' do + let(:options) { { deprecated: { reason: :renamed, milestone: '10.10' } } } + + it 'parses as a deprecation' do + expect(parsed_deprecation).to be_valid + expect(parsed_deprecation).to eq( + described_class.new(reason: 'This was renamed', milestone: '10.10') + ) + end + end + + context 'with an `alpha` argument' do + let(:options) { { alpha: { milestone: '10.10' } } } + + it 'parses as an alpha' do + expect(parsed_deprecation).to be_valid + expect(parsed_deprecation).to eq( + described_class.new(reason: :alpha, milestone: '10.10') + ) + end + end + + context 'with both `deprecated` and `alpha` arguments' do + let(:options) do + { alpha: { milestone: '10.10' }, deprecated: { reason: :renamed, milestone: '10.10' } } + end - it 'assigns the properties' do - expect(deprecation).to eq(described_class.new(reason: 'This was renamed', milestone: '10.10')) + it 'raises an error' do + expect { parsed_deprecation }.to raise_error(ArgumentError, + '`alpha` and `deprecated` arguments cannot be passed at the same time' + ) end end end @@ -210,4 +237,20 @@ RSpec.describe ::Gitlab::Graphql::Deprecation do end end end + + describe '#alpha?' do + let(:options) { { milestone: '10.10', reason: reason } } + + context 'when `reason` is `:alpha`' do + let(:reason) { described_class::REASON_ALPHA } + + it { is_expected.to be_alpha } + end + + context 'when `reason` is not `:alpha`' do + let(:reason) { described_class::REASON_RENAMED } + + it { is_expected.not_to be_alpha } + end + end end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb deleted file mode 100644 index eecdaa3409f..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::Conditions::NotNullCondition do - describe '#build' do - let(:operators) { ['>', '>'] } - let(:before_or_after) { :after } - let(:condition) { described_class.new(arel_table, order_list, values, operators, before_or_after) } - - context 'when there is only one ordering field' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'id')] } - let(:values) { [500] } - let(:operators) { ['>'] } - - it 'generates a single condition sql' do - expected_sql = <<~SQL - ("issues"."id" > 500) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when ordering by a column attribute' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'relative_position'), double(named_function: nil, attribute_name: 'id')] } - let(:values) { [1500, 500] } - - shared_examples ':after condition' do - it 'generates :after sql' do - expected_sql = <<~SQL - ("issues"."relative_position" > 1500) - OR ( - "issues"."relative_position" = 1500 - AND - "issues"."id" > 500 - ) - OR ("issues"."relative_position" IS NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :after' do - it_behaves_like ':after condition' - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ("issues"."relative_position" > 1500) - OR ( - "issues"."relative_position" = 1500 - AND - "issues"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :foo' do - let(:before_or_after) { :foo } - - it_behaves_like ':after condition' - end - end - - context 'when ordering by LOWER' do - let(:arel_table) { Project.arel_table } - let(:relation) { Project.order(arel_table['name'].lower.asc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - let(:values) { ['Test', 500] } - - context 'when :after' do - it 'generates :after sql' do - expected_sql = <<~SQL - (LOWER("projects"."name") > 'test') - OR ( - LOWER("projects"."name") = 'test' - AND - "projects"."id" > 500 - ) - OR (LOWER("projects"."name") IS NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - (LOWER("projects"."name") > 'test') - OR ( - LOWER("projects"."name") = 'test' - AND - "projects"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb deleted file mode 100644 index 582f96299ec..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::Conditions::NullCondition do - describe '#build' do - let(:values) { [nil, 500] } - let(:operators) { [nil, '>'] } - let(:before_or_after) { :after } - let(:condition) { described_class.new(arel_table, order_list, values, operators, before_or_after) } - - context 'when ordering by a column attribute' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'relative_position'), double(named_function: nil, attribute_name: 'id')] } - - shared_examples ':after condition' do - it 'generates sql' do - expected_sql = <<~SQL - ( - "issues"."relative_position" IS NULL - AND - "issues"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :after' do - it_behaves_like ':after condition' - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ( - "issues"."relative_position" IS NULL - AND - "issues"."id" > 500 - ) - OR ("issues"."relative_position" IS NOT NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :foo' do - let(:before_or_after) { :foo } - - it_behaves_like ':after condition' - end - end - - context 'when ordering by LOWER' do - let(:arel_table) { Project.arel_table } - let(:relation) { Project.order(arel_table['name'].lower.asc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - - context 'when :after' do - it 'generates sql' do - expected_sql = <<~SQL - ( - LOWER("projects"."name") IS NULL - AND - "projects"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ( - LOWER("projects"."name") IS NULL - AND - "projects"."id" > 500 - ) - OR (LOWER("projects"."name") IS NOT NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - 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 deleted file mode 100644 index 8a2b5ae0d38..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb +++ /dev/null @@ -1,415 +0,0 @@ -# frozen_string_literal: true - -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(:context) { GraphQL::Query::Context.new(query: query_double, 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) } - let_it_be(:column_order_updated_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'updated_at', order_expression: Project.arel_table[:updated_at].asc) } - let_it_be(:column_order_created_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'created_at', order_expression: Project.arel_table[:created_at].asc) } - let_it_be(:column_order_last_repo) do - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'last_repository_check_at', - column_expression: Project.arel_table[:last_repository_check_at], - order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, - reversed_order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, - order_direction: :asc, - nullable: :nulls_last, - distinct: false) - end - - let_it_be(:column_order_last_repo_desc) do - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'last_repository_check_at', - column_expression: Project.arel_table[:last_repository_check_at], - order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, - reversed_order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, - order_direction: :desc, - nullable: :nulls_last, - distinct: false) - end - - subject(:connection) do - described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) - end - - def encoded_cursor(node) - described_class.new(nodes, context: context).cursor_for(node) - end - - def decoded_cursor(cursor) - Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) - end - - describe "With generic keyset order support" do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - it_behaves_like 'a connection with collection methods' - - it_behaves_like 'a redactable connection' do - let_it_be(:projects) { create_list(:project, 2) } - let(:unwanted) { projects.second } - end - - describe '#cursor_for' do - let(:project) { create(:project) } - let(:cursor) { connection.cursor_for(project) } - - it 'returns an encoded ID' do - expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) - end - - context 'when an order is specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) - end - end - - context 'when multiple orders are specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end - end - end - - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } - - context 'when before is passed' do - let(:arguments) { { before: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - end - end - - context 'when after is passed' do - let(:arguments) { { after: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - end - end - - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_cursor(projects[1]), - before: encoded_cursor(projects[3]) - } - end - - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) - end - end - - shared_examples 'nodes are in ascending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq(ascending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) - end - end - end - - shared_examples 'nodes are in descending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq(descending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) - end - end - end - - context 'when multiple orders with nil values are defined' do - let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 - let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 - let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 - let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 - let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 - - context 'when ascending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } - - it_behaves_like 'nodes are in ascending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - - context 'when descending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } - - it_behaves_like 'nodes are in descending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - end - - context 'when ordering by similarity' do - let_it_be(:project1) { create(:project, name: 'test') } - let_it_be(:project2) { create(:project, name: 'testing') } - let_it_be(:project3) { create(:project, name: 'tests') } - let_it_be(:project4) { create(:project, name: 'testing stuff') } - let_it_be(:project5) { create(:project, name: 'test') } - - let_it_be(:nodes) do - # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. - Project.sorted_by_similarity_desc('test', include_in_select: true) - end - - let_it_be(:descending_nodes) { nodes.to_a } - - it_behaves_like 'nodes are in descending order' - end - - context 'when an invalid cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - - it 'raises an error' do - expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - end - - describe '#nodes' do - let_it_be(:all_nodes) { create_list(:project, 5) } - - let(:paged_nodes) { subject.nodes } - - it_behaves_like 'connection with paged nodes' do - let(:paged_nodes_size) { 3 } - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - - context 'when primary key is not in original order' do - let(:nodes) { Project.order(last_repository_check_at: :desc) } - - it 'is added to end' do - sliced = subject.sliced_nodes - - order_sql = sliced.order_values.last.to_sql - - expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) - end - end - - context 'when there is no primary key' do - before do - stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) - NoPrimaryKey.class_eval do - self.table_name = 'no_primary_key' - self.primary_key = nil - end - end - - let(:nodes) { NoPrimaryKey.all } - - it 'raises an error' do - expect(NoPrimaryKey.primary_key).to be_nil - expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') - end - end - end - - describe '#has_previous_page and #has_next_page' do - # using a list of 5 items with a max_page of 3 - let_it_be(:project_list) { create_list(:project, 5) } - let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - context 'when default query' do - let(:arguments) { {} } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before is first item' do - let(:arguments) { { before: encoded_cursor(project_list.first) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - describe 'using `before`' do - context 'when before is the last item' do - let(:arguments) { { before: encoded_cursor(project_list.last) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last specified' do - let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - expect(subject.nodes).to eq [project_list[0]] - end - end - end - - describe 'using `after`' do - context 'when after is the first item' do - let(:arguments) { { after: encoded_cursor(project_list.first) } } - - it 'has a previous, and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when after and first specified' do - let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } - - it 'has a previous but no next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_falsey - end - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 6574b3e3131..b54c618d8e0 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -5,10 +5,38 @@ 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(:context) { GraphQL::Query::Context.new(query: query_double, 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) } + let_it_be(:column_order_updated_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'updated_at', order_expression: Project.arel_table[:updated_at].asc) } + let_it_be(:column_order_created_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'created_at', order_expression: Project.arel_table[:created_at].asc) } + let_it_be(:column_order_last_repo) do + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'last_repository_check_at', + column_expression: Project.arel_table[:last_repository_check_at], + order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, + reversed_order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, + order_direction: :asc, + nullable: :nulls_last, + distinct: false) + end + + let_it_be(:column_order_last_repo_desc) do + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'last_repository_check_at', + column_expression: Project.arel_table[:last_repository_check_at], + order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, + reversed_order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, + order_direction: :desc, + nullable: :nulls_last, + distinct: false) + end + subject(:connection) do described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) end @@ -21,414 +49,293 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) end - # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358 - context 'the relation has been preloaded' do - let(:projects) { Project.all.preload(:issues) } - let(:nodes) { projects.first.issues } - - before do - project = create(:project) - create_list(:issue, 3, project: project) - end - - it 'is loaded' do - expect(nodes).to be_loaded - end - - it 'does not error when accessing pagination information' do - connection.first = 2 - - expect(connection).to have_attributes( - has_previous_page: false, - has_next_page: true - ) - end - - it 'can generate cursors' do - connection.send(:ordered_items) # necessary to generate the order-list - - expect(connection.cursor_for(nodes.first)).to be_a(String) - end - - it 'can read the next page' do - connection.send(:ordered_items) # necessary to generate the order-list - ordered = nodes.reorder(id: :desc) - next_page = described_class.new(nodes, - context: context, - max_page_size: 3, - after: connection.cursor_for(ordered.second)) - - expect(next_page.sliced_nodes).to contain_exactly(ordered.third) - end - end - - it_behaves_like 'a connection with collection methods' - - it_behaves_like 'a redactable connection' do - let_it_be(:projects) { create_list(:project, 2) } - let(:unwanted) { projects.second } - end - - describe '#cursor_for' do - let(:project) { create(:project) } - let(:cursor) { connection.cursor_for(project) } - - it 'returns an encoded ID' do - expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) - end - - context 'when SimpleOrderBuilder cannot build keyset paginated query' do - it 'increments the `old_keyset_pagination_usage` counter', :prometheus do - expect(Gitlab::Pagination::Keyset::SimpleOrderBuilder).to receive(:build).and_return([false, nil]) - - decoded_cursor(cursor) - - counter = Gitlab::Metrics.registry.get(:old_keyset_pagination_usage) - expect(counter.get(model: 'Project')).to eq(1) - end - end - - context 'when an order is specified' do - let(:nodes) { Project.order(:updated_at) } + describe "with generic keyset order support" do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end - - it 'includes the :id even when not specified in the order' do - expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) - end - end + it_behaves_like 'a connection with collection methods' - context 'when multiple orders are specified' do - let(:nodes) { Project.order(:updated_at).order(:created_at) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end + it_behaves_like 'a redactable connection' do + let_it_be(:projects) { create_list(:project, 2) } + let(:unwanted) { projects.second } end - context 'when multiple orders with SQL are specified' do - let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } + describe '#cursor_for' do + let(:project) { create(:project) } + let(:cursor) { connection.cursor_for(project) } - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) + it 'returns an encoded ID' do + expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) end - end - end - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } + context 'when an order is specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - context 'when before is passed' do - let(:arguments) { { before: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) + end end - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } + context 'when multiple orders are specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end end end - context 'when after is passed' do - let(:arguments) { { after: encoded_cursor(projects[1]) } } + describe '#sliced_nodes' do + let(:projects) { create_list(:project, 4) } - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end + context 'when before is passed' do + let(:arguments) { { before: encoded_cursor(projects[1]) } } - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } - - it 'returns the correct nodes' do + it 'only returns the project before the selected one' do expect(subject.sliced_nodes).to contain_exactly(projects.first) end - end - end - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_cursor(projects[1]), - before: encoded_cursor(projects[3]) - } - end + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + end + end end - end - shared_examples 'nodes are in ascending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } + context 'when after is passed' do + let(:arguments) { { after: encoded_cursor(projects[1]) } } - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq(ascending_nodes) + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) end - end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end end end - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } + context 'when both before and after are passed' do + let(:arguments) do + { + after: encoded_cursor(projects[1]), + before: encoded_cursor(projects[3]) + } + end - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + it 'returns the expected set' do + expect(subject.sliced_nodes).to contain_exactly(projects[2]) end end - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } + shared_examples 'nodes are in ascending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + it 'returns projects in ascending order' do + expect(subject.sliced_nodes).to eq(ascending_nodes) + end end - end - end - shared_examples 'nodes are in descending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq(descending_nodes) + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + end end - end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + end end - end - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + end end end - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } + shared_examples 'nodes are in descending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + it 'only returns projects in descending order' do + expect(subject.sliced_nodes).to eq(descending_nodes) + end end - end - end - context 'when ordering uses LOWER' do - let!(:project1) { create(:project, name: 'A') } # Asc: project1 Desc: project4 - let!(:project2) { create(:project, name: 'c') } # Asc: project5 Desc: project2 - let!(:project3) { create(:project, name: 'b') } # Asc: project3 Desc: project3 - let!(:project4) { create(:project, name: 'd') } # Asc: project2 Desc: project5 - let!(:project5) { create(:project, name: 'a') } # Asc: project4 Desc: project1 + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } - context 'when ascending' do - let(:nodes) do - Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(id: :asc) + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + end end - let(:ascending_nodes) { [project1, project5, project3, project2, project4] } + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } - it_behaves_like 'nodes are in ascending order' - end - - context 'when descending' do - let(:nodes) do - Project.order(Arel::Table.new(:projects)['name'].lower.desc).order(id: :desc) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + end end - let(:descending_nodes) { [project4, project2, project3, project5, project1] } + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } - it_behaves_like 'nodes are in descending order' + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + end + end end - end - context 'NULLS order' do - using RSpec::Parameterized::TableSyntax + context 'when multiple orders with nil values are defined' do + let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 + let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 + let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 + let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 + let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 - let_it_be(:issue1) { create(:issue, relative_position: nil) } - let_it_be(:issue2) { create(:issue, relative_position: 100) } - let_it_be(:issue3) { create(:issue, relative_position: 200) } - let_it_be(:issue4) { create(:issue, relative_position: nil) } - let_it_be(:issue5) { create(:issue, relative_position: 300) } + context 'when ascending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } - context 'when ascending NULLS LAST (ties broken by id DESC implicitly)' do - let(:ascending_nodes) { [issue2, issue3, issue5, issue4, issue1] } + it_behaves_like 'nodes are in ascending order' - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].asc.nulls_last) } - ] - end + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } - with_them do - it_behaves_like 'nodes are in ascending order' - end - end + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) + end + end - context 'when descending NULLS LAST (ties broken by id DESC implicitly)' do - let(:descending_nodes) { [issue5, issue3, issue2, issue4, issue1] } + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].desc.nulls_last) } -] + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end end - with_them do + context 'when descending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } + it_behaves_like 'nodes are in descending order' - end - end - context 'when ascending NULLS FIRST with a tie breaker' do - let(:ascending_nodes) { [issue1, issue4, issue2, issue3, issue5] } + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].asc.nulls_first).order(id: :asc) } -] - end + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) + end + end - with_them do - it_behaves_like 'nodes are in ascending order' + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end end end - context 'when descending NULLS FIRST with a tie breaker' do - let(:descending_nodes) { [issue1, issue4, issue5, issue3, issue2] } + context 'when ordering by similarity' do + let_it_be(:project1) { create(:project, name: 'test') } + let_it_be(:project2) { create(:project, name: 'testing') } + let_it_be(:project3) { create(:project, name: 'tests') } + let_it_be(:project4) { create(:project, name: 'testing stuff') } + let_it_be(:project5) { create(:project, name: 'test') } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].desc.nulls_first).order(id: :asc) } -] + let_it_be(:nodes) do + # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. + Project.sorted_by_similarity_desc('test', include_in_select: true) end - with_them do - it_behaves_like 'nodes are in descending order' - end - end - end + let_it_be(:descending_nodes) { nodes.to_a } - context 'when ordering by similarity' do - let!(:project1) { create(:project, name: 'test') } - let!(:project2) { create(:project, name: 'testing') } - let!(:project3) { create(:project, name: 'tests') } - let!(:project4) { create(:project, name: 'testing stuff') } - let!(:project5) { create(:project, name: 'test') } - - let(:nodes) do - Project.sorted_by_similarity_desc('test', include_in_select: true) + it_behaves_like 'nodes are in descending order' end - let(:descending_nodes) { nodes.to_a } - - it_behaves_like 'nodes are in descending order' - end + context 'when an invalid cursor is provided' do + let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - context 'when an invalid cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - - it 'raises an error' do - expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + it 'raises an error' do + expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end end end - end - describe '#nodes' do - let_it_be(:all_nodes) { create_list(:project, 5) } + describe '#nodes' do + let_it_be(:all_nodes) { create_list(:project, 5) } - let(:paged_nodes) { subject.nodes } + let(:paged_nodes) { subject.nodes } - it_behaves_like 'connection with paged nodes' do - let(:paged_nodes_size) { 3 } - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + it_behaves_like 'connection with paged nodes' do + let(:paged_nodes_size) { 3 } end - end - context 'when primary key is not in original order' do - let(:nodes) { Project.order(last_repository_check_at: :desc) } + context 'when both are passed' do + let(:arguments) { { first: 2, last: 2 } } - before do - stub_feature_flags(new_graphql_keyset_pagination: false) + it 'raises an error' do + expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end end - it 'is added to end' do - sliced = subject.sliced_nodes + context 'when primary key is not in original order' do + let(:nodes) { Project.order(last_repository_check_at: :desc) } - order_sql = sliced.order_values.last.to_sql + it 'is added to end' do + sliced = subject.sliced_nodes - expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) - end - end + order_sql = sliced.order_values.last.to_sql - context 'when there is no primary key' do - before do - stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) - NoPrimaryKey.class_eval do - self.table_name = 'no_primary_key' - self.primary_key = nil + expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) end end - let(:nodes) { NoPrimaryKey.all } - - it 'raises an error' do - expect(NoPrimaryKey.primary_key).to be_nil - expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') - end - end - end - - describe '#has_previous_page and #has_next_page' do - # using a list of 5 items with a max_page of 3 - let_it_be(:project_list) { create_list(:project, 5) } - let_it_be(:nodes) { Project.order(:id) } + context 'when there is no primary key' do + before do + stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) + NoPrimaryKey.class_eval do + self.table_name = 'no_primary_key' + self.primary_key = nil + end + end - context 'when default query' do - let(:arguments) { {} } + let(:nodes) { NoPrimaryKey.all } - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy + it 'raises an error' do + expect(NoPrimaryKey.primary_key).to be_nil + expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') + end end end - context 'when before is first item' do - let(:arguments) { { before: encoded_cursor(project_list.first) } } + describe '#has_previous_page and #has_next_page' do + # using a list of 5 items with a max_page of 3 + let_it_be(:project_list) { create_list(:project, 5) } + let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - describe 'using `before`' do - context 'when before is the last item' do - let(:arguments) { { before: encoded_cursor(project_list.last) } } + context 'when default query' do + let(:arguments) { {} } it 'has no previous, but a next' do expect(subject.has_previous_page).to be_falsey @@ -436,51 +343,71 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do end end - context 'when before and last specified' do - let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } + context 'when before is first item' do + let(:arguments) { { before: encoded_cursor(project_list.first) } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey expect(subject.has_next_page).to be_truthy end end - context 'when before and last does request all remaining nodes' do - let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } + describe 'using `before`' do + context 'when before is the last item' do + let(:arguments) { { before: encoded_cursor(project_list.last) } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - expect(subject.nodes).to eq [project_list[0]] + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + end end - end - end - describe 'using `after`' do - context 'when after is the first item' do - let(:arguments) { { after: encoded_cursor(project_list.first) } } + context 'when before and last specified' do + let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } - it 'has a previous, and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end end - end - context 'when after and first specified' do - let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } + context 'when before and last does request all remaining nodes' do + let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + expect(subject.nodes).to eq [project_list[0]] + end end end - context 'when before and last does request all remaining nodes' do - let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } + describe 'using `after`' do + context 'when after is the first item' do + let(:arguments) { { after: encoded_cursor(project_list.first) } } + + it 'has a previous, and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when after and first specified' do + let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } + + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before and last does request all remaining nodes' do + let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } - it 'has a previous but no next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_falsey + it 'has a previous but no next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_falsey + end end end end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb deleted file mode 100644 index 40ee47ece49..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do - describe '#build_order_list' do - let(:order_list) { described_class.build_order_list(relation) } - - context 'when multiple orders with SQL is specified' do - let(:relation) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } - - it 'ignores the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'updated_at' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when order contains NULLS LAST' do - let(:relation) { Project.order(Arel.sql('projects.updated_at Asc Nulls Last')).order(:id) } - - it 'does not ignore the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'projects.updated_at' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when order contains invalid formatted NULLS LAST ' do - let(:relation) { Project.order(Arel.sql('projects.updated_at created_at Asc Nulls Last')).order(:id) } - - it 'ignores the SQL order' do - expect(order_list.count).to eq 1 - end - end - - context 'when order contains LOWER' do - let(:relation) { Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(:id) } - - it 'does not ignore the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'name' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction) - expect(order_list.first.named_function.to_sql).to eq 'LOWER("projects"."name")' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when ordering by CASE', :aggregate_failuers do - let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) } - - it 'assigns the right attribute name, named function, and direction' do - expect(order_list.count).to eq 1 - expect(order_list.first.attribute_name).to eq 'case_order_value' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case) - expect(order_list.first.sort_direction).to eq :asc - end - end - - context 'when ordering by ARRAY_POSITION', :aggregate_failuers do - let(:array_position) { Arel::Nodes::NamedFunction.new('ARRAY_POSITION', [Arel.sql("ARRAY[1,0]::smallint[]"), Project.arel_table[:auto_cancel_pending_pipelines]]) } - let(:relation) { Project.order(array_position.asc) } - - it 'assigns the right attribute name, named function, and direction' do - expect(order_list.count).to eq 1 - expect(order_list.first.attribute_name).to eq 'array_position' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction) - expect(order_list.first.sort_direction).to eq :asc - end - end - end - - describe '#validate_ordering' do - let(:order_list) { described_class.build_order_list(relation) } - - context 'when number of ordering fields is 0' do - let(:relation) { Project.all } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, 'A minimum of 1 ordering field is required') - end - end - - context 'when number of ordering fields is over 2' do - let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc).order(:id) } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, 'A maximum of 2 ordering fields are allowed') - end - end - - context 'when the second (or first) column is nullable' do - let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc) } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, "Column `updated_at` must not allow NULL") - end - end - - context 'for last ordering field' do - let(:relation) { Project.order(namespace_id: :desc) } - - it 'raises error if primary key is not last field' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, "Last ordering field must be the primary key, `#{relation.primary_key}`") - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb deleted file mode 100644 index 31c02fd43e8..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::QueryBuilder do - context 'when number of ordering fields is 0' do - it 'raises an error' do - expect { described_class.new(Issue.arel_table, [], {}, :after) } - .to raise_error(ArgumentError, 'No ordering scopes have been supplied') - end - end - - describe '#conditions' do - let(:relation) { Issue.order(relative_position: :desc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - let(:arel_table) { Issue.arel_table } - let(:builder) { described_class.new(arel_table, order_list, decoded_cursor, before_or_after) } - let(:before_or_after) { :after } - - context 'when only a single ordering' do - let(:relation) { Issue.order(id: :desc) } - - context 'when the value is nil' do - let(:decoded_cursor) { { 'id' => nil } } - - it 'raises an error' do - expect { builder.conditions } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Before/after cursor invalid: `nil` was provided as only sortable value') - end - end - - context 'when value is not nil' do - let(:decoded_cursor) { { 'id' => 100 } } - let(:conditions) { builder.conditions } - - context 'when :after' do - it 'generates the correct condition' do - expect(conditions.strip).to eq '("issues"."id" < 100)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - expect(conditions.strip).to eq '("issues"."id" > 100)' - end - end - end - end - - context 'when two orderings' do - let(:decoded_cursor) { { 'relative_position' => 1500, 'id' => 100 } } - - context 'when no values are nil' do - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" < 1500' - expect(conditions).to include '"issues"."id" > 100' - expect(conditions).to include 'OR ("issues"."relative_position" IS NULL)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '("issues"."relative_position" > 1500)' - expect(conditions).to include '"issues"."id" < 100' - expect(conditions).to include '"issues"."relative_position" = 1500' - end - end - end - - context 'when first value is nil' do - let(:decoded_cursor) { { 'relative_position' => nil, 'id' => 100 } } - - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" IS NULL' - expect(conditions).to include '"issues"."id" > 100' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" IS NULL' - expect(conditions).to include '"issues"."id" < 100' - expect(conditions).to include 'OR ("issues"."relative_position" IS NOT NULL)' - end - end - end - end - - context 'when sorting using LOWER' do - let(:relation) { Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(:id) } - let(:arel_table) { Project.arel_table } - let(:decoded_cursor) { { 'name' => 'Test', 'id' => 100 } } - - context 'when no values are nil' do - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '(LOWER("projects"."name") > \'test\')' - expect(conditions).to include '"projects"."id" > 100' - expect(conditions).to include 'OR (LOWER("projects"."name") IS NULL)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '(LOWER("projects"."name") < \'test\')' - expect(conditions).to include '"projects"."id" < 100' - expect(conditions).to include 'LOWER("projects"."name") = \'test\'' - end - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/type_name_deprecations_spec.rb b/spec/lib/gitlab/graphql/type_name_deprecations_spec.rb new file mode 100644 index 00000000000..0505e709a3b --- /dev/null +++ b/spec/lib/gitlab/graphql/type_name_deprecations_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_relative '../../../support/helpers/type_name_deprecation_helpers' + +RSpec.describe Gitlab::Graphql::TypeNameDeprecations do + include TypeNameDeprecationHelpers + + let(:deprecation_1) do + described_class::NameDeprecation.new(old_name: 'Foo::Model', new_name: 'Bar', milestone: '9.0') + end + + let(:deprecation_2) do + described_class::NameDeprecation.new(old_name: 'Baz', new_name: 'Qux::Model', milestone: '10.0') + end + + before do + stub_type_name_deprecations(deprecation_1, deprecation_2) + end + + describe '.deprecated?' do + it 'returns a boolean to signal if model name has a deprecation', :aggregate_failures do + expect(described_class.deprecated?('Foo::Model')).to eq(true) + expect(described_class.deprecated?('Qux::Model')).to eq(false) + end + end + + describe '.deprecation_for' do + it 'returns the deprecation for the model if it exists', :aggregate_failures do + expect(described_class.deprecation_for('Foo::Model')).to eq(deprecation_1) + expect(described_class.deprecation_for('Qux::Model')).to be_nil + end + end + + describe '.deprecation_by' do + it 'returns the deprecation by the model if it exists', :aggregate_failures do + expect(described_class.deprecation_by('Foo::Model')).to be_nil + expect(described_class.deprecation_by('Qux::Model')).to eq(deprecation_2) + end + end + + describe '.apply_to_graphql_name' do + it 'returns the corresponding graphql_name of the GID for the new model', :aggregate_failures do + expect(described_class.apply_to_graphql_name('Foo::Model')).to eq('Bar') + expect(described_class.apply_to_graphql_name('Baz')).to eq('Qux::Model') + end + + it 'returns the same value if there is no deprecation' do + expect(described_class.apply_to_graphql_name('Project')).to eq('Project') + end + end +end diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb index 79cec2d8705..c3c696ceedc 100644 --- a/spec/lib/gitlab/graphs/commits_spec.rb +++ b/spec/lib/gitlab/graphs/commits_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Graphs::Commits do let!(:project) { create(:project, :public) } let!(:commit1) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: Time.now) } - let!(:commit1_yesterday) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: 1.day.ago)} + let!(:commit1_yesterday) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: 1.day.ago) } let!(:commit2) { create(:commit, git_commit: RepoHelpers.another_sample_commit, project: project, committed_date: Time.now) } diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 537e59d91c3..d7ae6ed06a4 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -71,7 +71,7 @@ RSpec.describe Gitlab::Highlight do context 'diff highlighting' do let(:file_name) { 'test.diff' } - let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"} + let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n" } let(:expected) do %q(<span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> <span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> diff --git a/spec/lib/gitlab/hook_data/group_builder_spec.rb b/spec/lib/gitlab/hook_data/group_builder_spec.rb index d7347ff99d4..4e6152390a4 100644 --- a/spec/lib/gitlab/hook_data/group_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/group_builder_spec.rb @@ -38,6 +38,7 @@ RSpec.describe Gitlab::HookData::GroupBuilder do let(:event) { :create } it { expect(event_name).to eq('group_create') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include old path attributes' end @@ -46,6 +47,7 @@ RSpec.describe Gitlab::HookData::GroupBuilder do let(:event) { :destroy } it { expect(event_name).to eq('group_destroy') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include old path attributes' end @@ -54,6 +56,7 @@ RSpec.describe Gitlab::HookData::GroupBuilder do let(:event) { :rename } it { expect(event_name).to eq('group_rename') } + it_behaves_like 'includes the required attributes' it 'includes old path details' do diff --git a/spec/lib/gitlab/hook_data/group_member_builder_spec.rb b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb index 78c62fd23c7..35ce31ab897 100644 --- a/spec/lib/gitlab/hook_data/group_member_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb @@ -39,6 +39,7 @@ RSpec.describe Gitlab::HookData::GroupMemberBuilder do let(:event) { :create } it { expect(event_name).to eq('user_add_to_group') } + it_behaves_like 'includes the required attributes' end @@ -46,6 +47,7 @@ RSpec.describe Gitlab::HookData::GroupMemberBuilder do let(:event) { :update } it { expect(event_name).to eq('user_update_for_group') } + it_behaves_like 'includes the required attributes' end @@ -53,6 +55,7 @@ RSpec.describe Gitlab::HookData::GroupMemberBuilder do let(:event) { :destroy } it { expect(event_name).to eq('user_remove_from_group') } + it_behaves_like 'includes the required attributes' end end diff --git a/spec/lib/gitlab/hook_data/key_builder_spec.rb b/spec/lib/gitlab/hook_data/key_builder_spec.rb index 86f33df115f..2c87c9a10e6 100644 --- a/spec/lib/gitlab/hook_data/key_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/key_builder_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Gitlab::HookData::KeyBuilder do it { expect(event_name).to eq('key_create') } it { expect(data[:username]).to eq(key.user.username) } + it_behaves_like 'includes the required attributes' end @@ -44,6 +45,7 @@ RSpec.describe Gitlab::HookData::KeyBuilder do it { expect(event_name).to eq('key_destroy') } it { expect(data[:username]).to eq(key.user.username) } + it_behaves_like 'includes the required attributes' end end @@ -58,6 +60,7 @@ RSpec.describe Gitlab::HookData::KeyBuilder do let(:event) { :create } it { expect(event_name).to eq('key_create') } + it_behaves_like 'includes the required attributes' end @@ -65,6 +68,7 @@ RSpec.describe Gitlab::HookData::KeyBuilder do let(:event) { :destroy } it { expect(event_name).to eq('key_destroy') } + it_behaves_like 'includes the required attributes' end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 25b84a67ab2..cb8fef60ab2 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -29,6 +29,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do merge_user_id merge_when_pipeline_succeeds milestone_id + reviewer_ids source_branch source_project_id state_id @@ -72,6 +73,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do human_time_estimate assignee_ids assignee_id + reviewer_ids labels state blocking_discussions_resolved diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb index e86ac66b1ad..729712510ea 100644 --- a/spec/lib/gitlab/hook_data/project_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb @@ -52,6 +52,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do let(:event) { :create } it { expect(event_name).to eq('project_create') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include `old_path_with_namespace` attribute' end @@ -60,6 +61,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do let(:event) { :destroy } it { expect(event_name).to eq('project_destroy') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include `old_path_with_namespace` attribute' end @@ -68,6 +70,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do let(:event) { :rename } it { expect(event_name).to eq('project_rename') } + it_behaves_like 'includes the required attributes' it_behaves_like 'includes `old_path_with_namespace` attribute' end @@ -76,6 +79,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do let(:event) { :transfer } it { expect(event_name).to eq('project_transfer') } + it_behaves_like 'includes the required attributes' it_behaves_like 'includes `old_path_with_namespace` attribute' end diff --git a/spec/lib/gitlab/hook_data/project_member_builder_spec.rb b/spec/lib/gitlab/hook_data/project_member_builder_spec.rb index 3fb84223581..76446adf7b7 100644 --- a/spec/lib/gitlab/hook_data/project_member_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/project_member_builder_spec.rb @@ -37,6 +37,7 @@ RSpec.describe Gitlab::HookData::ProjectMemberBuilder do let(:event) { :create } it { expect(event_name).to eq('user_add_to_team') } + it_behaves_like 'includes the required attributes' end @@ -44,6 +45,7 @@ RSpec.describe Gitlab::HookData::ProjectMemberBuilder do let(:event) { :update } it { expect(event_name).to eq('user_update_for_team') } + it_behaves_like 'includes the required attributes' end @@ -51,6 +53,7 @@ RSpec.describe Gitlab::HookData::ProjectMemberBuilder do let(:event) { :destroy } it { expect(event_name).to eq('user_remove_from_team') } + it_behaves_like 'includes the required attributes' end end diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb index 89e5dffd7b4..b25320af891 100644 --- a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb @@ -38,6 +38,7 @@ RSpec.describe Gitlab::HookData::SubgroupBuilder do let(:event) { :create } it { expect(event_name).to eq('subgroup_create') } + it_behaves_like 'includes the required attributes' end @@ -45,6 +46,7 @@ RSpec.describe Gitlab::HookData::SubgroupBuilder do let(:event) { :destroy } it { expect(event_name).to eq('subgroup_destroy') } + it_behaves_like 'includes the required attributes' end end diff --git a/spec/lib/gitlab/hook_data/user_builder_spec.rb b/spec/lib/gitlab/hook_data/user_builder_spec.rb index f971089850b..ae844308fb1 100644 --- a/spec/lib/gitlab/hook_data/user_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/user_builder_spec.rb @@ -44,6 +44,7 @@ RSpec.describe Gitlab::HookData::UserBuilder do let(:event) { :create } it { expect(event_name).to eq('user_create') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include old username attributes' it_behaves_like 'does not include state attributes' @@ -53,6 +54,7 @@ RSpec.describe Gitlab::HookData::UserBuilder do let(:event) { :destroy } it { expect(event_name).to eq('user_destroy') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include old username attributes' it_behaves_like 'does not include state attributes' @@ -62,6 +64,7 @@ RSpec.describe Gitlab::HookData::UserBuilder do let(:event) { :rename } it { expect(event_name).to eq('user_rename') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include state attributes' @@ -76,6 +79,7 @@ RSpec.describe Gitlab::HookData::UserBuilder do let(:event) { :failed_login } it { expect(event_name).to eq('user_failed_login') } + it_behaves_like 'includes the required attributes' it_behaves_like 'does not include old username attributes' diff --git a/spec/lib/gitlab/http_io_spec.rb b/spec/lib/gitlab/http_io_spec.rb index 5ba0cb5e686..1376b726df3 100644 --- a/spec/lib/gitlab/http_io_spec.rb +++ b/spec/lib/gitlab/http_io_spec.rb @@ -262,7 +262,7 @@ RSpec.describe Gitlab::HttpIO do end it 'reads a trace' do - expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) + expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError, 'Unexpected response code: 500') end end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb index 451fd6c6f46..42cf9c54798 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -9,12 +9,21 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do allow_next_instance_of(ProjectExportWorker) do |job| allow(job).to receive(:jid).and_return(SecureRandom.hex(8)) end + + stub_feature_flags(import_export_web_upload_stream: false) + stub_uploads_object_storage(FileUploader, enabled: false) end let(:example_url) { 'http://www.example.com' } let(:strategy) { subject.new(url: example_url, http_method: 'post') } - let!(:project) { create(:project, :with_export) } - let!(:user) { build(:user) } + let(:user) { build(:user) } + let(:project) { import_export_upload.project } + let(:import_export_upload) do + create( + :import_export_upload, + export_file: fixture_file_upload('spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz') + ) + end subject { described_class } @@ -36,20 +45,42 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do describe '#execute' do context 'when upload succeeds' do before do - allow(strategy).to receive(:send_file) - allow(strategy).to receive(:handle_response_error) + stub_full_request(example_url, method: :post).to_return(status: 200) end - it 'does not remove the exported project file after the upload' do + it 'does not remove the exported project file after the upload', :aggregate_failures do expect(project).not_to receive(:remove_exports) - strategy.execute(user, project) + expect { strategy.execute(user, project) }.not_to change(project, :export_status) + + expect(project.export_status).to eq(:finished) end - it 'has finished export status' do - strategy.execute(user, project) + it 'logs when upload starts and finishes' do + export_size = import_export_upload.export_file.size + + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:info).ordered.with( + { + message: "Started uploading project", + project_id: project.id, + project_name: project.name, + export_size: export_size + } + ) + + expect(logger).to receive(:info).ordered.with( + { + message: "Finished uploading project", + project_id: project.id, + project_name: project.name, + export_size: export_size, + upload_duration: anything + } + ) + end - expect(project.export_status).to eq(:finished) + strategy.execute(user, project) end end @@ -64,5 +95,124 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do expect(errors.first).to eq "Error uploading the project. Code 404: Page not found" end end + + context 'when object store is disabled' do + it 'reads file from disk and uploads to external url' do + stub_request(:post, example_url).to_return(status: 200) + expect(Gitlab::ImportExport::RemoteStreamUpload).not_to receive(:new) + expect(Gitlab::HttpIO).not_to receive(:new) + + strategy.execute(user, project) + + expect(a_request(:post, example_url)).to have_been_made + end + end + + context 'when object store is enabled' do + before do + object_store_url = 'http://object-storage/project.tar.gz' + stub_uploads_object_storage(FileUploader) + stub_request(:get, object_store_url) + stub_request(:post, example_url) + allow(import_export_upload.export_file).to receive(:url).and_return(object_store_url) + allow(import_export_upload.export_file).to receive(:file_storage?).and_return(false) + end + + it 'reads file using Gitlab::HttpIO and uploads to external url' do + expect_next_instance_of(Gitlab::HttpIO) do |http_io| + expect(http_io).to receive(:read).and_call_original + end + expect(Gitlab::ImportExport::RemoteStreamUpload).not_to receive(:new) + + strategy.execute(user, project) + + expect(a_request(:post, example_url)).to have_been_made + end + end + + context 'when `import_export_web_upload_stream` feature is enabled' do + before do + stub_feature_flags(import_export_web_upload_stream: true) + end + + context 'when remote object store is disabled' do + it 'reads file from disk and uploads to external url' do + stub_request(:post, example_url).to_return(status: 200) + expect(Gitlab::ImportExport::RemoteStreamUpload).not_to receive(:new) + expect(Gitlab::HttpIO).not_to receive(:new) + + strategy.execute(user, project) + + expect(a_request(:post, example_url)).to have_been_made + end + end + + context 'when object store is enabled' do + let(:object_store_url) { 'http://object-storage/project.tar.gz' } + + before do + stub_uploads_object_storage(FileUploader) + + allow(import_export_upload.export_file).to receive(:url).and_return(object_store_url) + allow(import_export_upload.export_file).to receive(:file_storage?).and_return(false) + end + + it 'uploads file as a remote stream' do + arguments = { + download_url: object_store_url, + upload_url: example_url, + options: { + upload_method: :post, + upload_content_type: 'application/gzip' + } + } + + expect_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload, arguments) do |remote_stream_upload| + expect(remote_stream_upload).to receive(:execute) + end + expect(Gitlab::HttpIO).not_to receive(:new) + + strategy.execute(user, project) + end + + context 'when upload as remote stream raises an exception' do + before do + allow_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload) do |remote_stream_upload| + allow(remote_stream_upload).to receive(:execute).and_raise( + Gitlab::ImportExport::RemoteStreamUpload::StreamError.new('Exception error message', 'Response body') + ) + end + end + + it 'logs the exception and stores the error message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).ordered.with( + { + project_id: project.id, + project_name: project.name, + message: 'Exception error message', + response_body: 'Response body' + } + ) + + expect(logger).to receive(:error).ordered.with( + { + project_id: project.id, + project_name: project.name, + message: 'After export strategy failed', + 'exception.class' => 'Gitlab::ImportExport::RemoteStreamUpload::StreamError', + 'exception.message' => 'Exception error message', + 'exception.backtrace' => anything + } + ) + end + + strategy.execute(user, project) + + expect(project.import_export_shared.errors.first).to eq('Exception error message') + end + end + end + end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8c1e60e78b0..9aec3271913 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -140,6 +140,12 @@ project_members: - project - member_task - member_namespace +- member_role +member_roles: +- members +- namespace +- base_access_level +- download_code merge_requests: - status_check_responses - subscriptions @@ -591,6 +597,7 @@ project: - alert_management_alerts - repository_storage_moves - freeze_periods +- pumble_integration - webex_teams_integration - build_report_results - vulnerability_statistic @@ -621,6 +628,7 @@ project: - security_trainings - vulnerability_reads - build_artifacts_size_refresh +- project_callouts award_emoji: - awardable - user @@ -646,6 +654,11 @@ search_data: merge_request_assignees: - merge_request - assignee +- updated_state_by +merge_request_reviewers: +- merge_request +- reviewer +- updated_state_by lfs_file_locks: - user project_badges: @@ -805,3 +818,6 @@ bulk_import_export: - group service_desk_setting: - file_template_project +approvals: + - user + - merge_request diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb index b8999f608b1..4ef8f4b5d76 100644 --- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb @@ -139,6 +139,30 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do expect(subject.value).to be_nil end end + + context 'with duplicate assignees' do + let(:relation_sym) { :issues } + let(:relation_hash) do + { "title" => "title", "state" => "opened" }.merge(issue_assignees) + end + + context 'when duplicate assignees are present' do + let(:issue_assignees) do + { + "issue_assignees" => [ + IssueAssignee.new(user_id: 1), + IssueAssignee.new(user_id: 2), + IssueAssignee.new(user_id: 1), + { user_id: 3 } + ] + } + end + + it 'removes duplicate assignees' do + expect(subject.issue_assignees.map(&:user_id)).to contain_exactly(1, 2) + end + end + end end end diff --git a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb index 7c84b9604a6..9f1b15aa049 100644 --- a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb @@ -58,8 +58,8 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do end context 'when subrelation collection count is small' do - let(:notes) { build_list(:note, 2, project: project, importing: true) } - let(:relation_object) { build(:issue, project: project, notes: notes) } + let(:note) { build(:note, project: project, importing: true) } + let(:relation_object) { build(:issue, project: project, notes: [note]) } let(:relation_definition) { { 'notes' => {} } } it 'saves subrelation as part of the relation object itself' do @@ -68,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do saver.execute issue = project.issues.last - expect(issue.notes.count).to eq(2) + expect(issue.notes.count).to eq(1) end end diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index dea584e5019..9af72cc0dea 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -51,10 +51,11 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do shared_examples 'logs raised exception and terminates validator process group' do let(:std) { double(:std, close: nil, value: nil) } let(:wait_thr) { double } + let(:wait_threads) { [wait_thr, wait_thr] } before do allow(Process).to receive(:getpgid).and_return(2) - allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr]) + allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads]) allow(wait_thr).to receive(:[]).with(:pid).and_return(1) allow(wait_thr).to receive(:value).and_raise(exception) end @@ -67,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do import_upload_archive_size: File.size(filepath), message: error_message ) - expect(Process).to receive(:kill).with(-1, 2) + expect(Process).to receive(:kill).with(-1, 2).twice expect(subject.valid?).to eq(false) end end diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb index 9b01005c2e9..89ae869ae86 100644 --- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb @@ -204,19 +204,5 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do end end - context 'when import_relation_object_persistence feature flag is enabled' do - before do - stub_feature_flags(import_relation_object_persistence: true) - end - - include_examples 'group restoration' - end - - context 'when import_relation_object_persistence feature flag is disabled' do - before do - stub_feature_flags(import_relation_object_persistence: false) - end - - include_examples 'group restoration' - end + include_examples 'group restoration' end diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb index 90966cb4915..51c0008b2b4 100644 --- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb +++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb @@ -88,8 +88,8 @@ RSpec.describe 'Test coverage of the Project Import' do def relations_from_json(json_file) json = Gitlab::Json.parse(IO.read(json_file)) - [].tap {|res| gather_relations({ project: json }, res, [])} - .map {|relation_names| relation_names.join('.')} + [].tap { |res| gather_relations({ project: json }, res, []) } + .map { |relation_names| relation_names.join('.') } end def gather_relations(item, res, path) @@ -103,7 +103,7 @@ RSpec.describe 'Test coverage of the Project Import' do end end when Array - item.each {|i| gather_relations(i, res, path)} + item.each { |i| gather_relations(i, res, path) } end end diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb index 9be95591ae9..452d63d548e 100644 --- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb +++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonWriter do file_path = File.join(path, exportable_path, "#{relation}.ndjson") subject.write_relation(exportable_path, relation, values[0]) - expect {subject.write_relation(exportable_path, relation, values[1])}.to raise_exception("The #{file_path} already exist") + expect { subject.write_relation(exportable_path, relation, values[1]) }.to raise_exception("The #{file_path} already exist") end end end diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index 3f73a730744..3088129a732 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do end let(:exportable_path) { 'project' } + let(:logger) { Gitlab::Export::Logger.build } let(:json_writer) { instance_double('Gitlab::ImportExport::Json::LegacyWriter') } let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys } let(:include) { [] } @@ -42,7 +43,7 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do end subject do - described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path) + described_class.new(exportable, relations_schema, json_writer, exportable_path: exportable_path, logger: logger) end describe '#execute' do @@ -73,6 +74,21 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do subject.execute end + it 'logs the relation name and the number of records to export' do + allow(json_writer).to receive(:write_relation_array) + allow(logger).to receive(:info) + + subject.execute + + expect(logger).to have_received(:info).with( + importer: 'Import/Export', + message: "Exporting issues relation. Number of records to export: 16", + project_id: exportable.id, + project_name: exportable.name, + project_path: exportable.full_path + ) + end + context 'default relation ordering' do it 'orders exported issues by primary key(:id)' do expected_issues = exportable.issues.reorder(:id).map(&:to_json) @@ -138,6 +154,21 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do subject.execute end + + it 'logs the relation name' do + allow(json_writer).to receive(:write_relation) + allow(logger).to receive(:info) + + subject.execute + + expect(logger).to have_received(:info).with( + importer: 'Import/Export', + message: 'Exporting group relation', + project_id: exportable.id, + project_name: exportable.name, + project_path: exportable.full_path + ) + end end context 'with array relation' do @@ -155,6 +186,21 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do subject.execute end + + it 'logs the relation name and the number of records to export' do + allow(json_writer).to receive(:write_relation_array) + allow(logger).to receive(:info) + + subject.execute + + expect(logger).to have_received(:info).with( + importer: 'Import/Export', + message: 'Exporting project_members relation. Number of records to export: 1', + project_id: exportable.id, + project_name: exportable.name, + project_path: exportable.full_path + ) + end end describe 'load balancing' do diff --git a/spec/lib/gitlab/import_export/log_util_spec.rb b/spec/lib/gitlab/import_export/log_util_spec.rb new file mode 100644 index 00000000000..2b1a4b7bb61 --- /dev/null +++ b/spec/lib/gitlab/import_export/log_util_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::LogUtil do + describe '.exportable_to_log_payload' do + subject { described_class.exportable_to_log_payload(exportable) } + + context 'when exportable is a group' do + let(:exportable) { build_stubbed(:group) } + + it 'returns hash with group keys' do + expect(subject).to be_a(Hash) + expect(subject.keys).to eq(%i[group_id group_name group_path]) + end + end + + context 'when exportable is a project' do + let(:exportable) { build_stubbed(:project) } + + it 'returns hash with project keys' do + expect(subject).to be_a(Hash) + expect(subject.keys).to eq(%i[project_id project_name project_path]) + end + end + + context 'when exportable is a new record' do + let(:exportable) { Project.new } + + it 'returns empty hash' do + expect(subject).to eq({}) + end + end + + context 'when exportable is an unexpected type' do + let(:exportable) { build_stubbed(:issue) } + + it 'returns empty hash' do + expect(subject).to eq({}) + end + end + end +end diff --git a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb new file mode 100644 index 00000000000..dec51b3afd1 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::RelationSaver do + include ImportExport::CommonUtil + + subject(:relation_saver) { described_class.new(project: project, shared: shared, relation: relation) } + + let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let_it_be(:project) { setup_project } + + let(:relation) { Projects::ImportExport::RelationExport::ROOT_RELATION } + let(:shared) do + shared = project.import_export_shared + allow(shared).to receive(:export_path).and_return(export_path) + shared + end + + after do + FileUtils.rm_rf(export_path) + end + + describe '#save' do + context 'when relation is the root node' do + let(:relation) { Projects::ImportExport::RelationExport::ROOT_RELATION } + + it 'serializes the root node as a json file in the export path' do + relation_saver.save # rubocop:disable Rails/SaveBang + + json = read_json(File.join(shared.export_path, 'project.json')) + expect(json).to include({ 'description' => 'Project description' }) + end + + it 'serializes only allowed attributes' do + relation_saver.save # rubocop:disable Rails/SaveBang + + json = read_json(File.join(shared.export_path, 'project.json')) + expect(json).to include({ 'description' => 'Project description' }) + expect(json.keys).not_to include('name') + end + + it 'successfuly serializes without errors' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(true) + expect(shared.errors).to be_empty + end + end + + context 'when relation is a child node' do + let(:relation) { 'labels' } + + it 'serializes the child node as a ndjson file in the export path inside the project folder' do + relation_saver.save # rubocop:disable Rails/SaveBang + + ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson")) + expect(ndjson.first).to include({ 'title' => 'Label 1' }) + expect(ndjson.second).to include({ 'title' => 'Label 2' }) + end + + it 'serializes only allowed attributes' do + relation_saver.save # rubocop:disable Rails/SaveBang + + ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson")) + expect(ndjson.first.keys).not_to include('description_html') + end + + it 'successfuly serializes without errors' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(true) + expect(shared.errors).to be_empty + end + end + + context 'when relation name is not supported' do + let(:relation) { 'unknown' } + + it 'returns false and register the error' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(false) + expect(shared.errors).to be_present + end + end + + context 'when an exception occurs during serialization' do + it 'returns false and register the exception error message' do + allow_next_instance_of(Gitlab::ImportExport::Json::StreamingSerializer) do |serializer| + allow(serializer).to receive(:serialize_root).and_raise('Error!') + end + + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(false) + expect(shared.errors).to include('Error!') + end + end + end + + def setup_project + project = create(:project, + description: 'Project description' + ) + + create(:label, project: project, title: 'Label 1') + create(:label, project: project, title: 'Label 2') + + project + end + + def read_json(path) + Gitlab::Json.parse(IO.read(path)) + end + + def read_ndjson(path) + relations = [] + File.foreach(path) do |line| + json = Gitlab::Json.parse(line) + relations << json + end + relations + end +end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index 157cd408da9..47d7555c8f4 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -254,6 +254,16 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end end + it 'has multiple merge request assignees' do + expect(MergeRequest.find_by(title: 'MR1').assignees).to contain_exactly(@user, *@existing_members) + expect(MergeRequest.find_by(title: 'MR2').assignees).to be_empty + end + + it 'has multiple merge request reviewers' do + expect(MergeRequest.find_by(title: 'MR1').reviewers).to contain_exactly(@user, *@existing_members) + expect(MergeRequest.find_by(title: 'MR2').reviewers).to be_empty + end + it 'has labels associated to label links, associated to issues' do expect(Label.first.label_links.first.target).not_to be_nil end @@ -262,6 +272,11 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do expect(ProjectLabel.count).to eq(3) end + it 'has merge request approvals' do + expect(MergeRequest.find_by(title: 'MR1').approvals.pluck(:user_id)).to contain_exactly(@user.id, *@existing_members.map(&:id)) + expect(MergeRequest.find_by(title: 'MR2').approvals).to be_empty + end + it 'has no group labels' do expect(GroupLabel.count).to eq(0) end @@ -589,7 +604,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do it 'issue system note metadata restored successfully' do note_content = 'created merge request !1 to address this issue' - note = project.issues.first.notes.find { |n| n.note.match(/#{note_content}/)} + note = project.issues.first.notes.find { |n| n.note.match(/#{note_content}/) } expect(note.noteable_type).to eq('Issue') expect(note.system).to eq(true) @@ -1085,35 +1100,13 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end end - context 'when import_relation_object_persistence feature flag is enabled' do - before do - stub_feature_flags(import_relation_object_persistence: true) - end - - context 'enable ndjson import' do - it_behaves_like 'project tree restorer work properly', :legacy_reader, true + context 'enable ndjson import' do + it_behaves_like 'project tree restorer work properly', :legacy_reader, true - it_behaves_like 'project tree restorer work properly', :ndjson_reader, true - end - - context 'disable ndjson import' do - it_behaves_like 'project tree restorer work properly', :legacy_reader, false - end + it_behaves_like 'project tree restorer work properly', :ndjson_reader, true end - context 'when import_relation_object_persistence feature flag is disabled' do - before do - stub_feature_flags(import_relation_object_persistence: false) - end - - context 'enable ndjson import' do - it_behaves_like 'project tree restorer work properly', :legacy_reader, true - - it_behaves_like 'project tree restorer work properly', :ndjson_reader, true - end - - context 'disable ndjson import' do - it_behaves_like 'project tree restorer work properly', :legacy_reader, false - end + context 'disable ndjson import' do + it_behaves_like 'project tree restorer work properly', :legacy_reader, false end end diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index ba781ae78b7..15108d28bf2 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -68,6 +68,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do it 'has merge request\'s milestones' do expect(subject.first['milestone']).not_to be_empty end + it 'has merge request\'s source branch SHA' do expect(subject.first['source_branch_sha']).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') end @@ -100,9 +101,30 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do expect(subject.first['notes'].first['author']).not_to be_empty end + it 'has merge request approvals' do + approval = subject.first['approvals'].first + + expect(approval).not_to be_nil + expect(approval['user_id']).to eq(user.id) + end + it 'has merge request resource label events' do expect(subject.first['resource_label_events']).not_to be_empty end + + it 'has merge request assignees' do + reviewer = subject.first['merge_request_assignees'].first + + expect(reviewer).not_to be_nil + expect(reviewer['user_id']).to eq(user.id) + end + + it 'has merge request reviewers' do + reviewer = subject.first['merge_request_reviewers'].first + + expect(reviewer).not_to be_nil + expect(reviewer['user_id']).to eq(user.id) + end end context 'with snippets' do @@ -404,7 +426,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do context 'when streaming has to retry', :aggregate_failures do let(:shared) { double('shared', export_path: exportable_path) } - let(:logger) { Gitlab::Import::Logger.build } + let(:logger) { Gitlab::Export::Logger.build } let(:serializer) { double('serializer') } let(:error_class) { Net::OpenTimeout } let(:info_params) do @@ -468,7 +490,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do create(:label_link, label: group_label, target: issue) create(:label_priority, label: group_label, priority: 1) milestone = create(:milestone, project: project) - merge_request = create(:merge_request, source_project: project, milestone: milestone) + merge_request = create(:merge_request, source_project: project, milestone: milestone, assignees: [user], reviewers: [user]) + create(:approval, merge_request: merge_request, user: user) ci_build = create(:ci_build, project: project, when: nil) ci_build.pipeline.update!(project: project) diff --git a/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb b/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb new file mode 100644 index 00000000000..b1bc6b7eeaf --- /dev/null +++ b/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do + include StubRequests + + subject do + described_class.new( + download_url: download_url, + upload_url: upload_url, + options: { + upload_method: upload_method, + upload_content_type: upload_content_type + } + ) + end + + let(:download_url) { 'http://object-storage/file.txt' } + let(:upload_url) { 'http://example.com/file.txt' } + let(:upload_method) { :post } + let(:upload_content_type) { 'text/plain' } + + describe '#execute' do + context 'when download request and upload request return 200' do + it 'uploads the downloaded content' do + stub_request(:get, download_url).to_return(status: 200, body: 'ABC', headers: { 'Content-Length' => 3 }) + stub_request(:post, upload_url) + + subject.execute + + expect( + a_request(:post, upload_url).with( + body: 'ABC', headers: { 'Content-Length' => 3, 'Content-Type' => 'text/plain' } + ) + ).to have_been_made + end + end + + context 'when upload method is put' do + let(:upload_method) { :put } + + it 'uploads using the put method' do + stub_request(:get, download_url).to_return(status: 200, body: 'ABC', headers: { 'Content-Length' => 3 }) + stub_request(:put, upload_url) + + subject.execute + + expect( + a_request(:put, upload_url).with( + body: 'ABC', headers: { 'Content-Length' => 3, 'Content-Type' => 'text/plain' } + ) + ).to have_been_made + end + end + + context 'when download request does not return 200' do + it do + stub_request(:get, download_url).to_return(status: 404) + + expect { subject.execute }.to raise_error( + Gitlab::ImportExport::RemoteStreamUpload::StreamError, + "Invalid response code while downloading file. Code: 404" + ) + end + end + + context 'when upload request does not returns 200' do + it do + stub_request(:get, download_url).to_return(status: 200, body: 'ABC', headers: { 'Content-Length' => 3 }) + stub_request(:post, upload_url).to_return(status: 403) + + expect { subject.execute }.to raise_error( + Gitlab::ImportExport::RemoteStreamUpload::StreamError, + "Invalid response code while uploading file. Code: 403" + ) + end + end + + context 'when download URL is a local address' do + let(:download_url) { 'http://127.0.0.1/file.txt' } + + before do + stub_request(:get, download_url) + stub_request(:post, upload_url) + end + + it 'raises error' do + expect { subject.execute }.to raise_error( + Gitlab::HTTP::BlockedUrlError, + "URL 'http://127.0.0.1/file.txt' is blocked: Requests to localhost are not allowed" + ) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'raises does not error' do + expect { subject.execute }.not_to raise_error + end + end + end + + context 'when download URL is a local network' do + let(:download_url) { 'http://172.16.0.0/file.txt' } + + before do + stub_request(:get, download_url) + stub_request(:post, upload_url) + end + + it 'raises error' do + expect { subject.execute }.to raise_error( + Gitlab::HTTP::BlockedUrlError, + "URL 'http://172.16.0.0/file.txt' is blocked: Requests to the local network are not allowed" + ) + end + + context 'when local network requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'raises does not error' do + expect { subject.execute }.not_to raise_error + end + end + end + + context 'when upload URL is a local address' do + let(:upload_url) { 'http://127.0.0.1/file.txt' } + + before do + stub_request(:get, download_url) + stub_request(:post, upload_url) + end + + it 'raises error' do + stub_request(:get, download_url) + + expect { subject.execute }.to raise_error( + Gitlab::HTTP::BlockedUrlError, + "URL 'http://127.0.0.1/file.txt' is blocked: Requests to localhost are not allowed" + ) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'raises does not error' do + expect { subject.execute }.not_to raise_error + end + end + end + + context 'when upload URL it is a request to local network' do + let(:upload_url) { 'http://172.16.0.0/file.txt' } + + before do + stub_request(:get, download_url) + stub_request(:post, upload_url) + end + + it 'raises error' do + expect { subject.execute }.to raise_error( + Gitlab::HTTP::BlockedUrlError, + "URL 'http://172.16.0.0/file.txt' is blocked: Requests to the local network are not allowed" + ) + end + + context 'when local network requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'raises does not error' do + expect { subject.execute }.not_to raise_error + end + end + end + + context 'when upload URL resolves to a local address' do + let(:upload_url) { 'http://example.com/file.txt' } + + it 'raises error' do + stub_request(:get, download_url) + stub_full_request(upload_url, ip_address: '127.0.0.1', method: upload_method) + + expect { subject.execute }.to raise_error( + Gitlab::HTTP::BlockedUrlError, + "URL 'http://example.com/file.txt' is blocked: Requests to localhost are not allowed" + ) + end + end + end + + describe Gitlab::ImportExport::RemoteStreamUpload::ChunkStream do + describe 'StringIO#copy_stream compatibility' do + it 'copies all chunks' do + chunks = %w[ABC EFD].to_enum + chunk_stream = described_class.new(chunks) + new_stream = StringIO.new + + IO.copy_stream(chunk_stream, new_stream) + new_stream.rewind + + expect(new_stream.read).to eq('ABCEFD') + end + + context 'with chunks smaller and bigger than buffer size' do + before do + stub_const('Gitlab::ImportExport::RemoteStreamUpload::ChunkStream::DEFAULT_BUFFER_SIZE', 4) + end + + it 'copies all chunks' do + chunks = %w[A BC DEF GHIJ KLMNOPQ RSTUVWXYZ].to_enum + chunk_stream = described_class.new(chunks) + new_stream = StringIO.new + + IO.copy_stream(chunk_stream, new_stream) + new_stream.rewind + + expect(new_stream.read).to eq('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index bd60bb53d49..6cfc24a8996 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -521,7 +521,6 @@ Project: - star_count - ci_id - shared_runners_enabled -- build_coverage_regex - build_allow_git_fetchs - build_timeout - pending_delete @@ -584,6 +583,9 @@ ProjectFeature: - security_and_compliance_access_level - container_registry_access_level - package_registry_access_level +- environments_access_level +- feature_flags_access_level +- releases_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: @@ -741,6 +743,14 @@ MergeRequestAssignee: - id - user_id - merge_request_id +- created_at +- state +MergeRequestReviewer: +- id +- user_id +- merge_request_id +- created_at +- state ProjectMetricsSetting: - project_id - external_dashboard_url @@ -903,3 +913,7 @@ MergeRequest::CleanupSchedule: - completed_at - created_at - updated_at +Approval: + - user_id + - created_at + - updated_at diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index 1945156ca59..408ed3a2176 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -68,12 +68,18 @@ RSpec.describe Gitlab::ImportExport::Shared do expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) end - it 'updates the import JID' do + it 'tracks exception' do import_state = create(:import_state, project: project, jid: 'jid-test') expect(Gitlab::ErrorTracking) .to receive(:track_exception) - .with(error, hash_including(import_jid: import_state.jid)) + .with(error, hash_including( + importer: 'Import/Export', + project_id: project.id, + project_name: project.name, + project_path: project.full_path, + import_jid: import_state.jid + )) subject.error(error) end diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index 9e69e04b17c..14c62edb786 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Gitlab::ImportExport::VersionChecker do end context 'newer version' do - let(:version) { '900.0'} + let(:version) { '900.0' } it 'returns false if export version is newer' do expect(described_class.check!(shared: shared)).to be false diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 79d626386d4..4fa9079144d 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -195,6 +195,28 @@ RSpec.describe Gitlab::InstrumentationHelper do expect(payload[:uploaded_file_size_bytes]).to eq(uploaded_file.size) end end + + context 'when an api call to the search api is made' do + before do + Gitlab::Instrumentation::GlobalSearchApi.set_information( + type: 'basic', + level: 'global', + scope: 'issues', + search_duration_s: 0.1 + ) + end + + it 'adds search data' do + subject + + expect(payload).to include({ + 'meta.search.type' => 'basic', + 'meta.search.level' => 'global', + 'meta.search.scope' => 'issues', + global_search_duration_s: 0.1 + }) + end + end end describe 'duration calculations' do diff --git a/spec/lib/gitlab/jira/dvcs_spec.rb b/spec/lib/gitlab/jira/dvcs_spec.rb index 09e777b38ea..76d81343875 100644 --- a/spec/lib/gitlab/jira/dvcs_spec.rb +++ b/spec/lib/gitlab/jira/dvcs_spec.rb @@ -24,8 +24,8 @@ RSpec.describe Gitlab::Jira::Dvcs do end describe '.encode_project_name' do - let(:group) { create(:group)} - let(:project) { create(:project, group: group)} + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } context 'root group' do it 'returns project path' do @@ -34,7 +34,7 @@ RSpec.describe Gitlab::Jira::Dvcs do end context 'nested group' do - let(:group) { create(:group, :nested)} + let(:group) { create(:group, :nested) } it 'returns encoded project full path' do expect(described_class.encode_project_name(project)).to eq(described_class.encode_slash(project.full_path)) diff --git a/spec/lib/gitlab/jira_import/issues_importer_spec.rb b/spec/lib/gitlab/jira_import/issues_importer_spec.rb index 1bc052ee0b6..a2a482dde7c 100644 --- a/spec/lib/gitlab/jira_import/issues_importer_spec.rb +++ b/spec/lib/gitlab/jira_import/issues_importer_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do context 'with results returned' do jira_issue = Struct.new(:id) - let_it_be(:jira_issues) { [jira_issue.new(1), jira_issue.new(2)] } + let_it_be(:jira_issues) { [jira_issue.new(1), jira_issue.new(2), jira_issue.new(3)] } def mock_issue_serializer(count, raise_exception_on_even_mocks: false) serializer = instance_double(Gitlab::JiraImport::IssueSerializer, execute: { key: 'data' }) @@ -125,6 +125,47 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do expect(Gitlab::JiraImport.get_issues_next_start_at(project.id)).to eq(2) end end + + context 'when number of issues is above the threshold' do + before do + stub_const("#{described_class.name}::JIRA_IMPORT_THRESHOLD", 2) + stub_const("#{described_class.name}::JIRA_IMPORT_PAUSE_LIMIT", 1) + allow(Gitlab::ErrorTracking).to receive(:track_exception) + allow_next_instance_of(Gitlab::JobWaiter) do |job_waiter| + allow(job_waiter).to receive(:wait).with(5).and_return(job_waiter.wait(0.1)) + end + end + + it 'schedules 2 import jobs with two pause points' do + expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issues[0], jira_issues[1], jira_issues[2]]) + expect(Gitlab::JiraImport::ImportIssueWorker).to receive(:perform_async).exactly(3).times + expect(Gitlab::JiraImport::ImportIssueWorker) + .to receive(:queue_size) + .exactly(6).times + .and_return(1, 2, 3, 2, 1, 0) + + mock_issue_serializer(3) + + expect(subject.execute).to have_received(:wait).with(5).twice + end + + it 'tracks the exception if the queue size does not reduce' do + expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issues[0]]) + expect(Gitlab::JiraImport::ImportIssueWorker).not_to receive(:perform_async) + expect(Gitlab::JiraImport::ImportIssueWorker) + .to receive(:queue_size) + .exactly(11).times + .and_return(3) + + mock_issue_serializer(1) + + expect(subject.execute).to have_received(:wait).with(5).exactly(10).times + expect(Gitlab::ErrorTracking) + .to have_received(:track_exception) + .with(described_class::RetriesExceededError, { project_id: project.id }) + .once + end + end end end end diff --git a/spec/lib/gitlab/kubernetes/rollout_status_spec.rb b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb index 8ed9fdd799c..21d345f0739 100644 --- a/spec/lib/gitlab/kubernetes/rollout_status_spec.rb +++ b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb @@ -213,7 +213,7 @@ RSpec.describe Gitlab::Kubernetes::RolloutStatus do let(:specs) { specs_half_finished } - it { is_expected.to be_falsy} + it { is_expected.to be_falsy } end end diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb index 06a25be757e..0c2c9b89005 100644 --- a/spec/lib/gitlab/mail_room/mail_room_spec.rb +++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb @@ -246,7 +246,7 @@ RSpec.describe Gitlab::MailRoom do redis_url: "localhost", redis_db: 99, namespace: "resque:gitlab", - queue: "email_receiver", + queue: "default", worker: "EmailReceiverWorker", sentinels: [{ host: "localhost", port: 1234 }] } @@ -259,7 +259,7 @@ RSpec.describe Gitlab::MailRoom do redis_url: "localhost", redis_db: 99, namespace: "resque:gitlab", - queue: "service_desk_email_receiver", + queue: "default", worker: "ServiceDeskEmailReceiverWorker", sentinels: [{ host: "localhost", port: 1234 }] } diff --git a/spec/lib/gitlab/memory/jemalloc_spec.rb b/spec/lib/gitlab/memory/jemalloc_spec.rb index 8847516b52c..482ac6e5802 100644 --- a/spec/lib/gitlab/memory/jemalloc_spec.rb +++ b/spec/lib/gitlab/memory/jemalloc_spec.rb @@ -28,11 +28,12 @@ RSpec.describe Gitlab::Memory::Jemalloc do describe '.dump_stats' do it 'writes stats JSON file' do - described_class.dump_stats(path: outdir, format: format) + file_path = described_class.dump_stats(path: outdir, format: format) file = Dir.entries(outdir).find { |e| e.match(/jemalloc_stats\.#{$$}\.\d+\.json$/) } expect(file).not_to be_nil - expect(File.read(File.join(outdir, file))).to eq(output) + expect(file_path).to eq(File.join(outdir, file)) + expect(File.read(file_path)).to eq(output) end end end @@ -52,12 +53,22 @@ RSpec.describe Gitlab::Memory::Jemalloc do end describe '.dump_stats' do - it 'writes stats text file' do - described_class.dump_stats(path: outdir, format: format) + shared_examples 'writes stats text file' do |filename_label, filename_pattern| + it do + described_class.dump_stats(path: outdir, format: format, filename_label: filename_label) + + file = Dir.entries(outdir).find { |e| e.match(filename_pattern) } + expect(file).not_to be_nil + expect(File.read(File.join(outdir, file))).to eq(output) + end + end - file = Dir.entries(outdir).find { |e| e.match(/jemalloc_stats\.#{$$}\.\d+\.txt$/) } - expect(file).not_to be_nil - expect(File.read(File.join(outdir, file))).to eq(output) + context 'when custom filename label is passed' do + include_examples 'writes stats text file', 'puma_0', /jemalloc_stats\.#{$$}\.puma_0\.\d+\.txt$/ + end + + context 'when custom filename label is not passed' do + include_examples 'writes stats text file', nil, /jemalloc_stats\.#{$$}\.\d+\.txt$/ end end end diff --git a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb new file mode 100644 index 00000000000..53fae48776b --- /dev/null +++ b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Memory::Reports::JemallocStats do + let(:reports_dir) { '/empty-dir' } + let(:jemalloc_stats) { described_class.new(reports_path: reports_dir) } + + describe '.run' do + context 'when :report_jemalloc_stats ops FF is enabled' do + let(:worker_id) { 'puma_1' } + let(:report_name) { 'report.json' } + let(:report_path) { File.join(reports_dir, report_name) } + + before do + allow(Prometheus::PidProvider).to receive(:worker_id).and_return(worker_id) + end + + it 'invokes Jemalloc.dump_stats and returns file path' do + expect(Gitlab::Memory::Jemalloc) + .to receive(:dump_stats).with(path: reports_dir, filename_label: worker_id).and_return(report_path) + + expect(jemalloc_stats.run).to eq(report_path) + end + + describe 'reports cleanup' do + let_it_be(:outdir) { Dir.mktmpdir } + + let(:jemalloc_stats) { described_class.new(reports_path: outdir) } + + before do + stub_env('GITLAB_DIAGNOSTIC_REPORTS_JEMALLOC_MAX_REPORTS_STORED', 3) + allow(Gitlab::Memory::Jemalloc).to receive(:dump_stats) + end + + after do + FileUtils.rm_f(outdir) + end + + context 'when number of reports exceeds `max_reports_stored`' do + let_it_be(:reports) do + now = Time.current + + (1..5).map do |i| + Tempfile.new("jemalloc_stats.#{i}.worker_#{i}.#{Time.current.to_i}.json", outdir).tap do |f| + FileUtils.touch(f, mtime: (now + i.second).to_i) + end + end + end + + after do + reports.each do |f| + f.close + f.unlink + rescue Errno::ENOENT + # Some of the files are already unlinked by the code we test; Ignore + end + end + + it 'keeps only `max_reports_stored` total newest files' do + expect { jemalloc_stats.run } + .to change { Dir.entries(outdir).count { |e| e.match(/jemalloc_stats.*/) } } + .from(5).to(3) + + # Keeps only the newest reports + expect(reports.last(3).all? { |r| File.exist?(r) }).to be true + end + end + + context 'when number of reports does not exceed `max_reports_stored`' do + let_it_be(:reports) do + now = Time.current + + (1..3).map do |i| + Tempfile.new("jemalloc_stats.#{i}.worker_#{i}.#{Time.current.to_i}.json", outdir).tap do |f| + FileUtils.touch(f, mtime: (now + i.second).to_i) + end + end + end + + after do + reports.each do |f| + f.close + f.unlink + end + end + + it 'does not remove any reports' do + expect { jemalloc_stats.run } + .not_to change { Dir.entries(outdir).count { |e| e.match(/jemalloc_stats.*/) } } + end + end + end + end + + context 'when :report_jemalloc_stats ops FF is disabled' do + before do + stub_feature_flags(report_jemalloc_stats: false) + end + + it 'does not run the report and returns nil' do + expect(Gitlab::Memory::Jemalloc).not_to receive(:dump_stats) + + expect(jemalloc_stats.run).to be_nil + end + end + end + + describe '.active?' do + subject(:active) { jemalloc_stats.active? } + + context 'when :report_jemalloc_stats ops FF is enabled' do + it { is_expected.to be true } + end + + context 'when :report_jemalloc_stats ops FF is disabled' do + before do + stub_feature_flags(report_jemalloc_stats: false) + end + + it { is_expected.to be false } + end + end +end diff --git a/spec/lib/gitlab/memory/reports_daemon_spec.rb b/spec/lib/gitlab/memory/reports_daemon_spec.rb new file mode 100644 index 00000000000..c9562470971 --- /dev/null +++ b/spec/lib/gitlab/memory/reports_daemon_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Memory::ReportsDaemon do + let(:daemon) { described_class.new } + + describe '#run_thread' do + let(:report_duration_counter) { instance_double(::Prometheus::Client::Counter) } + let(:file_size) { 1_000_000 } + + before do + allow(Gitlab::Metrics).to receive(:counter).and_return(report_duration_counter) + allow(report_duration_counter).to receive(:increment) + + # make sleep no-op + allow(daemon).to receive(:sleep) {} + + # let alive return 3 times: true, true, false + allow(daemon).to receive(:alive).and_return(true, true, false) + + allow(File).to receive(:size).with(/#{daemon.reports_path}.*\.json/).and_return(file_size) + end + + it 'runs reports' do + expect(daemon.send(:reports)).to all(receive(:run).twice.and_call_original) + + daemon.send(:run_thread) + end + + it 'logs report execution' do + expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1') + + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + :duration_s, + :cpu_s, + perf_report_size_bytes: file_size, + message: 'finished', + pid: Process.pid, + worker_id: 'worker_1', + perf_report: 'jemalloc_stats' + )).twice + + daemon.send(:run_thread) + end + + context 'when the report object returns invalid file path' do + before do + allow(File).to receive(:size).with(/#{daemon.reports_path}.*\.json/).and_raise(Errno::ENOENT) + end + + it 'logs `0` as `perf_report_size_bytes`' do + expect(Gitlab::AppLogger).to receive(:info).with(hash_including(perf_report_size_bytes: 0)).twice + + daemon.send(:run_thread) + end + end + + it 'sets real time duration gauge' do + expect(report_duration_counter).to receive(:increment).with({ report: 'jemalloc_stats' }, an_instance_of(Float)) + + daemon.send(:run_thread) + end + + it 'allows configure and run multiple reports' do + # rubocop: disable RSpec/VerifiedDoubles + # We test how ReportsDaemon could be extended in the future + # We configure it with new reports classes which are not yet defined so we cannot make this an instance_double. + active_report_1 = double("Active Report 1", active?: true) + active_report_2 = double("Active Report 2", active?: true) + inactive_report = double("Inactive Report", active?: false) + # rubocop: enable RSpec/VerifiedDoubles + + allow(daemon).to receive(:reports).and_return([active_report_1, inactive_report, active_report_2]) + + expect(active_report_1).to receive(:run).and_return('/tmp/report_1.json').twice + expect(active_report_2).to receive(:run).and_return('/tmp/report_2.json').twice + expect(inactive_report).not_to receive(:run) + + daemon.send(:run_thread) + end + + context 'sleep timers logic' do + it 'wakes up every (fixed interval + defined delta), sleeps between reports each cycle' do + stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_MAX_DELTA_S', 1) # rand(1) == 0, so we will have fixed sleep interval + daemon = described_class.new + allow(daemon).to receive(:alive).and_return(true, true, false) + + expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_S).ordered + expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_BETWEEN_REPORTS_S).ordered + expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_S).ordered + expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_BETWEEN_REPORTS_S).ordered + + daemon.send(:run_thread) + end + end + end + + describe '#stop_working' do + it 'changes :alive to false' do + expect { daemon.send(:stop_working) }.to change { daemon.send(:alive) }.from(true).to(false) + end + end + + context 'timer intervals settings' do + context 'when no settings are set in the environment' do + it 'uses defaults' do + daemon = described_class.new + + expect(daemon.sleep_s).to eq(described_class::DEFAULT_SLEEP_S) + expect(daemon.sleep_max_delta_s).to eq(described_class::DEFAULT_SLEEP_MAX_DELTA_S) + expect(daemon.sleep_between_reports_s).to eq(described_class::DEFAULT_SLEEP_BETWEEN_REPORTS_S) + expect(daemon.reports_path).to eq(described_class::DEFAULT_REPORTS_PATH) + end + end + + context 'when settings are passed through the environment' do + before do + stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_S', 100) + stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_MAX_DELTA_S', 50) + stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_BETWEEN_REPORTS_S', 2) + stub_env('GITLAB_DIAGNOSTIC_REPORTS_PATH', '/empty-dir') + end + + it 'uses provided values' do + daemon = described_class.new + + expect(daemon.sleep_s).to eq(100) + expect(daemon.sleep_max_delta_s).to eq(50) + expect(daemon.sleep_between_reports_s).to eq(2) + expect(daemon.reports_path).to eq('/empty-dir') + end + end + end +end diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb index 8b82078bcb9..010f6884df3 100644 --- a/spec/lib/gitlab/memory/watchdog_spec.rb +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -14,32 +14,57 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do let(:sleep_time) { 0.1 } let(:max_heap_fragmentation) { 0.2 } + # Tests should set this to control the number of loop iterations in `call`. + let(:watchdog_iterations) { 1 } + subject(:watchdog) do described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time, - max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation) + max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation).tap do |instance| + # We need to defuse `sleep` and stop the internal loop after N iterations. + iterations = 0 + expect(instance).to receive(:sleep) do + instance.stop if (iterations += 1) >= watchdog_iterations + end.at_most(watchdog_iterations) + end + end + + def stub_prometheus_metrics + allow(Gitlab::Metrics).to receive(:gauge) + .with(:gitlab_memwd_heap_frag_limit, anything) + .and_return(heap_frag_limit_gauge) + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_total, anything, anything) + .and_return(heap_frag_violations_counter) + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything) + .and_return(heap_frag_violations_handled_counter) + + allow(heap_frag_limit_gauge).to receive(:set) + allow(heap_frag_violations_counter).to receive(:increment) + allow(heap_frag_violations_handled_counter).to receive(:increment) end before do + stub_prometheus_metrics + allow(handler).to receive(:on_high_heap_fragmentation).and_return(true) allow(logger).to receive(:warn) allow(logger).to receive(:info) allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation) - end - after do - watchdog.stop + allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1') end - context 'when starting up' do + context 'when created' do let(:fragmentation) { 0 } let(:max_strikes) { 0 } it 'sets the heap fragmentation limit gauge' do - allow(Gitlab::Metrics).to receive(:gauge).and_return(heap_frag_limit_gauge) - expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation) + + watchdog end context 'when no settings are set in the environment' do @@ -76,77 +101,54 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do it 'does not signal the handler' do expect(handler).not_to receive(:on_high_heap_fragmentation) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end end context 'when process exceeds heap fragmentation threshold permanently' do let(:fragmentation) { max_heap_fragmentation + 0.1 } - - before do - allow(Gitlab::Metrics).to receive(:counter) - .with(:gitlab_memwd_heap_frag_violations_total, anything, anything) - .and_return(heap_frag_violations_counter) - allow(Gitlab::Metrics).to receive(:counter) - .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything) - .and_return(heap_frag_violations_handled_counter) - allow(heap_frag_violations_counter).to receive(:increment) - allow(heap_frag_violations_handled_counter).to receive(:increment) - end + let(:max_strikes) { 3 } context 'when process has not exceeded allowed number of strikes' do - let(:max_strikes) { 10 } + let(:watchdog_iterations) { max_strikes } it 'does not signal the handler' do expect(handler).not_to receive(:on_high_heap_fragmentation) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end it 'does not log any events' do expect(logger).not_to receive(:warn) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end it 'increments the violations counter' do - expect(heap_frag_violations_counter).to receive(:increment) - - watchdog.start + expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) - sleep sleep_time * 3 + watchdog.call end it 'does not increment violations handled counter' do expect(heap_frag_violations_handled_counter).not_to receive(:increment) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end end context 'when process exceeds the allowed number of strikes' do - let(:max_strikes) { 1 } + let(:watchdog_iterations) { max_strikes + 1 } it 'signals the handler and resets strike counter' do expect(handler).to receive(:on_high_heap_fragmentation).and_return(true) - watchdog.start - - sleep sleep_time * 3 + watchdog.call expect(watchdog.strikes).to eq(0) end it 'logs the event' do - expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1') expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) expect(logger).to receive(:warn).with({ message: 'heap fragmentation limit exceeded', @@ -161,18 +163,14 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do memwd_rss_bytes: 1024 }) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end it 'increments both the violations and violations handled counters' do - expect(heap_frag_violations_counter).to receive(:increment) + expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) expect(heap_frag_violations_handled_counter).to receive(:increment) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end context 'when enforce_memory_watchdog ops toggle is off' do @@ -186,35 +184,31 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true) ) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end end - end - - context 'when handler result is true' do - let(:max_strikes) { 1 } - it 'considers the event handled and stops itself' do - expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true) + context 'when handler result is true' do + it 'considers the event handled and stops itself' do + expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true) + expect(logger).to receive(:info).with(hash_including(message: 'stopped')) - watchdog.start - - sleep sleep_time * 3 + watchdog.call + end end - end - - context 'when handler result is false' do - let(:max_strikes) { 1 } - it 'keeps running' do - # Return true the third time to terminate the daemon. - expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true) + context 'when handler result is false' do + let(:max_strikes) { 0 } # to make sure the handler fires each iteration + let(:watchdog_iterations) { 3 } - watchdog.start + it 'keeps running' do + expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations) + expect(heap_frag_violations_handled_counter).to receive(:increment).exactly(watchdog_iterations) + # Return true the third time to terminate the daemon. + expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true) - sleep sleep_time * 4 + watchdog.call + end end end end @@ -222,6 +216,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do context 'when process exceeds heap fragmentation threshold temporarily' do let(:fragmentation) { max_heap_fragmentation } let(:max_strikes) { 1 } + let(:watchdog_iterations) { 4 } before do allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return( @@ -235,9 +230,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do it 'does not signal the handler' do expect(handler).not_to receive(:on_high_heap_fragmentation) - watchdog.start - - sleep sleep_time * 4 + watchdog.call end end @@ -252,9 +245,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do it 'does not monitor heap fragmentation' do expect(Gitlab::Metrics::Memory).not_to receive(:gc_heap_fragmentation) - watchdog.start - - sleep sleep_time * 3 + watchdog.call end end end diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb index 83bee84df99..2e48070cb4f 100644 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do end it 'removes the transaction from the current thread upon completion' do - transaction.run { } + transaction.run {} expect(Thread.current[described_class::THREAD_KEY]).to be_nil end diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 06ce58a9e84..d6590efcf4f 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do end it 'removes the transaction from the current thread upon completion' do - transaction.run { } + transaction.run {} expect(Thread.current[described_class::THREAD_KEY]).to be_nil expect(described_class.current).to be_nil diff --git a/spec/lib/gitlab/middleware/compressed_json_spec.rb b/spec/lib/gitlab/middleware/compressed_json_spec.rb index a07cd49c572..6d49ab58d5d 100644 --- a/spec/lib/gitlab/middleware/compressed_json_spec.rb +++ b/spec/lib/gitlab/middleware/compressed_json_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::Middleware::CompressedJson do describe '#call' do context 'with collector route' do - let(:path) { '/api/v4/error_tracking/collector/1/store'} + let(:path) { '/api/v4/error_tracking/collector/1/store' } it_behaves_like 'decompress middleware' @@ -45,7 +45,7 @@ RSpec.describe Gitlab::Middleware::CompressedJson do end context 'with collector route under relative url' do - let(:path) { '/gitlab/api/v4/error_tracking/collector/1/store'} + let(:path) { '/gitlab/api/v4/error_tracking/collector/1/store' } before do stub_config_setting(relative_url_root: '/gitlab') @@ -71,7 +71,7 @@ RSpec.describe Gitlab::Middleware::CompressedJson do let(:body_limit) { Gitlab::Middleware::CompressedJson::MAXIMUM_BODY_SIZE } let(:decompressed_input) { 'a' * (body_limit + 100) } let(:input) { ActiveSupport::Gzip.compress(decompressed_input) } - let(:path) { '/api/v4/error_tracking/collector/1/envelope'} + let(:path) { '/api/v4/error_tracking/collector/1/envelope' } it 'reads only limited size' do expect(middleware.call(env)) diff --git a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb index e6815a46a56..91c030a0f45 100644 --- a/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb +++ b/spec/lib/gitlab/middleware/sidekiq_web_static_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Middleware::SidekiqWebStatic do end context 'with an /admin/sidekiq route' do - let(:path) { '/admin/sidekiq/javascripts/application.js'} + let(:path) { '/admin/sidekiq/javascripts/application.js' } it 'deletes the HTTP_X_SENDFILE_TYPE header' do expect(app).to receive(:call) diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb index bc4d95738c7..92e424978ff 100644 --- a/spec/lib/gitlab/octokit/middleware_spec.rb +++ b/spec/lib/gitlab/octokit/middleware_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Octokit::Middleware do it_behaves_like 'Public URL' end - context 'when the URL is a localhost adresss' do + context 'when the URL is a localhost address' do let(:env) { { url: 'http://127.0.0.1' } } context 'when localhost requests are not allowed' do diff --git a/spec/lib/gitlab/otp_key_rotator_spec.rb b/spec/lib/gitlab/otp_key_rotator_spec.rb index e328b190db4..e3b9f006b19 100644 --- a/spec/lib/gitlab/otp_key_rotator_spec.rb +++ b/spec/lib/gitlab/otp_key_rotator_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Gitlab::OtpKeyRotator do it 'stores the calculated values in a spreadsheet' do rotation - expect(data).to match_array(users.map {|u| build_row(u) }) + expect(data).to match_array(users.map { |u| build_row(u) }) end context 'new key is too short' do diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb index dcb8138bdde..0bafd436bd0 100644 --- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb +++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb @@ -126,5 +126,19 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do end end end + + context 'with "none" pagination option' do + let(:expected_result) { double(:result) } + let(:query) { { pagination: 'none' } } + + it 'uses offset pagination' do + expect(finder).to receive(:execute).with(gitaly_pagination: false).and_return(expected_result) + expect(Kaminari).not_to receive(:paginate_array) + expect(Gitlab::Pagination::OffsetPagination).not_to receive(:new) + + actual_result = pager.paginate(finder) + expect(actual_result).to eq(expected_result) + end + end end end diff --git a/spec/lib/gitlab/pagination/keyset_spec.rb b/spec/lib/gitlab/pagination/keyset_spec.rb index 81dc40b35d5..8885e684d8a 100644 --- a/spec/lib/gitlab/pagination/keyset_spec.rb +++ b/spec/lib/gitlab/pagination/keyset_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::Pagination::Keyset do describe '.available?' do subject { described_class } - let(:request_context) { double("request context", page: page)} + let(:request_context) { double("request context", page: page) } let(:page) { double("page", order_by: order_by) } shared_examples_for 'keyset pagination is available' do diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb index c368b349a3c..a444e7fdf47 100644 --- a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb +++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe Gitlab::PhabricatorImport::Conduit::Response do - let(:response) { described_class.new(Gitlab::Json.parse(fixture_file('phabricator_responses/maniphest.search.json')))} + let(:response) { described_class.new(Gitlab::Json.parse(fixture_file('phabricator_responses/maniphest.search.json'))) } let(:error_response) { described_class.new(Gitlab::Json.parse(fixture_file('phabricator_responses/auth_failed.json'))) } describe '.parse!' do diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 89ddde4a01d..9083c5625d4 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -104,7 +104,7 @@ RSpec.describe Gitlab::PrometheusClient do end describe 'failure to reach a provided prometheus url' do - let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"} + let(:prometheus_url) { "https://prometheus.invalid.example.com/api/v1/query?query=1" } shared_examples 'exceptions are raised' do Gitlab::HTTP::HTTP_ERRORS.each do |error| diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb index c040a70e403..e2f289041ce 100644 --- a/spec/lib/gitlab/quick_actions/extractor_spec.rb +++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb @@ -7,10 +7,10 @@ RSpec.describe Gitlab::QuickActions::Extractor do Class.new do include Gitlab::QuickActions::Dsl - command(:reopen, :open) { } - command(:assign) { } - command(:labels) { } - command(:power) { } + command(:reopen, :open) {} + command(:assign) {} + command(:labels) {} + command(:power) {} command(:noop_command) substitution(:substitution) { 'foo' } substitution :shrug do |comment| diff --git a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb index bd167ee2e3e..8151519ddec 100644 --- a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb +++ b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::RackAttack::InstrumentedCacheStore do let(:store) { ::ActiveSupport::Cache::NullStore.new } - subject { described_class.new(upstream_store: store)} + subject { described_class.new(upstream_store: store) } where(:operation, :params, :test_proc) do :fetch | [:key] | ->(s) { s.fetch(:key) } diff --git a/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb index aa604dfab71..1b6fa584e3e 100644 --- a/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb +++ b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::RackAttack::UserAllowlist do using RSpec::Parameterized::TableSyntax - subject { described_class.new(input)} + subject { described_class.new(input) } where(:input, :elements) do nil | [] diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 31141ac1139..1f0ebbe107f 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -15,4 +15,16 @@ RSpec.describe Gitlab::Redis::Cache do expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380' ) end end + + describe '.active_support_config' do + it 'has a default ttl of 2 weeks' do + expect(described_class.active_support_config[:expires_in]).to eq(2.weeks) + end + + it 'allows configuring the TTL through an env variable' do + stub_env('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS' => '86400') + + expect(described_class.active_support_config[:expires_in]).to eq(1.day) + end + end end diff --git a/spec/lib/gitlab/redis/hll_spec.rb b/spec/lib/gitlab/redis/hll_spec.rb index e452e5b2f52..9cd339239bb 100644 --- a/spec/lib/gitlab/redis/hll_spec.rb +++ b/spec/lib/gitlab/redis/hll_spec.rb @@ -64,10 +64,10 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do let(:event_2020_33) { '2020-33-{expand_vulnerabilities}' } let(:event_2020_34) { '2020-34-{expand_vulnerabilities}' } - let(:entity1) { 'user_id_1'} - let(:entity2) { 'user_id_2'} - let(:entity3) { 'user_id_3'} - let(:entity4) { 'user_id_4'} + let(:entity1) { 'user_id_1' } + let(:entity2) { 'user_id_2' } + let(:entity3) { 'user_id_3' } + let(:entity4) { 'user_id_4' } before do track_event(event_2020_32, entity1) diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index 50ebf43a05e..ef8549548d7 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Gitlab::Redis::MultiStore do let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) } let_it_be(:instance_name) { 'TestStore' } - let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} + let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name) } subject { multi_store.send(name, *args) } @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Redis::MultiStore do end context 'when primary_store is nil' do - let(:multi_store) { described_class.new(nil, secondary_store, instance_name)} + let(:multi_store) { described_class.new(nil, secondary_store, instance_name) } it 'fails with exception' do expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/) @@ -46,7 +46,7 @@ RSpec.describe Gitlab::Redis::MultiStore do end context 'when secondary_store is nil' do - let(:multi_store) { described_class.new(primary_store, nil, instance_name)} + let(:multi_store) { described_class.new(primary_store, nil, instance_name) } it 'fails with exception' do expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/) @@ -55,7 +55,7 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'when instance_name is nil' do let(:instance_name) { nil } - let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} + let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name) } it 'fails with exception' do expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/) @@ -111,8 +111,8 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'with READ redis commands' do let_it_be(:key1) { "redis:{1}:key_a" } let_it_be(:key2) { "redis:{1}:key_b" } - let_it_be(:value1) { "redis_value1"} - let_it_be(:value2) { "redis_value2"} + let_it_be(:value1) { "redis_value1" } + let_it_be(:value2) { "redis_value2" } let_it_be(:skey) { "redis:set:key" } let_it_be(:keys) { [key1, key2] } let_it_be(:values) { [value1, value2] } @@ -330,7 +330,7 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'with both primary and secondary store using same redis instance' do let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) } - let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)} + let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name) } it_behaves_like 'secondary store' end @@ -356,8 +356,8 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'with WRITE redis commands' do let_it_be(:key1) { "redis:{1}:key_a" } let_it_be(:key2) { "redis:{1}:key_b" } - let_it_be(:value1) { "redis_value1"} - let_it_be(:value2) { "redis_value2"} + let_it_be(:value1) { "redis_value1" } + let_it_be(:value2) { "redis_value2" } let_it_be(:key1_value1) { [key1, value1] } let_it_be(:key1_value2) { [key1, value2] } let_it_be(:ttl) { 10 } @@ -395,7 +395,7 @@ RSpec.describe Gitlab::Redis::MultiStore do with_them do describe "#{name}" do - let(:expected_args) {args || no_args } + let(:expected_args) { args || no_args } before do allow(primary_store).to receive(name).and_call_original @@ -496,8 +496,8 @@ RSpec.describe Gitlab::Redis::MultiStore do RSpec.shared_examples_for 'pipelined command' do |name| let_it_be(:key1) { "redis:{1}:key_a" } - let_it_be(:value1) { "redis_value1"} - let_it_be(:value2) { "redis_value2"} + let_it_be(:value1) { "redis_value1" } + let_it_be(:value2) { "redis_value2" } let_it_be(:expected_value) { value1 } let_it_be(:verification_name) { :get } let_it_be(:verification_args) { key1 } diff --git a/spec/lib/gitlab/reference_counter_spec.rb b/spec/lib/gitlab/reference_counter_spec.rb index 83e4006c69b..05294fb84e7 100644 --- a/spec/lib/gitlab/reference_counter_spec.rb +++ b/spec/lib/gitlab/reference_counter_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Gitlab::ReferenceCounter, :clean_gitlab_redis_shared_state do it 'resets reference count down to zero' do 3.times { reference_counter.increase } - expect { reference_counter.reset! }.to change { reference_counter.value}.from(3).to(0) + expect { reference_counter.reset! }.to change { reference_counter.value }.from(3).to(0) end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index a3afbed18e2..d8f182d903d 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -270,7 +270,7 @@ RSpec.describe Gitlab::Regex do context 'conan recipe components' do shared_examples 'accepting valid recipe components values' do - let(:fifty_one_characters) { 'f_a' * 17} + let(:fifty_one_characters) { 'f_a' * 17 } it { is_expected.to match('foobar') } it { is_expected.to match('foo_bar') } @@ -374,12 +374,12 @@ RSpec.describe Gitlab::Regex do end end - it { is_expected.to match('0')} + it { is_expected.to match('0') } it { is_expected.to match('1') } it { is_expected.to match('03') } it { is_expected.to match('2.0') } it { is_expected.to match('01.2') } - it { is_expected.to match('10.2.3-beta')} + it { is_expected.to match('10.2.3-beta') } it { is_expected.to match('1.2-SNAPSHOT') } it { is_expected.to match('20') } it { is_expected.to match('20.3') } @@ -454,7 +454,7 @@ RSpec.describe Gitlab::Regex do it { is_expected.to match('0.1') } it { is_expected.to match('2.0') } - it { is_expected.to match('1.2.0')} + it { is_expected.to match('1.2.0') } it { is_expected.to match('0100!0.0') } it { is_expected.to match('00!1.2') } it { is_expected.to match('1.0a') } diff --git a/spec/lib/gitlab/search/abuse_detection_spec.rb b/spec/lib/gitlab/search/abuse_detection_spec.rb index a18d28456cd..2a8d74a62ab 100644 --- a/spec/lib/gitlab/search/abuse_detection_spec.rb +++ b/spec/lib/gitlab/search/abuse_detection_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Search::AbuseDetection do subject { described_class.new(params) } - let(:params) {{ query_string: 'foobar' }} + let(:params) { { query_string: 'foobar' } } describe 'abusive scopes validation' do it 'allows only approved scopes' do diff --git a/spec/lib/gitlab/search_context/builder_spec.rb b/spec/lib/gitlab/search_context/builder_spec.rb index a09115f3f21..78799b67a69 100644 --- a/spec/lib/gitlab/search_context/builder_spec.rb +++ b/spec/lib/gitlab/search_context/builder_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::SearchContext::Builder, type: :controller do - controller(ApplicationController) { } + controller(ApplicationController) {} subject(:builder) { described_class.new(controller.view_context) } diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb index a94ae2bca7a..0ad80323085 100644 --- a/spec/lib/gitlab/seeder_spec.rb +++ b/spec/lib/gitlab/seeder_spec.rb @@ -77,4 +77,44 @@ RSpec.describe Gitlab::Seeder do end end end + + describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) } + + subject(:build_report) do + described_class.new(project) + end + + describe '#seed' do + it 'creates daily build results for the project' do + expect { build_report.seed }.to change { + Ci::DailyBuildGroupReportResult.count + }.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + + it 'matches project data with last report' do + build_report.seed + + report = project.daily_build_group_report_results.last + reports_count = project.daily_build_group_report_results.count + + expect(build.group_name).to eq(report.group_name) + expect(pipeline.source_ref_path).to eq(report.ref_path) + expect(pipeline.default_branch?).to eq(report.default_branch) + expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + + it 'does not raise error on RecordNotUnique' do + build_report.seed + build_report.seed + + reports_count = project.daily_build_group_report_results.count + + expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS) + end + end + end end diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb index de680e8425e..67ad59f956d 100644 --- a/spec/lib/gitlab/session_spec.rb +++ b/spec/lib/gitlab/session_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Session do end it 'restores current store after' do - described_class.with_session(two: 2) { } + described_class.with_session(two: 2) {} expect(described_class.current).to eq nil end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 4a1a9beb21a..c62302d8bba 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -194,7 +194,7 @@ RSpec.describe Gitlab::SidekiqConfig do queues = described_class.routing_queues expect(queues).to match_array(%w[ - default mailers high_urgency gitaly email_receiver service_desk_email_receiver + default mailers high_urgency gitaly ]) expect(queues).not_to include('not_exist') end diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 01b7270d761..635f572daef 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do end describe '#stop_working' do - subject { memory_killer.send(:stop_working)} + subject { memory_killer.send(:stop_working) } it 'changes enable? to false' do expect { subject }.to change { memory_killer.send(:enabled?) } @@ -355,6 +355,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do let(:reason) { 'rss out of range reason description' } let(:queue) { 'default' } let(:running_jobs) { [{ jid: jid, worker_class: 'DummyWorker' }] } + let(:metrics) { memory_killer.instance_variable_get(:@metrics) } let(:worker) do Class.new do def self.name @@ -390,6 +391,9 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do reason: reason, running_jobs: running_jobs) + expect(metrics[:sidekiq_memory_killer_running_jobs]).to receive(:increment) + .with({ worker_class: "DummyWorker", deadline_exceeded: true }) + Gitlab::SidekiqDaemon::Monitor.instance.within_job(DummyWorker, jid, queue) do subject end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 9c0cbe21e6b..e3d9549a3c0 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(job, 'test_queue') { } + call_subject(job, 'test_queue') {} end end @@ -40,7 +40,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(wrapped_job, 'test_queue') { } + call_subject(wrapped_job, 'test_queue') {} end end @@ -175,7 +175,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(job, 'test_queue') { } + call_subject(job, 'test_queue') {} end end @@ -188,7 +188,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(job.except("created_at", "enqueued_at"), 'test_queue') { } + call_subject(job.except("created_at", "enqueued_at"), 'test_queue') {} end end end @@ -204,7 +204,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(job, 'test_queue') { } + call_subject(job, 'test_queue') {} end end end @@ -233,7 +233,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original - call_subject(job, 'test_queue') { } + call_subject(job, 'test_queue') {} end end end @@ -266,7 +266,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do expect(logger).to receive(:info).with(start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered - call_subject(job, 'test_queue') { } + call_subject(job, 'test_queue') {} end end end @@ -330,7 +330,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do Gitlab::SafeRequestStore.clear! - call_subject(job.dup, 'test_queue') { } + call_subject(job.dup, 'test_queue') {} end end diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb index 85cddfa7bf1..d61c9765753 100644 --- a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb @@ -41,7 +41,9 @@ RSpec.describe Gitlab::SidekiqMiddleware::Monitor do ::Sidekiq::DeadSet.new.clear expect do - subject rescue Sidekiq::JobRetry::Skip + subject + rescue Sidekiq::JobRetry::Skip + nil end.to change { ::Sidekiq::DeadSet.new.size }.by(1) end end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index 117b37ffda3..d6d24ea3a24 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -109,6 +109,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration) expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) + expect(sidekiq_mem_total_bytes).to receive(:set).with(labels_with_job_status, mem_total_bytes) subject.call(worker, job, :test) { nil } end diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 5167523ff58..5af234ff88e 100644 --- a/spec/lib/gitlab/slash_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -165,7 +165,7 @@ RSpec.describe Gitlab::SlashCommands::Deploy do context 'with ReDoS attempts' do def duration_for(&block) start = Time.zone.now - yield if block_given? + yield if block Time.zone.now - start end diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb index a6e7665569c..956ed2a976f 100644 --- a/spec/lib/gitlab/spamcheck/client_spec.rb +++ b/spec/lib/gitlab/spamcheck/client_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Gitlab::Spamcheck::Client do let(:stub) { double(:spamcheck_stub, check_for_spam_issue: response) } context 'is tls ' do - let(:endpoint) { 'tls://spamcheck.example.com'} + let(:endpoint) { 'tls://spamcheck.example.com' } it 'uses secure connection' do expect(Spamcheck::SpamcheckService::Stub).to receive(:new).with(endpoint.sub(%r{^tls://}, ''), @@ -97,7 +97,7 @@ RSpec.describe Gitlab::Spamcheck::Client do context: cxt) expect(issue_pb.title).to eq issue.title expect(issue_pb.description).to eq issue.description - expect(issue_pb.user_in_project). to be false + expect(issue_pb.user_in_project).to be false expect(issue_pb.project.project_id).to eq issue.project_id expect(issue_pb.created_at).to eq timestamp_to_protobuf_timestamp(issue.created_at) expect(issue_pb.updated_at).to eq timestamp_to_protobuf_timestamp(issue.updated_at) @@ -118,7 +118,7 @@ RSpec.describe Gitlab::Spamcheck::Client do end context 'when user has multiple email addresses' do - let(:secondary_email) {create(:email, :confirmed, user: user)} + let(:secondary_email) { create(:email, :confirmed, user: user) } before do user.emails << secondary_email diff --git a/spec/lib/gitlab/ssh/commit_spec.rb b/spec/lib/gitlab/ssh/commit_spec.rb new file mode 100644 index 00000000000..cc977a80f95 --- /dev/null +++ b/spec/lib/gitlab/ssh/commit_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::Ssh::Commit do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:signed_by_key) { create(:key) } + + let(:commit) { create(:commit, project: project) } + let(:signature_text) { 'signature_text' } + let(:signed_text) { 'signed_text' } + let(:signature_data) { [signature_text, signed_text] } + let(:verifier) { instance_double('Gitlab::Ssh::Signature') } + let(:verification_status) { :verified } + + subject(:signature) { described_class.new(commit).signature } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit.sha) + .and_return(signature_data) + + allow(verifier).to receive(:verification_status).and_return(verification_status) + allow(verifier).to receive(:signed_by_key).and_return(signed_by_key) + + allow(Gitlab::Ssh::Signature).to receive(:new) + .with(signature_text, signed_text, commit.committer_email) + .and_return(verifier) + end + + describe '#signature' do + it 'returns the cached signature on multiple calls' do + ssh_commit = described_class.new(commit) + + expect(ssh_commit).to receive(:create_cached_signature!).and_call_original + ssh_commit.signature + + expect(ssh_commit).not_to receive(:create_cached_signature!) + ssh_commit.signature + end + + context 'when all expected data is present' do + it 'calls signature verifier and uses returned attributes' do + expect(signature).to have_attributes( + commit_sha: commit.sha, + project: project, + key_id: signed_by_key.id, + verification_status: 'verified' + ) + end + end + + context 'when signed_by_key is nil' do + let_it_be(:signed_by_key) { nil } + + let(:verification_status) { :unknown_key } + + it 'creates signature without a key_id' do + expect(signature).to have_attributes( + commit_sha: commit.sha, + project: project, + key_id: nil, + verification_status: 'unknown_key' + ) + end + end + end + + describe '#update_signature!' do + it 'updates verification status' do + allow(verifier).to receive(:verification_status).and_return(:unverified) + signature + + stored_signature = CommitSignatures::SshSignature.find_by_commit_sha(commit.sha) + + allow(verifier).to receive(:verification_status).and_return(:verified) + + expect { described_class.new(commit).update_signature!(stored_signature) }.to( + change { signature.reload.verification_status }.from('unverified').to('verified') + ) + end + end +end diff --git a/spec/lib/gitlab/suggestions/file_suggestion_spec.rb b/spec/lib/gitlab/suggestions/file_suggestion_spec.rb index 1d25bf6edbd..5971f4ebbce 100644 --- a/spec/lib/gitlab/suggestions/file_suggestion_spec.rb +++ b/spec/lib/gitlab/suggestions/file_suggestion_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::Suggestions::FileSuggestion do let_it_be(:user) { create(:user) } - let_it_be(:file_path) { 'files/ruby/popen.rb'} + let_it_be(:file_path) { 'files/ruby/popen.rb' } let_it_be(:project) { create(:project, :repository) } diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb index 2554a15d97e..48092a33da3 100644 --- a/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb +++ b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb @@ -48,40 +48,8 @@ RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting) end - context 'when SNOWPLOW_MICRO_URI has scheme and port' do - before do - stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091') - end - - it 'returns hostname URI part' do - expect(subject.hostname).to eq('gdk.test:9091') - end - end - - context 'when SNOWPLOW_MICRO_URI is without protocol' do - before do - stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091') - end - - it 'returns hostname URI part' do - expect(subject.hostname).to eq('gdk.test:9091') - end - end - - context 'when SNOWPLOW_MICRO_URI is hostname only' do - before do - stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport') - end - - it 'returns hostname URI with default HTTP port' do - expect(subject.hostname).to eq('uriwithoutport:80') - end - end - - context 'when SNOWPLOW_MICRO_URI is not set' do - it 'returns localhost hostname' do - expect(subject.hostname).to eq('localhost:9090') - end + it 'returns localhost hostname' do + expect(subject.hostname).to eq('localhost:9090') end end end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index dd62c832f6f..028c985f3b3 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -90,15 +90,6 @@ RSpec.describe Gitlab::Tracking do it_behaves_like 'delegates to SnowplowMicro destination with proper options' end - - context "enabled with env variable" do - before do - allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting) - stub_env('SNOWPLOW_MICRO_ENABLE', '1') - end - - it_behaves_like 'delegates to SnowplowMicro destination with proper options' - end end it 'when feature flag is disabled' do @@ -149,7 +140,6 @@ RSpec.describe Gitlab::Tracking do context 'when destination is Snowplow' do before do - stub_env('SNOWPLOW_MICRO_ENABLE', '0') allow(Rails.env).to receive(:development?).and_return(true) end @@ -158,7 +148,6 @@ RSpec.describe Gitlab::Tracking do context 'when destination is SnowplowMicro' do before do - stub_env('SNOWPLOW_MICRO_ENABLE', '1') allow(Rails.env).to receive(:development?).and_return(true) end @@ -181,7 +170,7 @@ RSpec.describe Gitlab::Tracking do let_it_be(:definition_action) { 'definition_action' } let_it_be(:definition_category) { 'definition_category' } let_it_be(:label_description) { 'definition label description' } - let_it_be(:test_definition) {{ 'category': definition_category, 'action': definition_action }} + let_it_be(:test_definition) { { 'category': definition_category, 'action': definition_action } } before do allow_next_instance_of(described_class) do |instance| @@ -212,4 +201,28 @@ RSpec.describe Gitlab::Tracking do project: project, user: user, namespace: namespace, extra_key_1: 'extra value 1') end end + + describe 'snowplow_micro_enabled?' do + before do + allow(Rails.env).to receive(:development?).and_return(true) + end + + it 'returns true when snowplow_micro is enabled' do + stub_config(snowplow_micro: { enabled: true }) + + expect(described_class).to be_snowplow_micro_enabled + end + + it 'returns false when snowplow_micro is disabled' do + stub_config(snowplow_micro: { enabled: false }) + + expect(described_class).not_to be_snowplow_micro_enabled + end + + it 'returns false when snowplow_micro is not configured' do + allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting) + + expect(described_class).not_to be_snowplow_micro_enabled + 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 8e7bd7b84e6..f73155642d6 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb @@ -160,6 +160,38 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do end end end + + context 'with custom timestamp column' do + subject do + described_class.tap do |metric_class| + metric_class.relation { Issue } + metric_class.operation :count + metric_class.timestamp_column :last_edited_at + end.new(time_frame: '28d') + end + + it 'calculates a correct result' do + create(:issue, last_edited_at: 5.days.ago) + + expect(subject.value).to eq(1) + end + end + + context 'with default timestamp column' do + subject do + described_class.tap do |metric_class| + metric_class.relation { Issue } + metric_class.operation :count + end.new(time_frame: '28d') + end + + it 'calculates a correct result' do + create(:issue, last_edited_at: 5.days.ago) + create(:issue, created_at: 5.days.ago) + + expect(subject.value).to eq(1) + end + end end context 'with unimplemented operation method used' do diff --git a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb index 9ee8bc6b568..f9cd6e88e0a 100644 --- a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb +++ b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb @@ -66,7 +66,7 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do let(:key_path) { 'counts.jira_imports_total_imported_issues_count' } let(:operation) { :sum } let(:relation) { JiraImportState.finished } - let(:column) { :imported_issues_count} + let(:column) { :imported_issues_count } let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_jira_imports/ } end end @@ -77,7 +77,7 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do let(:key_path) { 'counts.ci_pipeline_duration' } let(:operation) { :average } let(:relation) { Ci::Pipeline } - let(:column) { :duration} + let(:column) { :duration } let(:name_suggestion) { /average_duration_from_ci_pipelines/ } end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb index 167dba9b57d..7e8b15d23db 100644 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do end describe '#add_metric' do - let(:metric) {'CountIssuesMetric' } + let(:metric) { 'CountIssuesMetric' } it 'computes the suggested name for given metric' do expect(described_class.add_metric(metric)).to eq('count_issues') diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb index 1e8f9db4dea..7a37a31b195 100644 --- a/spec/lib/gitlab/usage/service_ping_report_spec.rb +++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb @@ -111,8 +111,12 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c # 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 # about every metric, queries are wrapped explicitly in sub transactions. - ApplicationRecord.transaction do - ApplicationRecord.connection.execute(query)&.first&.values&.first + table = PgQuery.parse(query).tables.first + gitlab_schema = Gitlab::Database::GitlabSchema.tables_to_schema[table] + base_model = gitlab_schema == :gitlab_main ? ApplicationRecord : Ci::ApplicationRecord + + base_model.transaction do + base_model.connection.execute(query)&.first&.values&.first end rescue ActiveRecord::StatementInvalid => e e.message diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 54d49b432f4..e0b334cb5af 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -77,32 +77,18 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end describe '.unique_events_data' do - context 'with use_redis_hll_instrumentation_classes feature enabled' do - it 'does not include instrumented categories' do - stub_feature_flags(use_redis_hll_instrumentation_classes: true) - - expect(described_class.unique_events_data.keys) - .not_to include(*described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS) - end - end - - context 'with use_redis_hll_instrumentation_classes feature disabled' do - it 'includes instrumented categories' do - stub_feature_flags(use_redis_hll_instrumentation_classes: false) - - expect(described_class.unique_events_data.keys) - .to include(*described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS) - end + it 'does not include instrumented categories' do + expect(described_class.unique_events_data.keys) + .not_to include(*described_class.categories_collected_from_metrics_definitions) end end end describe '.categories' do - it 'gets all unique category names' do - expect(described_class.categories).to contain_exactly( + it 'gets CE unique category names' do + expect(described_class.categories).to include( 'deploy_token_packages', 'user_packages', - 'compliance', 'ecosystem', 'analytics', 'ide_edit', @@ -130,7 +116,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'work_items', 'ci_users', 'error_tracking', - 'manage' + 'manage', + 'kubernetes_agent' ) end end @@ -483,7 +470,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.weekly_redis_keys' do using RSpec::Parameterized::TableSyntax - let(:weekly_event) { 'g_compliance_dashboard' } + let(:weekly_event) { 'i_search_total' } let(:redis_event) { described_class.send(:event_for, weekly_event) } subject(:weekly_redis_keys) { described_class.send(:weekly_redis_keys, events: [redis_event], start_date: DateTime.parse(start_date), end_date: DateTime.parse(end_date)) } @@ -493,13 +480,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s '2020-12-21' | '2020-12-20' | [] '2020-12-21' | '2020-11-21' | [] '2021-01-01' | '2020-12-28' | [] - '2020-12-21' | '2020-12-28' | ['g_{compliance}_dashboard-2020-52'] - '2020-12-21' | '2021-01-01' | ['g_{compliance}_dashboard-2020-52'] - '2020-12-27' | '2021-01-01' | ['g_{compliance}_dashboard-2020-52'] - '2020-12-26' | '2021-01-04' | ['g_{compliance}_dashboard-2020-52', 'g_{compliance}_dashboard-2020-53'] - '2020-12-26' | '2021-01-11' | ['g_{compliance}_dashboard-2020-52', 'g_{compliance}_dashboard-2020-53', 'g_{compliance}_dashboard-2021-01'] - '2020-12-26' | '2021-01-17' | ['g_{compliance}_dashboard-2020-52', 'g_{compliance}_dashboard-2020-53', 'g_{compliance}_dashboard-2021-01'] - '2020-12-26' | '2021-01-18' | ['g_{compliance}_dashboard-2020-52', 'g_{compliance}_dashboard-2020-53', 'g_{compliance}_dashboard-2021-01', 'g_{compliance}_dashboard-2021-02'] + '2020-12-21' | '2020-12-28' | ['i_{search}_total-2020-52'] + '2020-12-21' | '2021-01-01' | ['i_{search}_total-2020-52'] + '2020-12-27' | '2021-01-01' | ['i_{search}_total-2020-52'] + '2020-12-26' | '2021-01-04' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53'] + '2020-12-26' | '2021-01-11' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01'] + '2020-12-26' | '2021-01-17' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01'] + '2020-12-26' | '2021-01-18' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01', 'i_{search}_total-2021-02'] end with_them do 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 index 60c4424d2ae..b778f532a11 100644 --- 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 @@ -43,18 +43,18 @@ RSpec.describe Gitlab::UsageDataCounters::IpynbDiffActivityCounter, :clean_gitla 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} + 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} + 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} + 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 @@ -62,35 +62,35 @@ RSpec.describe Gitlab::UsageDataCounters::IpynbDiffActivityCounter, :clean_gitla 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} + 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} + 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} + 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} + 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} + 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} + 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 diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index 1b73e5269d7..84a6f338282 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -6,7 +6,12 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git let_it_be(:user1) { build(:user, id: 1) } let_it_be(:user2) { build(:user, id: 2) } let_it_be(:user3) { build(:user, id: 3) } + let_it_be(:project) { build(:project) } + let_it_be(:category) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CATEGORY } + let_it_be(:event_action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION } + let_it_be(:event_label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL } + let(:event_property) { action } let(:time) { Time.zone.now } context 'for Issue title edit actions' do @@ -120,8 +125,8 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end context 'for Issue cloned actions' do - it_behaves_like 'a daily tracked issuable event' do - let(:action) { described_class::ISSUE_CLONED } + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do + let_it_be(:action) { described_class::ISSUE_CLONED } def track_action(params) described_class.track_issue_cloned_action(**params) @@ -239,8 +244,8 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end - context 'for Issue comment added actions' do - it_behaves_like 'a daily tracked issuable event' do + context 'for Issue comment added actions', :snowplow do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_COMMENT_ADDED } def track_action(params) @@ -249,8 +254,8 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end - context 'for Issue comment edited actions' do - it_behaves_like 'a daily tracked issuable event' do + context 'for Issue comment edited actions', :snowplow do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_COMMENT_EDITED } def track_action(params) @@ -259,8 +264,8 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end - context 'for Issue comment removed actions' do - it_behaves_like 'a daily tracked issuable event' do + context 'for Issue comment removed actions', :snowplow do + it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do let(:action) { described_class::ISSUE_COMMENT_REMOVED } def track_action(params) diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter_spec.rb new file mode 100644 index 00000000000..e073fac504a --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::MergeRequestWidgetExtensionCounter do + it_behaves_like 'a redis usage counter', 'Widget Extension', :test_summary_count_expand + + it_behaves_like 'a redis usage counter with totals', :i_code_review_merge_request_widget, test_summary_count_expand: 5 +end diff --git a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb index 0264236f087..0bcdbe82a7a 100644 --- a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb @@ -20,4 +20,12 @@ RSpec.describe Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter, :clean_ it_behaves_like 'work item unique counter' end + + describe '.track_work_item_date_changed_action' do + subject(:track_event) { described_class.track_work_item_date_changed_action(author: user) } + + let(:event_name) { described_class::WORK_ITEM_DATE_CHANGED } + + it_behaves_like 'work item unique counter' + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 6eb00053b17..692b6483149 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1203,12 +1203,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do describe 'redis_hll_counters' do subject { described_class.redis_hll_counters } - let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } + let(:migrated_categories) do + ::Gitlab::UsageDataCounters::HLLRedisCounter.categories_collected_from_metrics_definitions + end + let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories - migrated_categories } let(:ignored_metrics) { ["i_package_composer_deploy_token_weekly"] } it 'has all known_events' do - stub_feature_flags(use_redis_hll_instrumentation_classes: false) expect(subject).to have_key(:redis_hll_counters) expect(subject[:redis_hll_counters].keys).to match_array(categories) diff --git a/spec/lib/gitlab/utils/batch_loader_spec.rb b/spec/lib/gitlab/utils/batch_loader_spec.rb new file mode 100644 index 00000000000..c1f6d6df07a --- /dev/null +++ b/spec/lib/gitlab/utils/batch_loader_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'batch-loader' + +RSpec.describe Gitlab::Utils::BatchLoader do + let(:stubbed_loader) do + double( # rubocop:disable RSpec/VerifiedDoubles + 'Loader', + load_lazy_method: [], + load_lazy_method_same_batch_key: [], + load_lazy_method_other_batch_key: [] + ) + end + + let(:test_module) do + Module.new do + def self.lazy_method(id) + BatchLoader.for(id).batch(key: :my_batch_name) do |ids, loader| + stubbed_loader.load_lazy_method(ids) + + ids.each { |id| loader.call(id, id) } + end + end + + def self.lazy_method_same_batch_key(id) + BatchLoader.for(id).batch(key: :my_batch_name) do |ids, loader| + stubbed_loader.load_lazy_method_same_batch_key(ids) + + ids.each { |id| loader.call(id, id) } + end + end + + def self.lazy_method_other_batch_key(id) + BatchLoader.for(id).batch(key: :other_batch_name) do |ids, loader| + stubbed_loader.load_lazy_method_other_batch_key(ids) + + ids.each { |id| loader.call(id, id) } + end + end + end + end + + before do + BatchLoader::Executor.clear_current + allow(test_module).to receive(:stubbed_loader).and_return(stubbed_loader) + end + + describe '.clear_key' do + it 'clears batched items which match the specified batch key' do + test_module.lazy_method(1) + test_module.lazy_method_same_batch_key(2) + test_module.lazy_method_other_batch_key(3) + + described_class.clear_key(:my_batch_name) + + test_module.lazy_method(4).to_i + test_module.lazy_method_same_batch_key(5).to_i + test_module.lazy_method_other_batch_key(6).to_i + + expect(stubbed_loader).to have_received(:load_lazy_method).with([4]) + expect(stubbed_loader).to have_received(:load_lazy_method_same_batch_key).with([5]) + expect(stubbed_loader).to have_received(:load_lazy_method_other_batch_key).with([3, 6]) + end + + it 'clears loaded values which match the specified batch key' do + test_module.lazy_method(1).to_i + test_module.lazy_method_same_batch_key(2).to_i + test_module.lazy_method_other_batch_key(3).to_i + + described_class.clear_key(:my_batch_name) + + test_module.lazy_method(1).to_i + test_module.lazy_method_same_batch_key(2).to_i + test_module.lazy_method_other_batch_key(3).to_i + + expect(stubbed_loader).to have_received(:load_lazy_method).with([1]).twice + expect(stubbed_loader).to have_received(:load_lazy_method_same_batch_key).with([2]).twice + expect(stubbed_loader).to have_received(:load_lazy_method_other_batch_key).with([3]) + end + end +end diff --git a/spec/lib/gitlab/utils/link_header_parser_spec.rb b/spec/lib/gitlab/utils/link_header_parser_spec.rb new file mode 100644 index 00000000000..e15ef930271 --- /dev/null +++ b/spec/lib/gitlab/utils/link_header_parser_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Utils::LinkHeaderParser do + let(:parser) { described_class.new(header) } + + describe '#parse' do + subject { parser.parse } + + context 'with a valid header' do + let(:header) { generate_header(next: 'http://sandbox.org/next') } + let(:expected) { { next: { uri: URI('http://sandbox.org/next') } } } + + it { is_expected.to eq(expected) } + + context 'with multiple links' do + let(:header) { generate_header(next: 'http://sandbox.org/next', previous: 'http://sandbox.org/previous') } + let(:expected) do + { + next: { uri: URI('http://sandbox.org/next') }, + previous: { uri: URI('http://sandbox.org/previous') } + } + end + + it { is_expected.to eq(expected) } + end + + context 'with an incomplete uri' do + let(:header) { '<http://sandbox.org/next; rel="next"' } + + it { is_expected.to eq({}) } + end + + context 'with no rel' do + let(:header) { '<http://sandbox.org/next>; direction="next"' } + + it { is_expected.to eq({}) } + end + + context 'with multiple rel elements' do + # check https://datatracker.ietf.org/doc/html/rfc5988#section-5.3: + # occurrences after the first MUST be ignored by parsers + let(:header) { '<http://sandbox.org/next>; rel="next"; rel="dummy"' } + + it { is_expected.to eq(expected) } + end + + context 'when the url is too long' do + let(:header) { "<http://sandbox.org/#{'a' * 500}>; rel=\"next\"" } + + it { is_expected.to eq({}) } + end + end + + context 'with nil header' do + let(:header) { nil } + + it { is_expected.to eq({}) } + end + + context 'with empty header' do + let(:header) { '' } + + it { is_expected.to eq({}) } + end + + def generate_header(links) + stringified_links = links.map do |rel, url| + "<#{url}>; rel=\"#{rel}\"" + end + stringified_links.join(', ') + end + end +end diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb index 514051b1cc0..3ab592dfc62 100644 --- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb +++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb @@ -68,7 +68,7 @@ RSpec.describe Gitlab::Utils::SanitizeNodeLink do describe "#safe_protocol?" do let(:doc) { HTML::Pipeline.parse("<a href='#{scheme}alert(1);'>foo</a>") } let(:node) { doc.children.first } - let(:uri) { Addressable::URI.parse(node['href'])} + let(:uri) { Addressable::URI.parse(node['href']) } it "returns false" do expect(object.safe_protocol?(scheme)).to be_falsy diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb index 5350e090e2b..cb03797b3d9 100644 --- a/spec/lib/gitlab/utils/strong_memoize_spec.rb +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -1,10 +1,27 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-benchmark' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end RSpec.describe Gitlab::Utils::StrongMemoize do let(:klass) do - struct = Struct.new(:value) do + strong_memoize_class = described_class + + Struct.new(:value) do + include strong_memoize_class + + def self.method_added_list + @method_added_list ||= [] + end + + def self.method_added(name) + method_added_list << name + end + def method_name strong_memoize(:method_name) do trace << value @@ -12,21 +29,56 @@ RSpec.describe Gitlab::Utils::StrongMemoize do end end + def method_name_attr + trace << value + value + end + strong_memoize_attr :method_name_attr + + strong_memoize_attr :different_method_name_attr, :different_member_name_attr + def different_method_name_attr + trace << value + value + end + + strong_memoize_attr :enabled? + def enabled? + true + end + def trace @trace ||= [] end - end - struct.include(described_class) - struct + protected + + def private_method + end + private :private_method + strong_memoize_attr :private_method + + public + + def protected_method + end + protected :protected_method + strong_memoize_attr :protected_method + + private + + def public_method + end + public :public_method + strong_memoize_attr :public_method + end end subject(:object) { klass.new(value) } shared_examples 'caching the value' do it 'only calls the block once' do - value0 = object.method_name - value1 = object.method_name + value0 = object.send(method_name) + value1 = object.send(method_name) expect(value0).to eq(value) expect(value1).to eq(value) @@ -34,8 +86,8 @@ RSpec.describe Gitlab::Utils::StrongMemoize do end it 'returns and defines the instance variable for the exact value' do - returned_value = object.method_name - memoized_value = object.instance_variable_get(:@method_name) + returned_value = object.send(method_name) + memoized_value = object.instance_variable_get(:"@#{member_name}") expect(returned_value).to eql(value) expect(memoized_value).to eql(value) @@ -46,12 +98,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize do [nil, false, true, 'value', 0, [0]].each do |value| context "with value #{value}" do let(:value) { value } + let(:method_name) { :method_name } + let(:member_name) { :method_name } it_behaves_like 'caching the value' - it 'raises exception for invalid key' do + it 'raises exception for invalid type as key' do expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/ end + + it 'raises exception for invalid characters in key' do + expect { object.strong_memoize(:enabled?) { 20 } } + .to raise_error /is not allowed as an instance variable name/ + end end end @@ -109,4 +168,64 @@ RSpec.describe Gitlab::Utils::StrongMemoize do expect(object.instance_variable_defined?(:@method_name)).to be(false) end end + + describe '.strong_memoize_attr' do + [nil, false, true, 'value', 0, [0]].each do |value| + let(:value) { value } + + context "memoized after method definition with value #{value}" do + let(:method_name) { :method_name_attr } + let(:member_name) { :method_name_attr } + + it_behaves_like 'caching the value' + + it 'calls the existing .method_added' do + expect(klass.method_added_list).to include(:method_name_attr) + end + end + + context "memoized before method definition with different member name and value #{value}" do + let(:method_name) { :different_method_name_attr } + let(:member_name) { :different_member_name_attr } + + it_behaves_like 'caching the value' + + it 'calls the existing .method_added' do + expect(klass.method_added_list).to include(:different_method_name_attr) + end + end + + context 'with valid method name' do + let(:method_name) { :enabled? } + + context 'with invalid member name' do + let(:member_name) { :enabled? } + + it 'is invalid' do + expect { object.send(method_name) { value } }.to raise_error /is not allowed as an instance variable name/ + end + end + end + end + + describe 'method visibility' do + it 'sets private visibility' do + expect(klass.private_instance_methods).to include(:private_method) + expect(klass.protected_instance_methods).not_to include(:private_method) + expect(klass.public_instance_methods).not_to include(:private_method) + end + + it 'sets protected visibility' do + expect(klass.private_instance_methods).not_to include(:protected_method) + expect(klass.protected_instance_methods).to include(:protected_method) + expect(klass.public_instance_methods).not_to include(:protected_method) + end + + it 'sets public visibility' do + expect(klass.private_instance_methods).not_to include(:public_method) + expect(klass.protected_instance_methods).not_to include(:public_method) + expect(klass.public_instance_methods).to include(:public_method) + end + end + end end diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 25ba5a3e09e..13d046b0816 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Utils::UsageData do end describe '#add_metric' do - let(:metric) { 'UuidMetric'} + let(:metric) { 'UuidMetric' } it 'computes the metric value for given metric' do expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid) diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 0648d276a6b..ad1a65ffae8 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -115,7 +115,7 @@ RSpec.describe Gitlab::Utils do end it 'raises error for a non-string' do - expect {check_allowed_absolute_path_and_path_traversal!(nil, allowed_paths)}.to raise_error(StandardError) + expect { check_allowed_absolute_path_and_path_traversal!(nil, allowed_paths) }.to raise_error(StandardError) end it 'raises an exception if an absolute path is not allowed' do @@ -128,7 +128,7 @@ RSpec.describe Gitlab::Utils do end describe '.allowlisted?' do - let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd']} + let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] } it 'returns true if path is allowed' do expect(allowlisted?('/foo/bar', allowed_paths)).to be(true) diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb index 3e5154d5029..f9aa196ffde 100644 --- a/spec/lib/gitlab/verify/uploads_spec.rb +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -90,7 +90,7 @@ RSpec.describe Gitlab::Verify::Uploads do end def perform_task - described_class.new(batch_size: 100).run_batches { } + described_class.new(batch_size: 100).run_batches {} end end end diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index 6ed094f11c8..078f952afad 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -79,11 +79,12 @@ RSpec.describe Gitlab::VersionInfo do describe '.unknown' do it { expect(@unknown).not_to be @v0_0_1 } it { expect(@unknown).not_to be described_class.new } - it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } - it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } + it { expect { @unknown > @v0_0_1 }.to raise_error(ArgumentError) } + it { expect { @unknown < @v0_0_1 }.to raise_error(ArgumentError) } end describe '.parse' do + it { expect(described_class.parse(described_class.new(1, 0, 0))).to eq(@v1_0_0) } it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) } it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) } it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) } @@ -133,6 +134,20 @@ RSpec.describe Gitlab::VersionInfo do it { expect(@unknown.to_s).to eq("Unknown") } end + describe '.to_json' do + let(:correct_version) do + "{\"major\":1,\"minor\":0,\"patch\":1}" + end + + let(:unknown_version) do + "{\"major\":0,\"minor\":0,\"patch\":0}" + end + + it { expect(@v1_0_1.to_json).to eq(correct_version) } + it { expect(@v1_0_1_rc2.to_json).to eq(correct_version) } + it { expect(@unknown.to_json).to eq(unknown_version) } + end + describe '.hash' do it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) } it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } |