shoulda-matchers 2.8.0 → 3.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.hound_config/ruby.yml +7 -0
- data/.travis.yml +11 -54
- data/Appraisals +45 -100
- data/CONTRIBUTING.md +51 -7
- data/Gemfile +7 -19
- data/Gemfile.lock +60 -134
- data/Guardfile +5 -0
- data/NEWS.md +203 -0
- data/README.md +95 -50
- data/Rakefile +1 -0
- data/doc_config/yard/templates/default/layout/html/setup.rb +1 -1
- data/gemfiles/4.0.0.gemfile +10 -7
- data/gemfiles/4.0.0.gemfile.lock +103 -79
- data/gemfiles/4.0.1.gemfile +10 -7
- data/gemfiles/4.0.1.gemfile.lock +109 -83
- data/gemfiles/4.1.gemfile +10 -7
- data/gemfiles/4.1.gemfile.lock +109 -85
- data/gemfiles/4.2.gemfile +10 -9
- data/gemfiles/4.2.gemfile.lock +86 -78
- data/lib/shoulda/matchers.rb +13 -18
- data/lib/shoulda/matchers/action_controller.rb +4 -1
- data/lib/shoulda/matchers/action_controller/flash_store.rb +95 -0
- data/lib/shoulda/matchers/action_controller/{strong_parameters_matcher.rb → permit_matcher.rb} +147 -30
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +5 -1
- data/lib/shoulda/matchers/action_controller/route_params.rb +15 -6
- data/lib/shoulda/matchers/action_controller/session_store.rb +34 -0
- data/lib/shoulda/matchers/action_controller/set_flash_matcher.rb +30 -136
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +28 -109
- data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +103 -0
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +1 -12
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +79 -10
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +10 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +21 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +24 -0
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -5
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +29 -10
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +27 -10
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -12
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +56 -20
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +3 -11
- data/lib/shoulda/matchers/active_model/validation_message_finder.rb +65 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +40 -6
- data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +21 -7
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +11 -40
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -1
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +2 -6
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +137 -22
- data/lib/shoulda/matchers/configuration.rb +20 -0
- data/lib/shoulda/matchers/doublespeak.rb +11 -1
- data/lib/shoulda/matchers/doublespeak/double.rb +29 -11
- data/lib/shoulda/matchers/doublespeak/double_collection.rb +4 -3
- data/lib/shoulda/matchers/doublespeak/method_call.rb +35 -0
- data/lib/shoulda/matchers/doublespeak/object_double.rb +7 -2
- data/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +4 -3
- data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +3 -3
- data/lib/shoulda/matchers/doublespeak/world.rb +21 -1
- data/lib/shoulda/matchers/integrations.rb +43 -0
- data/lib/shoulda/matchers/integrations/configuration.rb +68 -0
- data/lib/shoulda/matchers/integrations/configuration_error.rb +9 -0
- data/lib/shoulda/matchers/integrations/inclusion.rb +20 -0
- data/lib/shoulda/matchers/integrations/libraries.rb +15 -0
- data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +31 -0
- data/lib/shoulda/matchers/integrations/libraries/active_model.rb +26 -0
- data/lib/shoulda/matchers/integrations/libraries/active_record.rb +26 -0
- data/lib/shoulda/matchers/integrations/libraries/missing_library.rb +19 -0
- data/lib/shoulda/matchers/integrations/libraries/rails.rb +30 -0
- data/lib/shoulda/matchers/integrations/rails.rb +12 -0
- data/lib/shoulda/matchers/integrations/registry.rb +28 -0
- data/lib/shoulda/matchers/integrations/test_frameworks.rb +16 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb +37 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb +36 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb +37 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb +40 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/rspec.rb +29 -0
- data/lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb +36 -0
- data/lib/shoulda/matchers/rails_shim.rb +0 -40
- data/lib/shoulda/matchers/version.rb +1 -1
- data/script/SUPPORTED_VERSIONS +1 -1
- data/script/update_gems_in_all_appraisals +14 -0
- data/shoulda-matchers.gemspec +2 -2
- data/spec/acceptance/active_model_integration_spec.rb +4 -1
- data/spec/acceptance/independent_matchers_spec.rb +6 -6
- data/spec/acceptance/multiple_libraries_integration_spec.rb +52 -0
- data/spec/acceptance/rails_integration_spec.rb +15 -5
- data/spec/acceptance_spec_helper.rb +8 -0
- data/spec/doublespeak_spec_helper.rb +14 -0
- data/spec/support/acceptance/adds_shoulda_matchers_to_project.rb +110 -0
- data/spec/support/acceptance/helpers.rb +2 -0
- data/spec/support/acceptance/helpers/base_helpers.rb +6 -1
- data/spec/support/acceptance/helpers/command_helpers.rb +6 -2
- data/spec/support/acceptance/helpers/minitest_helpers.rb +0 -8
- data/spec/support/acceptance/helpers/n_unit_helpers.rb +25 -0
- data/spec/support/acceptance/helpers/rspec_helpers.rb +2 -0
- data/spec/support/acceptance/helpers/step_helpers.rb +13 -19
- data/spec/support/acceptance/matchers/have_output.rb +1 -1
- data/spec/support/tests/bundle.rb +1 -1
- data/spec/support/tests/command_runner.rb +25 -13
- data/spec/support/tests/current_bundle.rb +47 -0
- data/spec/support/tests/database.rb +28 -0
- data/spec/support/tests/database_adapters/postgresql.rb +25 -0
- data/spec/support/tests/database_adapters/sqlite3.rb +26 -0
- data/spec/support/tests/database_configuration.rb +33 -0
- data/spec/support/tests/database_configuration_registry.rb +28 -0
- data/spec/support/tests/filesystem.rb +25 -2
- data/spec/support/unit/helpers/active_record_versions.rb +12 -0
- data/spec/support/unit/helpers/class_builder.rb +6 -2
- data/spec/support/unit/helpers/column_type_helpers.rb +26 -0
- data/spec/support/unit/helpers/controller_builder.rb +0 -28
- data/spec/support/unit/helpers/database_helpers.rb +18 -0
- data/spec/support/unit/helpers/model_builder.rb +38 -6
- data/spec/support/unit/helpers/rails_versions.rb +2 -2
- data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +9 -8
- data/spec/support/unit/matchers/fail_with_message_matcher.rb +1 -1
- data/spec/support/unit/rails_application.rb +29 -13
- data/spec/support/unit/record_validating_confirmation_builder.rb +1 -2
- data/spec/support/unit/shared_examples/set_session_or_flash.rb +355 -0
- data/spec/unit/shoulda/matchers/action_controller/permit_matcher_spec.rb +433 -0
- data/spec/unit/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +1 -5
- data/spec/unit/shoulda/matchers/action_controller/route_matcher_spec.rb +37 -0
- data/spec/unit/shoulda/matchers/action_controller/set_flash_matcher_spec.rb +23 -147
- data/spec/unit/shoulda/matchers/action_controller/set_session_matcher_spec.rb +8 -285
- data/spec/unit/shoulda/matchers/action_controller/set_session_or_flash_matcher_spec.rb +562 -0
- data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +81 -14
- data/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +16 -8
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +101 -9
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +39 -1
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +39 -1
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +39 -0
- data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +0 -17
- data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +0 -17
- data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +0 -17
- data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +838 -271
- data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +0 -19
- data/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +93 -0
- data/spec/unit/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +3 -3
- data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +25 -0
- data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +905 -0
- data/spec/unit/shoulda/matchers/doublespeak/double_collection_spec.rb +17 -11
- data/spec/unit/shoulda/matchers/doublespeak/double_implementation_registry_spec.rb +1 -1
- data/spec/unit/shoulda/matchers/doublespeak/double_spec.rb +144 -43
- data/spec/unit/shoulda/matchers/doublespeak/object_double_spec.rb +1 -1
- data/spec/unit/shoulda/matchers/doublespeak/proxy_implementation_spec.rb +36 -11
- data/spec/unit/shoulda/matchers/doublespeak/stub_implementation_spec.rb +29 -16
- data/spec/unit/shoulda/matchers/doublespeak/world_spec.rb +8 -5
- data/spec/unit/shoulda/matchers/doublespeak_spec.rb +1 -1
- data/spec/unit_spec_helper.rb +15 -14
- data/spec/warnings_spy.rb +1 -1
- metadata +68 -29
- data/docs.watchr +0 -5
- data/gemfiles/3.0.gemfile +0 -26
- data/gemfiles/3.0.gemfile.lock +0 -173
- data/gemfiles/3.1.gemfile +0 -32
- data/gemfiles/3.1.gemfile.lock +0 -212
- data/gemfiles/3.1_1.9.2.gemfile +0 -32
- data/gemfiles/3.1_1.9.2.gemfile.lock +0 -212
- data/gemfiles/3.2.gemfile +0 -33
- data/gemfiles/3.2.gemfile.lock +0 -212
- data/gemfiles/3.2_1.9.2.gemfile +0 -31
- data/gemfiles/3.2_1.9.2.gemfile.lock +0 -207
- data/lib/shoulda/matchers/assertion_error.rb +0 -27
- data/lib/shoulda/matchers/doublespeak/structs.rb +0 -10
- data/lib/shoulda/matchers/integrations/nunit_test_case_detection.rb +0 -39
- data/lib/shoulda/matchers/integrations/rspec.rb +0 -19
- data/lib/shoulda/matchers/integrations/test_unit.rb +0 -34
- data/spec/unit/shoulda/matchers/action_controller/strong_parameters_matcher_spec.rb +0 -331
- data/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +0 -564
@@ -163,25 +163,6 @@ describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher, type: :model
|
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
166
|
-
context 'when the attribute being tested intercepts the blank value we set on it (issue #479)' do
|
167
|
-
context 'for a non-collection attribute' do
|
168
|
-
it 'does not raise an error' do
|
169
|
-
record = define_model :example, attr: :string do
|
170
|
-
validates :attr, presence: true
|
171
|
-
|
172
|
-
def attr=(value)
|
173
|
-
value = '' if value.nil?
|
174
|
-
super(value)
|
175
|
-
end
|
176
|
-
end.new
|
177
|
-
|
178
|
-
expect do
|
179
|
-
expect(record).to validate_presence_of(:attr)
|
180
|
-
end.not_to raise_error
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
166
|
def matcher
|
186
167
|
validate_presence_of(:attr)
|
187
168
|
end
|
@@ -886,6 +886,99 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
|
|
886
886
|
end.to fail_with_message_including('missing columns: person_id, relative_id')
|
887
887
|
end
|
888
888
|
|
889
|
+
context 'when the association is declared with a :join_table option' do
|
890
|
+
it 'accepts when testing with the same :join_table option' do
|
891
|
+
join_table_name = 'people_and_their_families'
|
892
|
+
|
893
|
+
define_model :relative
|
894
|
+
|
895
|
+
define_model :person do
|
896
|
+
has_and_belongs_to_many(:relatives, join_table: join_table_name)
|
897
|
+
end
|
898
|
+
|
899
|
+
create_table(join_table_name, id: false) do |t|
|
900
|
+
t.references :person
|
901
|
+
t.references :relative
|
902
|
+
end
|
903
|
+
|
904
|
+
expect(Person.new).
|
905
|
+
to have_and_belong_to_many(:relatives).
|
906
|
+
join_table(join_table_name)
|
907
|
+
end
|
908
|
+
|
909
|
+
it 'accepts even when not explicitly testing with a :join_table option' do
|
910
|
+
join_table_name = 'people_and_their_families'
|
911
|
+
|
912
|
+
define_model :relative
|
913
|
+
|
914
|
+
define_model :person do
|
915
|
+
has_and_belongs_to_many(:relatives,
|
916
|
+
join_table: join_table_name
|
917
|
+
)
|
918
|
+
end
|
919
|
+
|
920
|
+
create_table(join_table_name, id: false) do |t|
|
921
|
+
t.references :person
|
922
|
+
t.references :relative
|
923
|
+
end
|
924
|
+
|
925
|
+
expect(Person.new).to have_and_belong_to_many(:relatives)
|
926
|
+
end
|
927
|
+
|
928
|
+
it 'rejects when testing with a different :join_table option' do
|
929
|
+
join_table_name = 'people_and_their_families'
|
930
|
+
|
931
|
+
define_model :relative
|
932
|
+
|
933
|
+
define_model :person do
|
934
|
+
has_and_belongs_to_many(
|
935
|
+
:relatives,
|
936
|
+
join_table: join_table_name
|
937
|
+
)
|
938
|
+
end
|
939
|
+
|
940
|
+
create_table(join_table_name, id: false) do |t|
|
941
|
+
t.references :person
|
942
|
+
t.references :relative
|
943
|
+
end
|
944
|
+
|
945
|
+
assertion = lambda do
|
946
|
+
expect(Person.new).
|
947
|
+
to have_and_belong_to_many(:relatives).
|
948
|
+
join_table('family_tree')
|
949
|
+
end
|
950
|
+
|
951
|
+
expect(&assertion).to fail_with_message_including(
|
952
|
+
"relatives should use 'family_tree' for :join_table option"
|
953
|
+
)
|
954
|
+
end
|
955
|
+
end
|
956
|
+
|
957
|
+
context 'when the association is not declared with a :join_table option' do
|
958
|
+
it 'rejects when testing with a :join_table option' do
|
959
|
+
define_model :relative
|
960
|
+
|
961
|
+
define_model :person do
|
962
|
+
has_and_belongs_to_many(:relatives)
|
963
|
+
end
|
964
|
+
|
965
|
+
create_table('people_relatives', id: false) do |t|
|
966
|
+
t.references :person
|
967
|
+
t.references :relative
|
968
|
+
end
|
969
|
+
|
970
|
+
assertion = lambda do
|
971
|
+
expect(Person.new).
|
972
|
+
to have_and_belong_to_many(:relatives).
|
973
|
+
join_table('family_tree')
|
974
|
+
end
|
975
|
+
|
976
|
+
expect(&assertion).to fail_with_message_including(
|
977
|
+
"relatives should use 'family_tree' for :join_table option"
|
978
|
+
)
|
979
|
+
end
|
980
|
+
end
|
981
|
+
|
889
982
|
context 'using a custom foreign key' do
|
890
983
|
it 'rejects an association with a join table with incorrect columns' do
|
891
984
|
define_model :relative
|
@@ -54,7 +54,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
describe '#
|
57
|
+
describe '#join_table_name' do
|
58
58
|
context 'when the association was defined with a :join_table option' do
|
59
59
|
it 'returns the value of the option' do
|
60
60
|
create_table :foos, id: false do |t|
|
@@ -68,7 +68,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
|
|
68
68
|
delegate_reflection = country_model.reflect_on_association(:people)
|
69
69
|
reflection = described_class.new(delegate_reflection)
|
70
70
|
|
71
|
-
expect(reflection.
|
71
|
+
expect(reflection.join_table_name).to eq 'foos'
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
@@ -81,7 +81,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
|
|
81
81
|
delegate_reflection = country_model.reflect_on_association(:people)
|
82
82
|
reflection = described_class.new(delegate_reflection)
|
83
83
|
|
84
|
-
expect(reflection.
|
84
|
+
expect(reflection.join_table_name).to eq 'countries_people'
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
@@ -2,6 +2,31 @@ require "unit_spec_helper"
|
|
2
2
|
|
3
3
|
describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
4
4
|
if active_record_supports_enum?
|
5
|
+
context 'if the attribute is given in plural form accidentally' do
|
6
|
+
it 'rejects' do
|
7
|
+
record = record_with_array_values
|
8
|
+
plural_enum_attribute = enum_attribute.to_s.pluralize
|
9
|
+
message = "Expected #{record.class} to define :#{plural_enum_attribute} as an enum"
|
10
|
+
assertion = -> {
|
11
|
+
expect(record).to define_enum_for(plural_enum_attribute)
|
12
|
+
}
|
13
|
+
expect(&assertion).to fail_with_message(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'if a method to hold enum values exists on the model but was not created via the enum macro' do
|
18
|
+
it 'rejects' do
|
19
|
+
model = define_model :example do
|
20
|
+
def self.statuses; end
|
21
|
+
end
|
22
|
+
message = "Expected #{model} to define :statuses as an enum"
|
23
|
+
assertion = -> {
|
24
|
+
expect(model.new).to define_enum_for(:statuses)
|
25
|
+
}
|
26
|
+
expect(&assertion).to fail_with_message(message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
5
30
|
describe "with only the attribute name specified" do
|
6
31
|
it "accepts a record where the attribute is defined as an enum" do
|
7
32
|
expect(record_with_array_values).to define_enum_for(enum_attribute)
|
@@ -0,0 +1,905 @@
|
|
1
|
+
require 'unit_spec_helper'
|
2
|
+
|
3
|
+
describe Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher, type: :model do
|
4
|
+
shared_context 'it supports scoped attributes of a certain type' do |options = {}|
|
5
|
+
column_type = options.fetch(:column_type)
|
6
|
+
value_type = options.fetch(:value_type, column_type)
|
7
|
+
array = options.fetch(:array, false)
|
8
|
+
|
9
|
+
context 'when the correct scope is specified' do
|
10
|
+
context 'when the subject is a new record' do
|
11
|
+
it 'accepts' do
|
12
|
+
record = build_record_validating_uniqueness(
|
13
|
+
scopes: [
|
14
|
+
build_attribute(name: :scope1),
|
15
|
+
{ name: :scope2 }
|
16
|
+
]
|
17
|
+
)
|
18
|
+
expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'still accepts if the scope is unset beforehand' do
|
22
|
+
record = build_record_validating_uniqueness(
|
23
|
+
scopes: [ build_attribute(name: :scope, value: nil) ]
|
24
|
+
)
|
25
|
+
|
26
|
+
expect(record).to validate_uniqueness.scoped_to(:scope)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when the subject is an existing record' do
|
31
|
+
it 'accepts' do
|
32
|
+
record = create_record_validating_uniqueness(
|
33
|
+
scopes: [
|
34
|
+
build_attribute(name: :scope1),
|
35
|
+
{ name: :scope2 }
|
36
|
+
]
|
37
|
+
)
|
38
|
+
|
39
|
+
expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'still accepts if the scope is unset beforehand' do
|
43
|
+
record = create_record_validating_uniqueness(
|
44
|
+
scopes: [ build_attribute(name: :scope, value: nil) ]
|
45
|
+
)
|
46
|
+
|
47
|
+
expect(record).to validate_uniqueness.scoped_to(:scope)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when more than one record exists that has the next version of the attribute's value" do
|
53
|
+
it 'accepts' do
|
54
|
+
value1 = dummy_value_for(value_type, array: array)
|
55
|
+
value2 = next_version_of(value1, value_type)
|
56
|
+
value3 = next_version_of(value2, value_type)
|
57
|
+
model = define_model_validating_uniqueness(
|
58
|
+
scopes: [ build_attribute(name: :scope) ]
|
59
|
+
)
|
60
|
+
create_record_from(model, scope: value2)
|
61
|
+
create_record_from(model, scope: value3)
|
62
|
+
record = build_record_from(model, scope: value1)
|
63
|
+
|
64
|
+
expect(record).to validate_uniqueness.scoped_to(:scope)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when too narrow of a scope is specified' do
|
69
|
+
it 'rejects' do
|
70
|
+
record = build_record_validating_uniqueness(
|
71
|
+
scopes: [
|
72
|
+
build_attribute(name: :scope1),
|
73
|
+
{ name: :scope2 }
|
74
|
+
],
|
75
|
+
)
|
76
|
+
expect(record).
|
77
|
+
not_to validate_uniqueness.
|
78
|
+
scoped_to(:scope1, :scope2, :other)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when too broad of a scope is specified' do
|
83
|
+
it 'rejects' do
|
84
|
+
record = build_record_validating_uniqueness(
|
85
|
+
scopes: [
|
86
|
+
build_attribute(name: :scope1),
|
87
|
+
{ name: :scope2 }
|
88
|
+
],
|
89
|
+
)
|
90
|
+
expect(record).
|
91
|
+
not_to validate_uniqueness.
|
92
|
+
scoped_to(:scope1)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when a different scope is specified' do
|
97
|
+
it 'rejects' do
|
98
|
+
record = build_record_validating_uniqueness(
|
99
|
+
scopes: [ build_attribute(name: :scope) ],
|
100
|
+
additional_attributes: [:other]
|
101
|
+
)
|
102
|
+
expect(record).
|
103
|
+
not_to validate_uniqueness.
|
104
|
+
scoped_to(:other)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when no scope is specified' do
|
109
|
+
it 'rejects' do
|
110
|
+
record = build_record_validating_uniqueness(
|
111
|
+
scopes: [ build_attribute(name: :scope) ]
|
112
|
+
)
|
113
|
+
expect(record).not_to validate_uniqueness
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'rejects if the scope is unset beforehand' do
|
117
|
+
record = build_record_validating_uniqueness(
|
118
|
+
scopes: [ build_attribute(name: :scope, value: nil) ]
|
119
|
+
)
|
120
|
+
expect(record).not_to validate_uniqueness
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'when a non-existent attribute is specified as a scope' do
|
125
|
+
it 'rejects' do
|
126
|
+
record = build_record_validating_uniqueness(
|
127
|
+
scopes: [ build_attribute(name: :scope) ]
|
128
|
+
)
|
129
|
+
expect(record).not_to validate_uniqueness.scoped_to(:non_existent)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
define_method(:build_attribute) do |attribute_options|
|
134
|
+
attribute_options.merge(
|
135
|
+
column_type: column_type,
|
136
|
+
value_type: value_type,
|
137
|
+
array: array
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'when the model does not have a uniqueness validation' do
|
143
|
+
it 'rejects' do
|
144
|
+
model = define_model(:example, attribute_name => :string) do |m|
|
145
|
+
m.attr_accessible attribute_name
|
146
|
+
end
|
147
|
+
|
148
|
+
model.create!(attr: 'value')
|
149
|
+
|
150
|
+
expect(model.new).not_to validate_uniqueness_of(attribute_name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'when the model has a uniqueness validation' do
|
155
|
+
context 'when the attribute has a character limit' do
|
156
|
+
it 'accepts' do
|
157
|
+
record = build_record_validating_uniqueness(
|
158
|
+
attribute_type: :string,
|
159
|
+
attribute_options: { limit: 1 }
|
160
|
+
)
|
161
|
+
|
162
|
+
expect(record).to validate_uniqueness
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'when the record is created beforehand' do
|
167
|
+
context 'when the subject is a new record' do
|
168
|
+
it 'accepts' do
|
169
|
+
create_record_validating_uniqueness
|
170
|
+
expect(new_record_validating_uniqueness).
|
171
|
+
to validate_uniqueness
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'when the subject is an existing record' do
|
176
|
+
it 'accepts' do
|
177
|
+
expect(existing_record_validating_uniqueness).to validate_uniqueness
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when the validation has no scope and a scope is specified' do
|
182
|
+
it 'rejects' do
|
183
|
+
model = define_model_validating_uniqueness(
|
184
|
+
additional_attributes: [:other]
|
185
|
+
)
|
186
|
+
create_record_from(model)
|
187
|
+
record = build_record_from(model)
|
188
|
+
expect(record).not_to validate_uniqueness.scoped_to(:other)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'when the record is not created beforehand' do
|
194
|
+
it 'creates the record automatically' do
|
195
|
+
model = define_model_validating_uniqueness
|
196
|
+
assertion = -> {
|
197
|
+
record = build_record_from(model)
|
198
|
+
expect(record).to validate_uniqueness
|
199
|
+
}
|
200
|
+
expect(&assertion).to change(model, :count).from(0).to(1)
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'and the table has required attributes other than the attribute being validated, set beforehand' do
|
204
|
+
it 'does not require the record to be persisted' do
|
205
|
+
options = {
|
206
|
+
additional_attributes: [
|
207
|
+
{ name: :required_attribute, options: { null: false } }
|
208
|
+
]
|
209
|
+
}
|
210
|
+
model = define_model_validating_uniqueness(options) do |m|
|
211
|
+
m.validates_presence_of :required_attribute
|
212
|
+
end
|
213
|
+
|
214
|
+
record = build_record_from(model, required_attribute: 'something')
|
215
|
+
expect(record).to validate_uniqueness
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
context 'and the validation has a custom message' do
|
221
|
+
context 'when no message is specified' do
|
222
|
+
it 'rejects' do
|
223
|
+
record = build_record_validating_uniqueness(
|
224
|
+
validation_options: { message: 'bad value' }
|
225
|
+
)
|
226
|
+
expect(record).not_to validate_uniqueness
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context 'given a string' do
|
231
|
+
context 'when the given and actual messages do not match' do
|
232
|
+
it 'rejects' do
|
233
|
+
record = build_record_validating_uniqueness(
|
234
|
+
validation_options: { message: 'bad value' }
|
235
|
+
)
|
236
|
+
expect(record).
|
237
|
+
not_to validate_uniqueness.
|
238
|
+
with_message('something else entirely')
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'when the given and actual messages match' do
|
243
|
+
it 'accepts' do
|
244
|
+
record = build_record_validating_uniqueness(
|
245
|
+
validation_options: { message: 'bad value' }
|
246
|
+
)
|
247
|
+
expect(record).
|
248
|
+
to validate_uniqueness.
|
249
|
+
with_message('bad value')
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context 'given a regex' do
|
255
|
+
context 'when the given and actual messages do not match' do
|
256
|
+
it 'rejects' do
|
257
|
+
record = build_record_validating_uniqueness(
|
258
|
+
validation_options: { message: 'Bad value' }
|
259
|
+
)
|
260
|
+
expect(record).
|
261
|
+
not_to validate_uniqueness.
|
262
|
+
with_message(/something else entirely/)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context 'when the given and actual messages match' do
|
267
|
+
it 'accepts' do
|
268
|
+
record = build_record_validating_uniqueness(
|
269
|
+
validation_options: { message: 'bad value' }
|
270
|
+
)
|
271
|
+
expect(record).
|
272
|
+
to validate_uniqueness.
|
273
|
+
with_message(/bad/)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context 'when the model has a scoped uniqueness validation' do
|
281
|
+
context 'when one of the scoped attributes is a string column' do
|
282
|
+
include_context 'it supports scoped attributes of a certain type',
|
283
|
+
column_type: :string
|
284
|
+
end
|
285
|
+
|
286
|
+
context 'when one of the scoped attributes is a boolean column' do
|
287
|
+
include_context 'it supports scoped attributes of a certain type',
|
288
|
+
column_type: :boolean
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'when there is more than one scoped attribute and all are boolean columns' do
|
292
|
+
it 'accepts when all of the scoped attributes are true' do
|
293
|
+
record = build_record_validating_uniqueness(
|
294
|
+
scopes: [
|
295
|
+
{ type: :boolean, name: :scope1, value: true },
|
296
|
+
{ type: :boolean, name: :scope2, value: true }
|
297
|
+
]
|
298
|
+
)
|
299
|
+
expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'accepts when all the scoped attributes are false' do
|
303
|
+
record = build_record_validating_uniqueness(
|
304
|
+
scopes: [
|
305
|
+
{ type: :boolean, name: :scope1, value: false },
|
306
|
+
{ type: :boolean, name: :scope2, value: false }
|
307
|
+
]
|
308
|
+
)
|
309
|
+
expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'accepts when one of the scoped attributes is true and the other is false' do
|
313
|
+
record = build_record_validating_uniqueness(
|
314
|
+
scopes: [
|
315
|
+
{ type: :boolean, name: :scope1, value: true },
|
316
|
+
{ type: :boolean, name: :scope2, value: false }
|
317
|
+
]
|
318
|
+
)
|
319
|
+
expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
context 'when one of the scoped attributes is an integer column' do
|
324
|
+
include_context 'it supports scoped attributes of a certain type',
|
325
|
+
column_type: :integer
|
326
|
+
|
327
|
+
if active_record_supports_enum?
|
328
|
+
context 'when one of the scoped attributes is an enum' do
|
329
|
+
it 'accepts' do
|
330
|
+
record = build_record_validating_scoped_uniqueness_with_enum(
|
331
|
+
enum_scope: :scope
|
332
|
+
)
|
333
|
+
expect(record).to validate_uniqueness.scoped_to(:scope)
|
334
|
+
end
|
335
|
+
|
336
|
+
context 'when too narrow of a scope is specified' do
|
337
|
+
it 'rejects' do
|
338
|
+
record = build_record_validating_scoped_uniqueness_with_enum(
|
339
|
+
enum_scope: :scope1,
|
340
|
+
additional_scopes: [:scope2],
|
341
|
+
additional_attributes: [:other]
|
342
|
+
)
|
343
|
+
expect(record).
|
344
|
+
not_to validate_uniqueness.
|
345
|
+
scoped_to(:scope1, :scope2, :other)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
context 'when too broad of a scope is specified' do
|
350
|
+
it 'rejects' do
|
351
|
+
record = build_record_validating_scoped_uniqueness_with_enum(
|
352
|
+
enum_scope: :scope1,
|
353
|
+
additional_scopes: [:scope2]
|
354
|
+
)
|
355
|
+
expect(record).
|
356
|
+
not_to validate_uniqueness.
|
357
|
+
scoped_to(:scope1)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
context 'when one of the scoped attributes is a date column' do
|
365
|
+
include_context 'it supports scoped attributes of a certain type',
|
366
|
+
column_type: :date
|
367
|
+
end
|
368
|
+
|
369
|
+
context 'when one of the scoped attributes is a datetime column (using DateTime)' do
|
370
|
+
include_context 'it supports scoped attributes of a certain type',
|
371
|
+
column_type: :datetime
|
372
|
+
end
|
373
|
+
|
374
|
+
context 'when one of the scoped attributes is a datetime column (using Time)' do
|
375
|
+
include_context 'it supports scoped attributes of a certain type',
|
376
|
+
column_type: :datetime,
|
377
|
+
value_type: :time
|
378
|
+
end
|
379
|
+
|
380
|
+
context 'when one of the scoped attributes is a text column' do
|
381
|
+
include_context 'it supports scoped attributes of a certain type',
|
382
|
+
column_type: :text
|
383
|
+
end
|
384
|
+
|
385
|
+
if database_supports_uuid_columns?
|
386
|
+
context 'when one of the scoped attributes is a UUID column' do
|
387
|
+
include_context 'it supports scoped attributes of a certain type',
|
388
|
+
column_type: :uuid
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
if database_supports_array_columns? && active_record_supports_array_columns?
|
393
|
+
context 'when one of the scoped attributes is a array-of-string column' do
|
394
|
+
include_examples 'it supports scoped attributes of a certain type',
|
395
|
+
column_type: :string,
|
396
|
+
array: true
|
397
|
+
end
|
398
|
+
|
399
|
+
context 'when one of the scoped attributes is an array-of-integer column' do
|
400
|
+
include_examples 'it supports scoped attributes of a certain type',
|
401
|
+
column_type: :integer,
|
402
|
+
array: true
|
403
|
+
end
|
404
|
+
|
405
|
+
context 'when one of the scoped attributes is an array-of-date column' do
|
406
|
+
include_examples 'it supports scoped attributes of a certain type',
|
407
|
+
column_type: :date,
|
408
|
+
array: true
|
409
|
+
end
|
410
|
+
|
411
|
+
context 'when one of the scoped attributes is an array-of-datetime column (using DateTime)' do
|
412
|
+
include_examples 'it supports scoped attributes of a certain type',
|
413
|
+
column_type: :datetime,
|
414
|
+
array: true
|
415
|
+
end
|
416
|
+
|
417
|
+
context 'when one of the scoped attributes is an array-of-datetime column (using Time)' do
|
418
|
+
include_examples 'it supports scoped attributes of a certain type',
|
419
|
+
column_type: :datetime,
|
420
|
+
value_type: :time,
|
421
|
+
array: true
|
422
|
+
end
|
423
|
+
|
424
|
+
context 'when one of the scoped attributes is an array-of-text column' do
|
425
|
+
include_examples 'it supports scoped attributes of a certain type',
|
426
|
+
column_type: :text,
|
427
|
+
array: true
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context "when an existing record that is not the first has a nil value for the scoped attribute" do
|
432
|
+
it 'still works' do
|
433
|
+
model = define_model_validating_uniqueness(scopes: [:scope])
|
434
|
+
create_record_from(model, scope: 'some value')
|
435
|
+
create_record_from(model, scope: nil)
|
436
|
+
record = build_record_from(model, scope: 'a different value')
|
437
|
+
|
438
|
+
expect(record).to validate_uniqueness.scoped_to(:scope)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
context 'when the model has a case-sensitive validation' do
|
444
|
+
context 'when case_insensitive is not specified' do
|
445
|
+
it 'accepts' do
|
446
|
+
record = build_record_validating_uniqueness(
|
447
|
+
attribute_type: :string,
|
448
|
+
validation_options: { case_sensitive: true }
|
449
|
+
)
|
450
|
+
|
451
|
+
expect(record).to validate_uniqueness
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
context 'when case_insensitive is specified' do
|
456
|
+
it 'rejects' do
|
457
|
+
record = build_record_validating_uniqueness(
|
458
|
+
attribute_type: :string,
|
459
|
+
validation_options: { case_sensitive: true }
|
460
|
+
)
|
461
|
+
|
462
|
+
expect(record).not_to validate_uniqueness.case_insensitive
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
context 'when the model has a case-insensitive validation' do
|
468
|
+
context 'when case_insensitive is not specified' do
|
469
|
+
it 'rejects' do
|
470
|
+
record = build_record_validating_uniqueness(
|
471
|
+
attribute_type: :string,
|
472
|
+
validation_options: { case_sensitive: false }
|
473
|
+
)
|
474
|
+
|
475
|
+
expect(record).not_to validate_uniqueness
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
context 'when case_insensitive is specified' do
|
480
|
+
it 'accepts' do
|
481
|
+
record = build_record_validating_uniqueness(
|
482
|
+
attribute_type: :string,
|
483
|
+
validation_options: { case_sensitive: false }
|
484
|
+
)
|
485
|
+
|
486
|
+
expect(record).to validate_uniqueness.case_insensitive
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
context 'when the validation is declared with allow_nil' do
|
492
|
+
context 'given a new record whose attribute is nil' do
|
493
|
+
it 'accepts' do
|
494
|
+
model = define_model_validating_uniqueness(
|
495
|
+
validation_options: { allow_nil: true }
|
496
|
+
)
|
497
|
+
record = build_record_from(model, attribute_name => nil)
|
498
|
+
expect(record).to validate_uniqueness.allow_nil
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
context 'given an existing record whose attribute is nil' do
|
503
|
+
it 'accepts' do
|
504
|
+
model = define_model_validating_uniqueness(
|
505
|
+
validation_options: { allow_nil: true }
|
506
|
+
)
|
507
|
+
record = create_record_from(model, attribute_name => nil)
|
508
|
+
expect(record).to validate_uniqueness.allow_nil
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
if active_record_supports_has_secure_password?
|
513
|
+
context 'when the model is declared with has_secure_password' do
|
514
|
+
it 'accepts' do
|
515
|
+
model = define_model_validating_uniqueness(
|
516
|
+
validation_options: { allow_nil: true },
|
517
|
+
additional_attributes: [{ name: :password_digest, type: :string }]
|
518
|
+
) do |m|
|
519
|
+
m.has_secure_password
|
520
|
+
end
|
521
|
+
|
522
|
+
record = build_record_from(model, attribute_name => nil)
|
523
|
+
|
524
|
+
expect(record).to validate_uniqueness.allow_nil
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
context 'when the validation is not declared with allow_nil' do
|
531
|
+
context 'given a new record whose attribute is nil' do
|
532
|
+
it 'rejects' do
|
533
|
+
model = define_model_validating_uniqueness
|
534
|
+
record = build_record_from(model, attribute_name => nil)
|
535
|
+
expect(record).not_to validate_uniqueness.allow_nil
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context 'given an existing record whose attribute is nil' do
|
540
|
+
it 'rejects' do
|
541
|
+
model = define_model_validating_uniqueness
|
542
|
+
record = create_record_from(model, attribute_name => nil)
|
543
|
+
expect(record).not_to validate_uniqueness.allow_nil
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
context 'when the validation is declared with allow_blank' do
|
549
|
+
context 'given a new record whose attribute is nil' do
|
550
|
+
it 'accepts' do
|
551
|
+
model = define_model_validating_uniqueness(
|
552
|
+
validation_options: { allow_blank: true }
|
553
|
+
)
|
554
|
+
record = build_record_from(model, attribute_name => nil)
|
555
|
+
expect(record).to validate_uniqueness.allow_blank
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
context 'given an existing record whose attribute is nil' do
|
560
|
+
it 'accepts' do
|
561
|
+
model = define_model_validating_uniqueness(
|
562
|
+
validation_options: { allow_blank: true }
|
563
|
+
)
|
564
|
+
record = create_record_from(model, attribute_name => nil)
|
565
|
+
expect(record).to validate_uniqueness.allow_blank
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
context 'given a new record whose attribute is empty' do
|
570
|
+
it 'accepts' do
|
571
|
+
model = define_model_validating_uniqueness(
|
572
|
+
attribute_type: :string,
|
573
|
+
validation_options: { allow_blank: true }
|
574
|
+
)
|
575
|
+
record = build_record_from(model, attribute_name => '')
|
576
|
+
expect(record).to validate_uniqueness.allow_blank
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
context 'given an existing record whose attribute is empty' do
|
581
|
+
it 'accepts' do
|
582
|
+
model = define_model_validating_uniqueness(
|
583
|
+
attribute_type: :string,
|
584
|
+
validation_options: { allow_blank: true }
|
585
|
+
)
|
586
|
+
record = create_record_from(model, attribute_name => '')
|
587
|
+
expect(record).to validate_uniqueness.allow_blank
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
if active_record_supports_has_secure_password?
|
592
|
+
context 'when the model is declared with has_secure_password' do
|
593
|
+
context 'given a record whose attribute is nil' do
|
594
|
+
it 'accepts' do
|
595
|
+
model = define_model_validating_uniqueness(
|
596
|
+
validation_options: { allow_blank: true },
|
597
|
+
additional_attributes: [{ name: :password_digest, type: :string }]
|
598
|
+
) do |m|
|
599
|
+
m.has_secure_password
|
600
|
+
end
|
601
|
+
|
602
|
+
record = build_record_from(model, attribute_name => nil)
|
603
|
+
|
604
|
+
expect(record).to validate_uniqueness.allow_blank
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
context 'given a record whose attribute is empty' do
|
609
|
+
it 'accepts' do
|
610
|
+
model = define_model_validating_uniqueness(
|
611
|
+
attribute_type: :string,
|
612
|
+
validation_options: { allow_blank: true },
|
613
|
+
additional_attributes: [{ name: :password_digest, type: :string }]
|
614
|
+
) do |m|
|
615
|
+
m.has_secure_password
|
616
|
+
end
|
617
|
+
|
618
|
+
record = build_record_from(model, attribute_name => '')
|
619
|
+
|
620
|
+
expect(record).to validate_uniqueness.allow_blank
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
context 'when the validation is not declared with allow_blank' do
|
628
|
+
context 'given a new record whose attribute is nil' do
|
629
|
+
it 'rejects' do
|
630
|
+
model = define_model_validating_uniqueness
|
631
|
+
record = build_record_from(model, attribute_name => nil)
|
632
|
+
expect(record).not_to validate_uniqueness.allow_blank
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
context 'given an existing record whose attribute is nil' do
|
637
|
+
it 'rejects' do
|
638
|
+
model = define_model_validating_uniqueness
|
639
|
+
record = create_record_from(model, attribute_name => nil)
|
640
|
+
expect(record).not_to validate_uniqueness.allow_blank
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
context 'given a new record whose attribute is empty' do
|
645
|
+
it 'rejects' do
|
646
|
+
model = define_model_validating_uniqueness(
|
647
|
+
attribute_type: :string
|
648
|
+
)
|
649
|
+
record = build_record_from(model, attribute_name => '')
|
650
|
+
expect(record).not_to validate_uniqueness.allow_blank
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
context 'given an existing record whose attribute is empty' do
|
655
|
+
it 'rejects' do
|
656
|
+
model = define_model_validating_uniqueness(
|
657
|
+
attribute_type: :string
|
658
|
+
)
|
659
|
+
record = create_record_from(model, attribute_name => '')
|
660
|
+
expect(record).not_to validate_uniqueness.allow_blank
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
context 'when testing that a polymorphic *_type column is one of the validation scopes' do
|
666
|
+
it 'sets that column to a meaningful value that works with other validations on the same column' do
|
667
|
+
user_model = define_model 'User'
|
668
|
+
favorite_columns = {
|
669
|
+
favoriteable_id: { type: :integer, options: { null: false } },
|
670
|
+
favoriteable_type: { type: :string, options: { null: false } }
|
671
|
+
}
|
672
|
+
favorite_model = define_model 'Favorite', favorite_columns do
|
673
|
+
attr_accessible :favoriteable
|
674
|
+
belongs_to :favoriteable, polymorphic: true
|
675
|
+
validates :favoriteable, presence: true
|
676
|
+
validates :favoriteable_id, uniqueness: { scope: :favoriteable_type }
|
677
|
+
end
|
678
|
+
|
679
|
+
user = user_model.create!
|
680
|
+
favorite_model.create!(favoriteable: user)
|
681
|
+
new_favorite = favorite_model.new
|
682
|
+
|
683
|
+
expect(new_favorite).
|
684
|
+
to validate_uniqueness_of(:favoriteable_id).
|
685
|
+
scoped_to(:favoriteable_type)
|
686
|
+
end
|
687
|
+
|
688
|
+
context 'if the model the *_type column refers to is namespaced, and shares the last part of its name with an existing model' do
|
689
|
+
it 'still works' do
|
690
|
+
define_class 'User'
|
691
|
+
define_module 'Models'
|
692
|
+
user_model = define_model 'Models::User'
|
693
|
+
favorite_columns = {
|
694
|
+
favoriteable_id: { type: :integer, options: { null: false } },
|
695
|
+
favoriteable_type: { type: :string, options: { null: false } }
|
696
|
+
}
|
697
|
+
favorite_model = define_model 'Models::Favorite', favorite_columns do
|
698
|
+
attr_accessible :favoriteable
|
699
|
+
belongs_to :favoriteable, polymorphic: true
|
700
|
+
validates :favoriteable, presence: true
|
701
|
+
validates :favoriteable_id, uniqueness: { scope: :favoriteable_type }
|
702
|
+
end
|
703
|
+
|
704
|
+
user = user_model.create!
|
705
|
+
favorite_model.create!(favoriteable: user)
|
706
|
+
new_favorite = favorite_model.new
|
707
|
+
|
708
|
+
expect(new_favorite).
|
709
|
+
to validate_uniqueness_of(:favoriteable_id).
|
710
|
+
scoped_to(:favoriteable_type)
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
let(:model_attributes) { {} }
|
716
|
+
|
717
|
+
def default_attribute
|
718
|
+
{
|
719
|
+
value_type: :string,
|
720
|
+
column_type: :string,
|
721
|
+
array: false
|
722
|
+
}
|
723
|
+
end
|
724
|
+
|
725
|
+
def normalize_attribute(attribute)
|
726
|
+
if attribute.is_a?(Hash)
|
727
|
+
if attribute.key?(:type)
|
728
|
+
attribute[:value_type] = attribute[:type]
|
729
|
+
attribute[:column_type] = attribute[:type]
|
730
|
+
end
|
731
|
+
|
732
|
+
default_attribute.merge(attribute)
|
733
|
+
else
|
734
|
+
default_attribute.merge(name: attribute)
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
def normalize_attributes(attributes)
|
739
|
+
attributes.map do |attribute|
|
740
|
+
normalize_attribute(attribute)
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
def column_options_from(attributes)
|
745
|
+
attributes.inject({}) do |options, attribute|
|
746
|
+
options_for_attribute = options[attribute[:name]] = {
|
747
|
+
type: attribute[:column_type],
|
748
|
+
options: attribute.fetch(:options, {})
|
749
|
+
}
|
750
|
+
|
751
|
+
if attribute[:array]
|
752
|
+
options_for_attribute[:options][:array] = attribute[:array]
|
753
|
+
end
|
754
|
+
|
755
|
+
options
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
def attributes_with_values_for(model)
|
760
|
+
model_attributes[model].each_with_object({}) do |attribute, attrs|
|
761
|
+
attrs[attribute[:name]] = attribute.fetch(:value) do
|
762
|
+
dummy_value_for(
|
763
|
+
attribute[:value_type],
|
764
|
+
array: attribute[:array]
|
765
|
+
)
|
766
|
+
end
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
def dummy_value_for(attribute_type, array: false)
|
771
|
+
if array
|
772
|
+
[ dummy_scalar_value_for(attribute_type) ]
|
773
|
+
else
|
774
|
+
dummy_scalar_value_for(attribute_type)
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
def dummy_scalar_value_for(attribute_type)
|
779
|
+
case attribute_type
|
780
|
+
when :string, :text
|
781
|
+
'dummy value'
|
782
|
+
when :integer
|
783
|
+
1
|
784
|
+
when :date
|
785
|
+
Date.today
|
786
|
+
when :datetime
|
787
|
+
Date.today.to_datetime
|
788
|
+
when :time
|
789
|
+
Time.now
|
790
|
+
when :uuid
|
791
|
+
SecureRandom.uuid
|
792
|
+
when :boolean
|
793
|
+
true
|
794
|
+
else
|
795
|
+
raise ArgumentError, "Unknown type '#{attribute_type}'"
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
def next_version_of(value, value_type)
|
800
|
+
if value.is_a?(Array)
|
801
|
+
[ next_version_of(value[0], value_type) ]
|
802
|
+
elsif value_type == :uuid
|
803
|
+
SecureRandom.uuid
|
804
|
+
elsif value.is_a?(Time)
|
805
|
+
value + 1
|
806
|
+
elsif value.in?([true, false])
|
807
|
+
!value
|
808
|
+
elsif value.respond_to?(:next)
|
809
|
+
value.next
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
def build_record_from(model, extra_attributes = {})
|
814
|
+
attributes = attributes_with_values_for(model)
|
815
|
+
model.new(attributes.merge(extra_attributes))
|
816
|
+
end
|
817
|
+
|
818
|
+
def create_record_from(model, extra_attributes = {})
|
819
|
+
build_record_from(model, extra_attributes).tap do |record|
|
820
|
+
record.save!
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
def determine_scope_attribute_names_from(scope_attributes)
|
825
|
+
scope_attributes.map do |attribute|
|
826
|
+
if attribute.is_a?(Hash)
|
827
|
+
attribute[:name]
|
828
|
+
else
|
829
|
+
attribute
|
830
|
+
end
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
def define_model_validating_uniqueness(options = {}, &block)
|
835
|
+
attribute_type = options.fetch(:attribute_type, :string)
|
836
|
+
attribute_options = options.fetch(:attribute_options, {})
|
837
|
+
attribute = {
|
838
|
+
name: attribute_name,
|
839
|
+
value_type: attribute_type,
|
840
|
+
column_type: attribute_type,
|
841
|
+
options: attribute_options
|
842
|
+
}
|
843
|
+
scope_attributes = normalize_attributes(options.fetch(:scopes, []))
|
844
|
+
scope_attribute_names = scope_attributes.map { |attr| attr[:name] }
|
845
|
+
additional_attributes = normalize_attributes(
|
846
|
+
options.fetch(:additional_attributes, [])
|
847
|
+
)
|
848
|
+
attributes = [attribute] + scope_attributes + additional_attributes
|
849
|
+
validation_options = options.fetch(:validation_options, {})
|
850
|
+
column_options = column_options_from(attributes)
|
851
|
+
|
852
|
+
model = define_model(:example, column_options) do |m|
|
853
|
+
m.validates_uniqueness_of attribute_name,
|
854
|
+
validation_options.merge(scope: scope_attribute_names)
|
855
|
+
|
856
|
+
attributes.each do |attr|
|
857
|
+
m.attr_accessible(attr[:name])
|
858
|
+
end
|
859
|
+
|
860
|
+
block.call(m) if block
|
861
|
+
end
|
862
|
+
|
863
|
+
model_attributes[model] = attributes
|
864
|
+
|
865
|
+
model
|
866
|
+
end
|
867
|
+
|
868
|
+
def build_record_validating_uniqueness(options = {}, &block)
|
869
|
+
model = define_model_validating_uniqueness(options, &block)
|
870
|
+
build_record_from(model)
|
871
|
+
end
|
872
|
+
alias_method :new_record_validating_uniqueness,
|
873
|
+
:build_record_validating_uniqueness
|
874
|
+
|
875
|
+
def create_record_validating_uniqueness(options = {}, &block)
|
876
|
+
build_record_validating_uniqueness(options, &block).tap do |record|
|
877
|
+
record.save!
|
878
|
+
end
|
879
|
+
end
|
880
|
+
alias_method :existing_record_validating_uniqueness,
|
881
|
+
:create_record_validating_uniqueness
|
882
|
+
|
883
|
+
def build_record_validating_scoped_uniqueness_with_enum(options = {})
|
884
|
+
options = options.dup
|
885
|
+
enum_scope_attribute =
|
886
|
+
normalize_attribute(options.delete(:enum_scope)).
|
887
|
+
merge(value_type: :integer, column_type: :integer)
|
888
|
+
additional_scopes = options.delete(:additional_scopes) { [] }
|
889
|
+
options[:scopes] = [enum_scope_attribute] + additional_scopes
|
890
|
+
dummy_enum_values = [:foo, :bar]
|
891
|
+
|
892
|
+
model = define_model_validating_uniqueness(options)
|
893
|
+
model.enum(enum_scope_attribute[:name] => dummy_enum_values)
|
894
|
+
|
895
|
+
build_record_from(model)
|
896
|
+
end
|
897
|
+
|
898
|
+
def validate_uniqueness
|
899
|
+
validate_uniqueness_of(attribute_name)
|
900
|
+
end
|
901
|
+
|
902
|
+
def attribute_name
|
903
|
+
:attr
|
904
|
+
end
|
905
|
+
end
|