mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-23 00:45:28 +00:00
269 lines
8.6 KiB
Ruby
269 lines
8.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe CustomUniquenessValidator, feature_category: :shared do
|
|
before_all do
|
|
ApplicationRecord.connection.execute(
|
|
<<~SQL
|
|
CREATE TABLE IF NOT EXISTS _test_custom_uniqueness (
|
|
id smallint PRIMARY KEY,
|
|
name VARCHAR(500),
|
|
number smallint,
|
|
second_number smallint
|
|
);
|
|
SQL
|
|
)
|
|
end
|
|
|
|
let_it_be(:existing_record_name) { 'Some Name' }
|
|
|
|
shared_examples 'custom uniqueness validator' do
|
|
subject { test_model.new(**new_record_attributes) }
|
|
|
|
before do
|
|
test_model.create!(id: 1, **existing_record_attributes)
|
|
end
|
|
|
|
it 'uses TRIM(BOTH FROM lower()) to validate' do
|
|
new_record = test_model.new(**valid_new_attributes)
|
|
|
|
expect do
|
|
new_record.valid?
|
|
end.to make_queries_matching(
|
|
/TRIM\(BOTH FROM lower\(_test_custom_uniqueness.name\)\) = TRIM\(BOTH FROM lower\('/
|
|
)
|
|
end
|
|
|
|
it 'adds an error to the record' do
|
|
new_record = test_model.new(**existing_record_attributes)
|
|
|
|
new_record.valid?
|
|
|
|
expect(new_record.errors.full_messages).to include('Name has already been taken')
|
|
end
|
|
|
|
context 'when new record has leading whitespace' do
|
|
let(:new_record_name) { " #{existing_record_name}" }
|
|
|
|
it { is_expected.to be_invalid }
|
|
end
|
|
|
|
context 'when new record has trailing whitespace' do
|
|
let(:new_record_name) { "#{existing_record_name} " }
|
|
|
|
it { is_expected.to be_invalid }
|
|
end
|
|
|
|
context 'when new record does not match casing' do
|
|
let(:new_record_name) { existing_record_name.upcase }
|
|
|
|
it { is_expected.to be_invalid }
|
|
end
|
|
|
|
context 'when new record name does not match' do
|
|
let(:new_record_name) { "#{existing_record_name} something else" }
|
|
|
|
it { is_expected.to be_valid }
|
|
end
|
|
|
|
context 'when updating a record' do
|
|
let(:existing_record) { test_model.first }
|
|
|
|
subject(:updated) { existing_record.update(**new_record_attributes) } # rubocop:disable Rails/SaveBang -- Checking the return value in specs
|
|
|
|
context 'when new value is exactly the same' do
|
|
let(:new_record_name) { existing_record_name }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'when new value is the same in a different casing' do
|
|
let(:new_record_name) { existing_record_name.upcase }
|
|
|
|
it { is_expected.to be_truthy }
|
|
|
|
it 'does update the record to the new casing' do
|
|
expect do
|
|
updated
|
|
end.to change { existing_record.reload.name }.from(existing_record_name).to(existing_record_name.upcase)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when validation is not scoped to another column' do
|
|
let_it_be(:test_model) do
|
|
Class.new(ApplicationRecord) do
|
|
self.table_name = :_test_custom_uniqueness
|
|
|
|
def self.name
|
|
'TestCustomUniqueness'
|
|
end
|
|
|
|
validates :name, custom_uniqueness: { unique_sql: 'TRIM(BOTH FROM lower(?))' }
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'custom uniqueness validator' do
|
|
let(:existing_record_attributes) { { name: existing_record_name } }
|
|
let(:valid_new_attributes) { { name: "#{existing_record_name} something else" } }
|
|
let(:new_record_attributes) { { name: new_record_name } }
|
|
end
|
|
end
|
|
|
|
context 'when validation is scoped to another column' do
|
|
let_it_be(:test_model) do
|
|
Class.new(ApplicationRecord) do
|
|
self.table_name = :_test_custom_uniqueness
|
|
|
|
def self.name
|
|
'TestCustomUniqueness'
|
|
end
|
|
|
|
validates :name, custom_uniqueness: { unique_sql: 'TRIM(BOTH FROM lower(?))', scope: :number }
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'custom uniqueness validator' do
|
|
let(:existing_record_attributes) { { name: existing_record_name, number: 1 } }
|
|
let(:valid_new_attributes) { { name: "#{existing_record_name} something else", number: 2 } }
|
|
let(:new_record_attributes) { { name: new_record_name, number: 1 } }
|
|
end
|
|
|
|
context 'when name matches but scope value does not' do
|
|
it 'does not invalidate the record' do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1)
|
|
new_record = test_model.new(name: existing_record_name, number: 2)
|
|
|
|
expect(new_record).to be_valid
|
|
end
|
|
end
|
|
|
|
context 'when name and scope value matches an existing record' do
|
|
it 'adds an error to the record' do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1)
|
|
new_record = test_model.new(name: existing_record_name, number: 1)
|
|
|
|
new_record.valid?
|
|
|
|
expect(new_record.errors.full_messages).to include('Name has already been taken')
|
|
end
|
|
|
|
context 'when name and scope value matches an existing record' do
|
|
it 'adds an error to the record' do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1)
|
|
new_record = test_model.new(name: existing_record_name, number: 1)
|
|
|
|
new_record.valid?
|
|
|
|
expect(new_record.errors.full_messages).to include('Name has already been taken')
|
|
end
|
|
end
|
|
|
|
context 'when updating an existing record' do
|
|
before_all do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1)
|
|
end
|
|
|
|
context 'when changing the attribute value' do
|
|
it 'adds an error to the record' do
|
|
record = test_model.create!(id: 2, name: "#{existing_record_name} different", number: 1)
|
|
record.name = existing_record_name
|
|
|
|
expect(record).to be_invalid
|
|
end
|
|
end
|
|
|
|
context 'when changing the scope value' do
|
|
it 'adds an error to the record' do
|
|
record = test_model.create!(id: 2, name: existing_record_name, number: 2)
|
|
record.number = 1
|
|
|
|
expect(record).to be_invalid
|
|
end
|
|
end
|
|
|
|
context 'when making valid changes to the attribute and scope values' do
|
|
it 'does not invalidate the record' do
|
|
record = test_model.create!(id: 2, name: "#{existing_record_name} different", number: 1)
|
|
record.name = existing_record_name
|
|
record.number = 2
|
|
|
|
expect(record).to be_valid
|
|
end
|
|
end
|
|
|
|
context 'when not changing the validated attributes or scoped values' do
|
|
it 'does not issue queries to the DB' do
|
|
record = test_model.create!(id: 2, name: "#{existing_record_name} different", number: 1)
|
|
record.id = 100
|
|
|
|
expect do
|
|
record.valid?
|
|
end.not_to make_queries
|
|
|
|
expect(record).to be_valid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when validation is scoped to multiple columns' do
|
|
let_it_be(:test_model) do
|
|
Class.new(ApplicationRecord) do
|
|
self.table_name = :_test_custom_uniqueness
|
|
|
|
def self.name
|
|
'TestCustomUniqueness'
|
|
end
|
|
|
|
validates :name, custom_uniqueness: { unique_sql: 'TRIM(BOTH FROM lower(?))', scope: [:number, :second_number] }
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'custom uniqueness validator' do
|
|
let(:existing_record_attributes) { { name: existing_record_name, number: 1, second_number: 101 } }
|
|
let(:valid_new_attributes) { { name: "#{existing_record_name} something else", number: 2, second_number: 102 } }
|
|
let(:new_record_attributes) { { name: new_record_name, number: 1, second_number: 101 } }
|
|
end
|
|
|
|
context 'when name matches but scope value does not' do
|
|
it 'does not invalidate the record' do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1, second_number: 101)
|
|
new_record = test_model.new(name: existing_record_name, number: 1, second_number: 102)
|
|
|
|
expect(new_record).to be_valid
|
|
end
|
|
end
|
|
|
|
context 'when name and scope values matches an existing record' do
|
|
it 'adds an error to the record' do
|
|
test_model.create!(id: 1, name: existing_record_name, number: 1, second_number: 101)
|
|
new_record = test_model.new(name: existing_record_name, number: 1, second_number: 101)
|
|
|
|
new_record.valid?
|
|
|
|
expect(new_record.errors.full_messages).to include('Name has already been taken')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when validation definition is missing the `unique_sql key`' do
|
|
it 'raises an ArgumentError' do
|
|
expect do
|
|
Class.new(ApplicationRecord) do
|
|
self.table_name = :_test_custom_uniqueness
|
|
|
|
def self.name
|
|
'TestCustomUniqueness'
|
|
end
|
|
|
|
validates :name, custom_uniqueness: true
|
|
end
|
|
end.to raise_error(ArgumentError, '`unique_sql` option must be provided to the `custom_uniqueness` validator')
|
|
end
|
|
end
|
|
end
|