diff options
Diffstat (limited to 'spec/models/concerns')
-rw-r--r-- | spec/models/concerns/bulk_insert_safe_spec.rb | 6 | ||||
-rw-r--r-- | spec/models/concerns/ci/has_variable_spec.rb | 1 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable/switch_spec.rb | 316 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable_spec.rb | 21 | ||||
-rw-r--r-- | spec/models/concerns/encrypted_user_password_spec.rb | 138 | ||||
-rw-r--r-- | spec/models/concerns/file_store_mounter_spec.rb | 93 | ||||
-rw-r--r-- | spec/models/concerns/has_user_type_spec.rb | 42 | ||||
-rw-r--r-- | spec/models/concerns/issuable_spec.rb | 41 | ||||
-rw-r--r-- | spec/models/concerns/pg_full_text_searchable_spec.rb | 38 | ||||
-rw-r--r-- | spec/models/concerns/project_features_compatibility_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/concerns/sha_attribute_spec.rb | 10 | ||||
-rw-r--r-- | spec/models/concerns/subquery_spec.rb | 61 | ||||
-rw-r--r-- | spec/models/concerns/token_authenticatable_spec.rb | 10 |
13 files changed, 734 insertions, 45 deletions
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb index 569dc3a3a3e..577004c2cf6 100644 --- a/spec/models/concerns/bulk_insert_safe_spec.rb +++ b/spec/models/concerns/bulk_insert_safe_spec.rb @@ -73,9 +73,9 @@ RSpec.describe BulkInsertSafe do key: Settings.attr_encrypted_db_key_base_32, insecure_mode: false - default_value_for :enum_value, 'case_1' - default_value_for :sha_value, '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12' - default_value_for :jsonb_value, { "key" => "value" } + attribute :enum_value, default: 'case_1' + attribute :sha_value, default: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12' + attribute :jsonb_value, default: -> { { "key" => "value" } } def self.name 'BulkInsertItem' diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb index bf699119a37..861d8f3b974 100644 --- a/spec/models/concerns/ci/has_variable_spec.rb +++ b/spec/models/concerns/ci/has_variable_spec.rb @@ -84,6 +84,7 @@ RSpec.describe Ci::HasVariable do key: subject.key, value: subject.value, public: false, + raw: false, masked: false } end diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb new file mode 100644 index 00000000000..d955ad223f8 --- /dev/null +++ b/spec/models/concerns/ci/partitionable/switch_spec.rb @@ -0,0 +1,316 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do + let(:model) do + Class.new(Ci::ApplicationRecord) do + self.primary_key = :id + self.table_name = :_test_ci_jobs_metadata + self.sequence_name = :_test_ci_jobs_metadata_id_seq + + def self.name + 'TestSwitchJobMetadata' + end + end + end + + let(:table_rollout_flag) { :ci_partitioning_use_test_routing_table } + + let(:partitioned_model) { model::Partitioned } + + let(:jobs_model) do + Class.new(Ci::ApplicationRecord) do + self.primary_key = :id + self.table_name = :_test_ci_jobs + + def self.name + 'TestSwitchJob' + end + end + end + + before do + allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited) + + create_tables(<<~SQL) + CREATE TABLE _test_ci_jobs_metadata( + id serial NOT NULL PRIMARY KEY, + job_id int, + partition_id int NOT NULL DEFAULT 1, + expanded_environment_name text); + + CREATE TABLE _test_p_ci_jobs_metadata ( + LIKE _test_ci_jobs_metadata INCLUDING DEFAULTS + ) PARTITION BY LIST(partition_id); + + ALTER TABLE _test_p_ci_jobs_metadata + ADD CONSTRAINT _test_p_ci_jobs_metadata_id_partition_id + UNIQUE (id, partition_id); + + ALTER TABLE _test_p_ci_jobs_metadata + ATTACH PARTITION _test_ci_jobs_metadata FOR VALUES IN (1); + + CREATE TABLE _test_ci_jobs(id serial NOT NULL PRIMARY KEY); + SQL + + stub_const('Ci::Partitionable::Testing::PARTITIONABLE_MODELS', [model.name]) + + model.include(Ci::Partitionable) + + model.partitionable scope: ->(r) { 1 }, + through: { table: :_test_p_ci_jobs_metadata, flag: table_rollout_flag } + + model.belongs_to :job, anonymous_class: jobs_model + + jobs_model.has_one :metadata, anonymous_class: model, + foreign_key: :job_id, inverse_of: :job, + dependent: :destroy + + allow(Feature::Definition).to receive(:get).and_call_original + allow(Feature::Definition).to receive(:get).with(table_rollout_flag) + .and_return( + Feature::Definition.new("development/#{table_rollout_flag}.yml", + { type: 'development', name: table_rollout_flag } + ) + ) + end + + it { expect(model).not_to be_routing_class } + + it { expect(partitioned_model).to be_routing_class } + + it { expect(partitioned_model.table_name).to eq('_test_p_ci_jobs_metadata') } + + it { expect(partitioned_model.quoted_table_name).to eq('"_test_p_ci_jobs_metadata"') } + + it { expect(partitioned_model.arel_table.name).to eq('_test_p_ci_jobs_metadata') } + + it { expect(partitioned_model.sequence_name).to eq('_test_ci_jobs_metadata_id_seq') } + + context 'when switching the tables' do + before do + stub_feature_flags(table_rollout_flag => false) + end + + %i[table_name quoted_table_name arel_table predicate_builder].each do |name| + it "switches #{name} to routing table and rollbacks" do + old_value = model.public_send(name) + routing_value = partitioned_model.public_send(name) + + expect(old_value).not_to eq(routing_value) + + expect { stub_feature_flags(table_rollout_flag => true) } + .to change(model, name).from(old_value).to(routing_value) + + expect { stub_feature_flags(table_rollout_flag => false) } + .to change(model, name).from(routing_value).to(old_value) + end + end + + it 'can switch aggregate methods' do + rollout_and_rollback_flag( + -> { expect(sql { model.count }).to all match(/FROM "_test_ci_jobs_metadata"/) }, + -> { expect(sql { model.count }).to all match(/FROM "_test_p_ci_jobs_metadata"/) } + ) + end + + it 'can switch reads' do + rollout_and_rollback_flag( + -> { expect(sql { model.last }).to all match(/FROM "_test_ci_jobs_metadata"/) }, + -> { expect(sql { model.last }).to all match(/FROM "_test_p_ci_jobs_metadata"/) } + ) + end + + it 'can switch inserts' do + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT/) { model.create! }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /INSERT/) { model.create! }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch deletes' do + 3.times { model.create! } + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /DELETE/) { model.last.destroy! }) + .to all match(/DELETE FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /DELETE/) { model.last.destroy! }) + .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + context 'with associations' do + let(:job) { jobs_model.create! } + + it 'reads' do + model.create!(job_id: job.id) + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata }) + .to all match(/FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata }) + .to all match(/FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'writes' do + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'deletes' do + 3.times do + job = jobs_model.create! + job.create_metadata! + end + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! }) + .to all match(/DELETE FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! }) + .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch joins from jobs' do + rollout_and_rollback_flag( + -> { + expect(sql { jobs_model.joins(:metadata).last }) + .to all match(/INNER JOIN "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql { jobs_model.joins(:metadata).last }) + .to all match(/INNER JOIN "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch joins from metadata' do + rollout_and_rollback_flag( + -> { + expect(sql { model.joins(:job).last }) + .to all match(/FROM "_test_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/) + }, + -> { + expect(sql { model.joins(:job).last }) + .to all match(/FROM "_test_p_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/) + } + ) + end + + it 'preloads' do + job = jobs_model.create! + job.create_metadata! + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last }) + .to all match(/FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last }) + .to all match(/FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + context 'with nested attributes' do + before do + jobs_model.accepts_nested_attributes_for :metadata + end + + it 'writes' do + attrs = { metadata_attributes: { expanded_environment_name: 'test_env_name' } } + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata" .* 'test_env_name'/) + }, + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata" .* 'test_env_name'/) + } + ) + end + end + end + end + + context 'with safe request store', :request_store do + it 'changing the flag to true does not affect the current request' do + stub_feature_flags(table_rollout_flag => false) + + expect(model.table_name).to eq('_test_ci_jobs_metadata') + + stub_feature_flags(table_rollout_flag => true) + + expect(model.table_name).to eq('_test_ci_jobs_metadata') + end + + it 'changing the flag to false does not affect the current request' do + stub_feature_flags(table_rollout_flag => true) + + expect(model.table_name).to eq('_test_p_ci_jobs_metadata') + + stub_feature_flags(table_rollout_flag => false) + + expect(model.table_name).to eq('_test_p_ci_jobs_metadata') + end + end + + def rollout_and_rollback_flag(old, new) + # Load class and SQL statements cache + old.call + + stub_feature_flags(table_rollout_flag => true) + + # Test switch + new.call + + stub_feature_flags(table_rollout_flag => false) + + # Test that it can switch back in the same process + old.call + end + + def create_tables(table_sql) + Ci::ApplicationRecord.connection.execute(table_sql) + end + + def sql(filter: nil, &block) + result = ActiveRecord::QueryRecorder.new(&block) + result = result.log + + return result unless filter + + result.select { |statement| statement.match?(filter) } + end +end diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb index d53501ccc3d..f3d33c971c7 100644 --- a/spec/models/concerns/ci/partitionable_spec.rb +++ b/spec/models/concerns/ci/partitionable_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Ci::Partitionable do - describe 'partitionable models inclusion' do - let(:ci_model) { Class.new(Ci::ApplicationRecord) } + let(:ci_model) { Class.new(Ci::ApplicationRecord) } + describe 'partitionable models inclusion' do subject { ci_model.include(described_class) } it 'raises an exception' do @@ -23,4 +23,21 @@ RSpec.describe Ci::Partitionable do end end end + + context 'with through options' do + before do + allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited) + stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name]) + + ci_model.include(described_class) + ci_model.partitionable scope: ->(r) { 1 }, + through: { table: :_test_table_name, flag: :some_flag } + end + + it { expect(ci_model.routing_table_name).to eq(:_test_table_name) } + + it { expect(ci_model.routing_table_name_flag).to eq(:some_flag) } + + it { expect(ci_model.ancestors).to include(described_class::Switch) } + end end diff --git a/spec/models/concerns/encrypted_user_password_spec.rb b/spec/models/concerns/encrypted_user_password_spec.rb new file mode 100644 index 00000000000..b6447313967 --- /dev/null +++ b/spec/models/concerns/encrypted_user_password_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe User do + describe '#authenticatable_salt' do + let(:user) { build(:user, encrypted_password: encrypted_password) } + + subject(:authenticatable_salt) { user.authenticatable_salt } + + context 'when password is stored in BCrypt format' do + let(:encrypted_password) { '$2a$10$AvwDCyF/8HnlAv./UkAZx.vAlKRS89yNElP38FzdgOmVaSaiDL7xm' } + + it 'returns the first 30 characters of the encrypted_password' do + expect(authenticatable_salt).to eq(user.encrypted_password[0, 29]) + end + end + + context 'when password is stored in PBKDF2 format' do + let(:encrypted_password) { '$pbkdf2-sha512$20000$rKbYsScsDdk$iwWBewXmrkD2fFfaG1SDcMIvl9gvEo3fBWUAfiqyVceTlw/DYgKBByHzf45pF5Qn59R4R.NQHsFpvZB4qlsYmw' } # rubocop:disable Layout/LineLength + + it 'uses the decoded password salt' do + expect(authenticatable_salt).to eq('aca6d8b1272c0dd9') + end + + it 'does not use the first 30 characters of the encrypted_password' do + expect(authenticatable_salt).not_to eq(encrypted_password[0, 29]) + end + end + + context 'when the encrypted_password is an unknown type' do + let(:encrypted_password) { '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' } + + it 'returns the first 30 characters of the encrypted_password' do + expect(authenticatable_salt).to eq(encrypted_password[0, 29]) + end + end + end + + describe '#valid_password?' do + subject(:validate_password) { user.valid_password?(password) } + + let(:user) { build(:user, encrypted_password: encrypted_password) } + let(:password) { described_class.random_password } + + shared_examples 'password validation fails when the password is encrypted using an unsupported method' do + let(:encrypted_password) { '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' } + + it { is_expected.to eq(false) } + end + + context 'when the default encryption method is BCrypt' do + it_behaves_like 'password validation fails when the password is encrypted using an unsupported method' + + context 'when the user password PBKDF2+SHA512' do + let(:encrypted_password) do + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest( + password, 20_000, Devise.friendly_token[0, 16]) + end + + it { is_expected.to eq(true) } + + it 're-encrypts the password as BCrypt' do + expect(user.encrypted_password).to start_with('$pbkdf2-sha512$') + + validate_password + + expect(user.encrypted_password).to start_with('$2a$') + end + end + end + + context 'when the default encryption method is PBKDF2+SHA512 and the user password is BCrypt', :fips_mode do + it_behaves_like 'password validation fails when the password is encrypted using an unsupported method' + + context 'when the user password BCrypt' do + let(:encrypted_password) { Devise::Encryptor.digest(described_class, password) } + + it { is_expected.to eq(true) } + + it 're-encrypts the password as PBKDF2+SHA512' do + expect(user.encrypted_password).to start_with('$2a$') + + validate_password + + expect(user.reload.encrypted_password).to start_with('$pbkdf2-sha512$') + end + end + end + end + + describe '#password=' do + let(:user) { build(:user) } + let(:password) { described_class.random_password } + + def compare_bcrypt_password(user, password) + Devise::Encryptor.compare(described_class, user.encrypted_password, password) + end + + def compare_pbkdf2_password(user, password) + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(user.encrypted_password, password) + end + + context 'when FIPS mode is enabled', :fips_mode do + it 'calls PBKDF2 digest and not the default Devise encryptor' do + expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512) + .to receive(:digest).at_least(:once).and_call_original + expect(Devise::Encryptor).not_to receive(:digest) + + user.password = password + end + + it 'saves the password in PBKDF2 format' do + user.password = password + user.save! + + expect(compare_pbkdf2_password(user, password)).to eq(true) + expect { compare_bcrypt_password(user, password) }.to raise_error(::BCrypt::Errors::InvalidHash) + end + end + + it 'calls default Devise encryptor and not the PBKDF2 encryptor' do + expect(Devise::Encryptor).to receive(:digest).at_least(:once).and_call_original + expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).not_to receive(:digest) + + user.password = password + end + + it 'saves the password in BCrypt format' do + user.password = password + user.save! + + expect { compare_pbkdf2_password(user, password) } + .to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + expect(compare_bcrypt_password(user, password)).to eq(true) + end + end +end diff --git a/spec/models/concerns/file_store_mounter_spec.rb b/spec/models/concerns/file_store_mounter_spec.rb new file mode 100644 index 00000000000..459f3d35668 --- /dev/null +++ b/spec/models/concerns/file_store_mounter_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe FileStoreMounter, :aggregate_failures do + let(:uploader_class) do + Class.new do + def object_store + :object_store + end + end + end + + let(:test_class) { Class.new { include(FileStoreMounter) } } + + let(:uploader_instance) { uploader_class.new } + + describe '.mount_file_store_uploader' do + using RSpec::Parameterized::TableSyntax + + subject(:mount_file_store_uploader) do + test_class.mount_file_store_uploader uploader_class, skip_store_file: skip_store_file, file_field: file_field + end + + where(:skip_store_file, :file_field) do + true | :file + false | :file + false | :signed_file + true | :signed_file + end + + with_them do + it 'defines instance methods and registers a callback' do + expect(test_class).to receive(:mount_uploader).with(file_field, uploader_class) + expect(test_class).to receive(:define_method).with("update_#{file_field}_store") + expect(test_class).to receive(:define_method).with("store_#{file_field}_now!") + + if skip_store_file + expect(test_class).to receive(:skip_callback).with(:save, :after, "store_#{file_field}!".to_sym) + expect(test_class).not_to receive(:after_save) + else + expect(test_class).not_to receive(:skip_callback) + expect(test_class) + .to receive(:after_save) + .with("update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym) + end + + mount_file_store_uploader + end + end + + context 'with an unknown file_field' do + let(:skip_store_file) { false } + let(:file_field) { 'unknown' } + + it do + expect { mount_file_store_uploader }.to raise_error(ArgumentError, 'file_field not allowed: unknown') + end + end + end + + context 'with an instance' do + let(:instance) { test_class.new } + + before do + allow(test_class).to receive(:mount_uploader) + allow(test_class).to receive(:after_save) + test_class.mount_file_store_uploader uploader_class + end + + describe '#update_file_store' do + subject(:update_file_store) { instance.update_file_store } + + it 'calls update column' do + expect(instance).to receive(:file).and_return(uploader_instance) + expect(instance).to receive(:update_column).with('file_store', :object_store) + + update_file_store + end + end + + describe '#store_file_now!' do + subject(:store_file_now!) { instance.store_file_now! } + + it 'calls the dynamic functions' do + expect(instance).to receive(:store_file!) + expect(instance).to receive(:update_file_store) + + store_file_now! + end + end + end +end diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb index a6a0e074589..b2ea7b22dea 100644 --- a/spec/models/concerns/has_user_type_spec.rb +++ b/spec/models/concerns/has_user_type_spec.rb @@ -88,5 +88,47 @@ RSpec.describe User do end end end + + describe '#redacted_name(viewing_user)' do + let_it_be(:viewing_user) { human } + + subject { observed_user.redacted_name(viewing_user) } + + context 'when user is not a project bot' do + let(:observed_user) { support_bot } + + it { is_expected.to eq(support_bot.name) } + end + + context 'when user is a project_bot' do + let(:observed_user) { project_bot } + + context 'when groups are present and user can :read_group' do + let_it_be(:group) { create(:group) } + + before do + group.add_developer(observed_user) + group.add_developer(viewing_user) + end + + it { is_expected.to eq(observed_user.name) } + end + + context 'when user can :read_project' do + let_it_be(:project) { create(:project) } + + before do + project.add_developer(observed_user) + project.add_developer(viewing_user) + end + + it { is_expected.to eq(observed_user.name) } + end + + context 'when requester does not have permissions to read project_bot name' do + it { is_expected.to eq('****') } + end + end + end end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 8842a36f40a..e553e34ab51 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -337,31 +337,6 @@ RSpec.describe Issuable do it { expect(MergeRequest.to_ability_name).to eq("merge_request") } end - describe "#today?" do - it "returns true when created today" do - # Avoid timezone differences and just return exactly what we want - allow(Date).to receive(:today).and_return(issue.created_at.to_date) - expect(issue.today?).to be_truthy - end - - it "returns false when not created today" do - allow(Date).to receive(:today).and_return(Date.yesterday) - expect(issue.today?).to be_falsey - end - end - - describe "#new?" do - it "returns false when created 30 hours ago" do - allow(issue).to receive(:created_at).and_return(Time.current - 30.hours) - expect(issue.new?).to be_falsey - end - - it "returns true when created 20 hours ago" do - allow(issue).to receive(:created_at).and_return(Time.current - 20.hours) - expect(issue.new?).to be_truthy - end - end - describe "#sort_by_attribute" do let(:project) { create(:project) } @@ -1055,6 +1030,22 @@ RSpec.describe Issuable do end end + describe '#supports_confidentiality?' do + where(:issuable_type, :supports_confidentiality) do + :issue | true + :incident | true + :merge_request | false + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.supports_confidentiality? } + + it { is_expected.to eq(supports_confidentiality) } + end + end + describe '#severity' do subject { issuable.severity } diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb index 3e42a3504ac..98b44a2eec2 100644 --- a/spec/models/concerns/pg_full_text_searchable_spec.rb +++ b/spec/models/concerns/pg_full_text_searchable_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe PgFullTextSearchable do - let(:project) { create(:project) } + let(:project) { build(:project) } let(:model_class) do Class.new(ActiveRecord::Base) do @@ -76,7 +76,7 @@ RSpec.describe PgFullTextSearchable do end describe '.pg_full_text_search' do - let(:english) { model_class.create!(project: project, title: 'title', description: 'something english') } + let(:english) { model_class.create!(project: project, title: 'title', description: 'something description english') } let(:with_accent) { model_class.create!(project: project, title: 'Jürgen', description: 'Ærøskøbing') } let(:japanese) { model_class.create!(project: project, title: '日本語 title', description: 'another english description') } @@ -90,8 +90,19 @@ RSpec.describe PgFullTextSearchable do expect(model_class.pg_full_text_search('title english')).to contain_exactly(english, japanese) end + it 'searches specified columns only' do + matching_object = model_class.create!(project: project, title: 'english', description: 'some description') + matching_object.update_search_data! + + expect(model_class.pg_full_text_search('english', matched_columns: %w(title))).to contain_exactly(matching_object) + end + + it 'uses prefix matching' do + expect(model_class.pg_full_text_search('tit eng')).to contain_exactly(english, japanese) + end + it 'searches for exact term with quotes' do - expect(model_class.pg_full_text_search('"something english"')).to contain_exactly(english) + expect(model_class.pg_full_text_search('"description english"')).to contain_exactly(english) end it 'ignores accents' do @@ -113,6 +124,27 @@ RSpec.describe PgFullTextSearchable do expect(model_class.pg_full_text_search('gopher://gitlab.com/gitlab-org/gitlab')).to contain_exactly(with_url) end end + + context 'when search term is a path with underscores' do + let(:path) { 'browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb' } + let(:with_underscore) { model_class.create!(project: project, title: 'issue with path', description: "some #{path} other text") } + + it 'allows searching by the path' do + with_underscore.update_search_data! + + expect(model_class.pg_full_text_search(path)).to contain_exactly(with_underscore) + end + end + + context 'when text has numbers preceded by a dash' do + let(:with_dash) { model_class.create!(project: project, title: 'issue with dash', description: 'ABC-123') } + + it 'allows searching by numbers only' do + with_dash.update_search_data! + + expect(model_class.pg_full_text_search('123')).to contain_exactly(with_dash) + end + end end describe '#update_search_data!' do diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 89f34834aa4..f168bedc8eb 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -8,7 +8,7 @@ RSpec.describe ProjectFeaturesCompatibility do let(:features) do features_enabled + %w( repository pages operations container_registry package_registry environments feature_flags releases - monitor + monitor infrastructure ) end diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb index 790e6936803..fca94b50fee 100644 --- a/spec/models/concerns/sha_attribute_spec.rb +++ b/spec/models/concerns/sha_attribute_spec.rb @@ -72,9 +72,10 @@ RSpec.describe ShaAttribute do end it 'validates column type' do - if expected_error == :no_error + case expected_error + when :no_error expect { load_schema! }.not_to raise_error - elsif expected_error == :sha_mismatch_error + when :sha_mismatch_error expect { load_schema! }.to raise_error( described_class::ShaAttributeTypeMismatchError, /sha_attribute.*#{column_name}.* should be a :binary column/ @@ -89,9 +90,10 @@ RSpec.describe ShaAttribute do end it 'validates column type' do - if expected_error == :no_error + case expected_error + when :no_error expect { load_schema! }.not_to raise_error - elsif expected_error == :sha_mismatch_error + when :sha_mismatch_error expect { load_schema! }.to raise_error( described_class::Sha256AttributeTypeMismatchError, /sha256_attribute.*#{column_name}.* should be a :binary column/ diff --git a/spec/models/concerns/subquery_spec.rb b/spec/models/concerns/subquery_spec.rb new file mode 100644 index 00000000000..95487fd8c2d --- /dev/null +++ b/spec/models/concerns/subquery_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Subquery do + let_it_be(:projects) { create_list :project, 3 } + let_it_be(:project_ids) { projects.map(&:id) } + let(:relation) { Project.where(id: projects) } + + subject { relation.subquery(:id) } + + shared_examples 'subquery as array values' do + specify { is_expected.to match_array project_ids } + specify { expect { subject }.not_to make_queries } + end + + shared_examples 'subquery as relation' do + it { is_expected.to be_a ActiveRecord::Relation } + specify { expect { subject.load }.to make_queries } + end + + shared_context 'when array size exceeds max_limit' do + subject { relation.subquery(:id, max_limit: 1) } + end + + context 'when relation is not loaded' do + it_behaves_like 'subquery as relation' + + context 'when array size exceeds max_limit' do + include_context 'when array size exceeds max_limit' + + it_behaves_like 'subquery as relation' + end + end + + context 'when relation is loaded' do + before do + relation.load + end + + it_behaves_like 'subquery as array values' + + context 'when array size exceeds max_limit' do + include_context 'when array size exceeds max_limit' + + it_behaves_like 'subquery as relation' + end + + context 'with a select' do + let(:relation) { Project.where(id: projects).select(:id) } + + it_behaves_like 'subquery as array values' + + context 'and querying with an unloaded column' do + subject { relation.subquery(:namespace_id) } + + it { expect { subject }.to raise_error(ActiveModel::MissingAttributeError) } + end + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index e8db83b7144..e53fdafe3b1 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -214,19 +214,15 @@ end RSpec.describe Ci::Build, 'TokenAuthenticatable' do let(:token_field) { :token } - let(:build) { FactoryBot.build(:ci_build) } + let(:build) { FactoryBot.build(:ci_build, :created) } it_behaves_like 'TokenAuthenticatable' describe 'generating new token' do context 'token is not generated yet' do describe 'token field accessor' do - it 'makes it possible to access token' do - expect(build.token).to be_nil - - build.save! - - expect(build.token).to be_present + it 'does not generate a token when saving a build' do + expect { build.save! }.not_to change(build, :token).from(nil) end end |