Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/concerns')
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb6
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb1
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb316
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb21
-rw-r--r--spec/models/concerns/encrypted_user_password_spec.rb138
-rw-r--r--spec/models/concerns/file_store_mounter_spec.rb93
-rw-r--r--spec/models/concerns/has_user_type_spec.rb42
-rw-r--r--spec/models/concerns/issuable_spec.rb41
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb38
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb2
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb10
-rw-r--r--spec/models/concerns/subquery_spec.rb61
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb10
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