active_record_doctor 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +52 -22
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  9. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
  10. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
  11. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
  12. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
  13. data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
  14. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
  15. data/lib/active_record_doctor/version.rb +1 -1
  16. data/lib/active_record_doctor.rb +1 -0
  17. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  18. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  19. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  20. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
  21. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  22. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
  23. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
  24. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
  25. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  26. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  27. data/test/setup.rb +6 -2
  28. metadata +8 -3
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_doctor/detectors/base"
4
+
5
+ module ActiveRecordDoctor
6
+ module Detectors
7
+ class IncorrectLengthValidation < Base # :nodoc:
8
+ @description = "detect mismatches between database length limits and model length validations"
9
+ @config = {
10
+ ignore_models: {
11
+ description: "models whose validators should not be checked",
12
+ global: true
13
+ },
14
+ ignore_attributes: {
15
+ description: "attributes, written as Model.attribute, whose validators should not be checked"
16
+ }
17
+ }
18
+
19
+ private
20
+
21
+ def message(model:, attribute:, table:, database_maximum:, model_maximum:)
22
+ # rubocop:disable Layout/LineLength
23
+ if database_maximum && model_maximum
24
+ "the schema limits #{table}.#{attribute} to #{database_maximum} characters but the length validator on #{model}.#{attribute} enforces a maximum of #{model_maximum} characters - set both limits to the same value or remove both"
25
+ elsif database_maximum && model_maximum.nil?
26
+ "the schema limits #{table}.#{attribute} to #{database_maximum} characters but there's no length validator on #{model}.#{attribute} - remove the database limit or add the validator"
27
+ elsif database_maximum.nil? && model_maximum
28
+ "the length validator on #{model}.#{attribute} enforces a maximum of #{model_maximum} characters but there's no schema limit on #{table}.#{attribute} - remove the validator or the schema length limit"
29
+ end
30
+ # rubocop:enable Layout/LineLength
31
+ end
32
+
33
+ def detect
34
+ models(except: config(:ignore_models)).each do |model|
35
+ next unless model.table_exists?
36
+
37
+ connection.columns(model.table_name).each do |column|
38
+ next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
39
+ next if ![:string, :text].include?(column.type)
40
+
41
+ model_maximum = maximum_allowed_by_validations(model)
42
+ next if model_maximum == column.limit
43
+
44
+ problem!(
45
+ model: model.name,
46
+ attribute: column.name,
47
+ table: model.table_name,
48
+ database_maximum: column.limit,
49
+ model_maximum: model_maximum
50
+ )
51
+ end
52
+ end
53
+ end
54
+
55
+ def maximum_allowed_by_validations(model)
56
+ length_validator = model.validators.find do |validator|
57
+ validator.kind == :length && validator.options.include?(:maximum)
58
+ end
59
+ length_validator ? length_validator.options[:maximum] : nil
60
+ end
61
+ end
62
+ end
63
+ end
@@ -24,7 +24,7 @@ module ActiveRecordDoctor
24
24
 
25
25
  def detect
26
26
  table_models = models.group_by(&:table_name)
27
- table_models.delete_if { |table| table.nil? || !table_exists?(table) }
27
+ table_models.delete_if { |_table, models| !models.first.table_exists? }
28
28
 
29
29
  table_models.each do |table, models|
30
30
  next if config(:ignore_tables).include?(table)
@@ -37,6 +37,7 @@ module ActiveRecordDoctor
37
37
  next if config(:ignore_columns).include?("#{table}.#{column.name}")
38
38
  next if !column.null
39
39
  next if !concrete_models.all? { |model| non_null_needed?(model, column) }
40
+ next if not_null_check_constraint_exists?(table, column)
40
41
 
41
42
  problem!(column: column.name, table: table)
42
43
  end
@@ -24,8 +24,7 @@ module ActiveRecordDoctor
24
24
 
25
25
  def detect
26
26
  models(except: config(:ignore_models)).each do |model|
27
- next if model.table_name.nil?
28
- next unless table_exists?(model.table_name)
27
+ next unless model.table_exists?
29
28
 
30
29
  connection.columns(model.table_name).each do |column|
31
30
  next unless validator_needed?(model, column)
@@ -38,8 +37,8 @@ module ActiveRecordDoctor
38
37
  end
39
38
 
40
39
  def validator_needed?(model, column)
41
- ![model.primary_key, "created_at", "updated_at"].include?(column.name) &&
42
- !column.null
40
+ ![model.primary_key, "created_at", "updated_at", "created_on", "updated_on"].include?(column.name) &&
41
+ (!column.null || not_null_check_constraint_exists?(model.table_name, column))
43
42
  end
44
43
 
45
44
  def validator_present?(model, column)
@@ -18,31 +18,60 @@ module ActiveRecordDoctor
18
18
 
19
19
  private
20
20
 
21
- def message(table:, columns:)
22
- # rubocop:disable Layout/LineLength
23
- "add a unique index on #{table}(#{columns.join(', ')}) - validating uniqueness in the model without an index can lead to duplicates"
24
- # rubocop:enable Layout/LineLength
21
+ # rubocop:disable Layout/LineLength
22
+ def message(model:, table:, columns:, problem:)
23
+ case problem
24
+ when :validations
25
+ "add a unique index on #{table}(#{columns.join(', ')}) - validating uniqueness in the model without an index can lead to duplicates"
26
+ when :has_ones
27
+ "add a unique index on #{table}(#{columns.first}) - using `has_one` in the #{model.name} model without an index can lead to duplicates"
28
+ end
25
29
  end
30
+ # rubocop:enable Layout/LineLength
26
31
 
27
32
  def detect
28
- ignore_columns = config(:ignore_columns).map do |column|
29
- column.gsub(" ", "")
30
- end
33
+ validations_without_indexes
34
+ has_ones_without_indexes
35
+ end
31
36
 
37
+ def validations_without_indexes
32
38
  models(except: config(:ignore_models)).each do |model|
33
- next if model.table_name.nil?
39
+ next unless model.table_exists?
34
40
 
35
41
  model.validators.each do |validator|
36
42
  scope = Array(validator.options.fetch(:scope, []))
37
43
 
38
44
  next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
39
45
  next unless supported_validator?(validator)
40
- next if unique_index?(model.table_name, validator.attributes, scope)
41
46
 
42
- columns = (scope + validator.attributes).map(&:to_s)
43
- next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
47
+ validator.attributes.each do |attribute|
48
+ columns = resolve_attributes(model, scope + [attribute])
49
+
50
+ next if unique_index?(model.table_name, columns)
51
+ next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
52
+
53
+ problem!(model: model, table: model.table_name, columns: columns, problem: :validations)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def has_ones_without_indexes # rubocop:disable Naming/PredicateName
60
+ models.each do |model|
61
+ has_ones = model.reflect_on_all_associations(:has_one)
62
+ has_ones.each do |has_one|
63
+ next if has_one.is_a?(ActiveRecord::Reflection::ThroughReflection) || has_one.scope
64
+
65
+ association_model = has_one.klass
66
+ next if config(:ignore_models).include?(association_model.name)
44
67
 
45
- problem!(table: model.table_name, columns: columns)
68
+ foreign_key = has_one.foreign_key
69
+ next if ignore_columns.include?(foreign_key.to_s)
70
+
71
+ table_name = association_model.table_name
72
+ next if unique_index?(table_name, [foreign_key])
73
+
74
+ problem!(model: model, table: table_name, columns: [foreign_key], problem: :has_ones)
46
75
  end
47
76
  end
48
77
  end
@@ -59,11 +88,32 @@ module ActiveRecordDoctor
59
88
  validator.options.fetch(:case_sensitive, true)
60
89
  end
61
90
 
62
- def unique_index?(table_name, columns, scope)
63
- columns = (Array(scope) + columns).map(&:to_s)
91
+ def resolve_attributes(model, attributes)
92
+ attributes.flat_map do |attribute|
93
+ reflection = model.reflect_on_association(attribute)
94
+
95
+ if reflection.nil?
96
+ attribute
97
+ elsif reflection.polymorphic?
98
+ [reflection.foreign_type, reflection.foreign_key]
99
+ else
100
+ reflection.foreign_key
101
+ end
102
+ end.map(&:to_s)
103
+ end
64
104
 
105
+ def unique_index?(table_name, columns, scope = nil)
106
+ columns = (Array(scope) + columns).map(&:to_s)
65
107
  indexes(table_name).any? do |index|
66
- index.columns.to_set == columns.to_set && index.unique
108
+ index.unique &&
109
+ index.where.nil? &&
110
+ (Array(index.columns) - columns).empty?
111
+ end
112
+ end
113
+
114
+ def ignore_columns
115
+ @ignore_columns ||= config(:ignore_columns).map do |column|
116
+ column.gsub(" ", "")
67
117
  end
68
118
  end
69
119
  end
@@ -23,7 +23,7 @@ module ActiveRecordDoctor
23
23
  tables(except: config(:ignore_tables)).each do |table|
24
24
  column = primary_key(table)
25
25
  next if column.nil?
26
- next if bigint?(column)
26
+ next if bigint?(column) || uuid?(column)
27
27
 
28
28
  problem!(table: table, column: column.name)
29
29
  end
@@ -36,6 +36,10 @@ module ActiveRecordDoctor
36
36
  /\Abigint\b/.match?(column.sql_type)
37
37
  end
38
38
  end
39
+
40
+ def uuid?(column)
41
+ column.sql_type == "uuid"
42
+ end
39
43
  end
40
44
  end
41
45
  end
@@ -21,9 +21,7 @@ module ActiveRecordDoctor
21
21
 
22
22
  def detect
23
23
  models(except: config(:ignore_models)).each do |model|
24
- next if model.table_name.nil?
25
- next if tables.include?(model.table_name)
26
- next if tables_and_views.include?(model.table_name)
24
+ next if model.table_exists? || views.include?(model.table_name)
27
25
 
28
26
  problem!(model: model.name, table: model.table_name)
29
27
  end
@@ -26,7 +26,7 @@ module ActiveRecordDoctor
26
26
 
27
27
  def message(index:, column_name:)
28
28
  # rubocop:disable Layout/LineLength
29
- "consider adding `WHERE #{column_name} IS NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
29
+ "consider adding `WHERE #{column_name} IS NULL` or `WHERE #{column_name} IS NOT NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
30
30
  # rubocop:enable Layout/LineLength
31
31
  end
32
32
 
@@ -42,8 +42,7 @@ module ActiveRecordDoctor
42
42
 
43
43
  timestamp_columns.each do |timestamp_column|
44
44
  indexes(table, except: config(:ignore_indexes)).each do |index|
45
- # TODO: whole word
46
- next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+NULL\b/i
45
+ next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+(NOT\s+)?NULL\b/i
47
46
 
48
47
  problem!(index: index.name, column_name: timestamp_column.name)
49
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "1.9.0"
4
+ VERSION = "1.10.0"
5
5
  end
@@ -7,6 +7,7 @@ require "active_record_doctor/detectors/missing_presence_validation"
7
7
  require "active_record_doctor/detectors/missing_foreign_keys"
8
8
  require "active_record_doctor/detectors/missing_unique_indexes"
9
9
  require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
10
+ require "active_record_doctor/detectors/incorrect_length_validation"
10
11
  require "active_record_doctor/detectors/extraneous_indexes"
11
12
  require "active_record_doctor/detectors/unindexed_deleted_at"
12
13
  require "active_record_doctor/detectors/undefined_table_references"
@@ -62,14 +62,14 @@ MIGRATION
62
62
  end
63
63
 
64
64
  def add_index(table, column)
65
- index_name = Class.new.extend(ActiveRecord::ConnectionAdapters::SchemaStatements).index_name table, column
66
- # rubocop:disable Layout/LineLength
67
- if index_name.size > ActiveRecord::Base.connection.allowed_index_name_length
68
- " add_index :#{table}, :#{column}, name: '#{index_name.first ActiveRecord::Base.connection.allowed_index_name_length}'"
65
+ connection = ActiveRecord::Base.connection
66
+
67
+ index_name = connection.index_name(table, column)
68
+ if index_name.size > connection.index_name_length
69
+ " add_index :#{table}, :#{column}, name: '#{index_name.first(connection.index_name_length)}'"
69
70
  else
70
71
  " add_index :#{table}, :#{column}"
71
72
  end
72
- # rubocop:enable Layout/LineLength
73
73
  end
74
74
 
75
75
  def migration_version
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::DisableTest < Minitest::Test
4
+ # Disabling detectors is implemented in the base class. It's enought to test
5
+ # it on a single detector to be reasonably certain it works on all of them.
6
+ def test_disabling
7
+ create_table(:users) do |t|
8
+ t.string :name, null: true
9
+ end.create_model do
10
+ validates :name, presence: true
11
+ end
12
+
13
+ config_file(<<-CONFIG)
14
+ ActiveRecordDoctor.configure do |config|
15
+ config.detector :missing_non_null_constraint,
16
+ enabled: false
17
+ end
18
+ CONFIG
19
+
20
+ refute_problems
21
+ end
22
+
23
+ private
24
+
25
+ # We need to override that method in order to skip the mechanism that
26
+ # infers detector name from the test class name.
27
+ def detector_name
28
+ :missing_non_null_constraint
29
+ end
30
+ end
@@ -11,6 +11,27 @@ remove index_users_on_id - coincides with the primary key on the table
11
11
  OUTPUT
12
12
  end
13
13
 
14
+ def test_partial_index_on_primary_key
15
+ skip("MySQL doesn't support partial indexes") if mysql?
16
+
17
+ create_table(:users) do |t|
18
+ t.boolean :admin
19
+ t.index :id, where: "admin"
20
+ end
21
+
22
+ refute_problems
23
+ end
24
+
25
+ def test_index_on_non_standard_primary_key
26
+ create_table(:profiles, primary_key: :user_id) do |t|
27
+ t.index :user_id
28
+ end
29
+
30
+ assert_problems(<<OUTPUT)
31
+ remove index_profiles_on_user_id - coincides with the primary key on the table
32
+ OUTPUT
33
+ end
34
+
14
35
  def test_non_unique_version_of_index_is_duplicate
15
36
  create_table(:users) do |t|
16
37
  t.string :email
@@ -61,6 +82,19 @@ remove index_users_on_last_name_and_first_name - can be replaced by index_users_
61
82
  OUTPUT
62
83
  end
63
84
 
85
+ def test_unique_index_with_fewer_columns
86
+ create_table(:users) do |t|
87
+ t.string :first_name
88
+ t.string :last_name
89
+ t.index :first_name, unique: true
90
+ t.index [:last_name, :first_name], unique: true
91
+ end
92
+
93
+ assert_problems(<<OUTPUT)
94
+ remove index_users_on_last_name_and_first_name - can be replaced by index_users_on_first_name
95
+ OUTPUT
96
+ end
97
+
64
98
  def test_not_covered_by_different_index_type
65
99
  create_table(:users) do |t|
66
100
  t.string :first_name
@@ -14,7 +14,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
14
14
  end
15
15
 
16
16
  assert_problems(<<~OUTPUT)
17
- use `dependent: :delete_all` or similar on ModelFactory::Models::Company.users - associated models have no validations and can be deleted in bulk
17
+ use `dependent: :delete_all` or similar on ModelFactory::Models::Company.users - associated model ModelFactory::Models::User has no validations and can be deleted in bulk
18
18
  OUTPUT
19
19
  end
20
20
 
@@ -56,7 +56,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
56
56
  end
57
57
 
58
58
  assert_problems(<<~OUTPUT)
59
- use `dependent: :destroy` or similar on ModelFactory::Models::Company.users - the associated model has callbacks that are currently skipped
59
+ use `dependent: :destroy` or similar on ModelFactory::Models::Company.users - the associated model ModelFactory::Models::User has callbacks that are currently skipped
60
60
  OUTPUT
61
61
  end
62
62
 
@@ -93,7 +93,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
93
93
  end
94
94
 
95
95
  assert_problems(<<~OUTPUT)
96
- use `dependent: :delete` or similar on ModelFactory::Models::Company.owner - the associated model has no callbacks and can be deleted without loading
96
+ use `dependent: :delete` or similar on ModelFactory::Models::Company.owner - the associated model ModelFactory::Models::User has no callbacks and can be deleted without loading
97
97
  OUTPUT
98
98
  end
99
99
 
@@ -110,7 +110,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
110
110
  end
111
111
 
112
112
  assert_problems(<<~OUTPUT)
113
- use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
113
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
114
114
  OUTPUT
115
115
  end
116
116
 
@@ -134,7 +134,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
134
134
  end
135
135
 
136
136
  assert_problems(<<~OUTPUT)
137
- use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
137
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
138
138
  OUTPUT
139
139
  end
140
140
 
@@ -158,7 +158,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
158
158
  end
159
159
 
160
160
  assert_problems(<<~OUTPUT)
161
- use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
161
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
162
162
  OUTPUT
163
163
  end
164
164
 
@@ -187,7 +187,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
187
187
  end
188
188
 
189
189
  assert_problems(<<~OUTPUT)
190
- use `dependent: :destroy` or similar on ModelFactory::Models::User.company - the associated model has callbacks that are currently skipped
190
+ use `dependent: :destroy` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has callbacks that are currently skipped
191
191
  OUTPUT
192
192
  end
193
193
 
@@ -228,6 +228,104 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
228
228
  refute_problems
229
229
  end
230
230
 
231
+ def test_polymorphic_destroy_reported_when_all_associations_deletable
232
+ create_table(:images) do |t|
233
+ t.bigint :imageable_id, null: false
234
+ t.string :imageable_type, null: true
235
+ end.create_model do
236
+ belongs_to :imageable, polymorphic: true, dependent: :destroy
237
+ end
238
+
239
+ create_table(:users) do
240
+ end.create_model do
241
+ has_one :image, as: :imageable
242
+ end
243
+
244
+ create_table(:companies) do
245
+ end.create_model do
246
+ has_one :image, as: :imageable
247
+ end
248
+
249
+ assert_problems(<<~OUTPUT)
250
+ use `dependent: :delete` or similar on ModelFactory::Models::Image.imageable - the associated models ModelFactory::Models::Company, ModelFactory::Models::User have no callbacks and can be deleted without loading
251
+ OUTPUT
252
+ end
253
+
254
+ def test_polymorphic_destroy_not_reported_when_some_associations_not_deletable
255
+ create_table(:images) do |t|
256
+ t.bigint :imageable_id, null: false
257
+ t.string :imageable_type, null: true
258
+ end.create_model do
259
+ belongs_to :imageable, polymorphic: true, dependent: :destroy
260
+ end
261
+
262
+ create_table(:users) do
263
+ end.create_model do
264
+ has_one :image, as: :imageable
265
+
266
+ before_destroy :log
267
+
268
+ def log
269
+ end
270
+ end
271
+
272
+ create_table(:companies) do
273
+ end.create_model do
274
+ has_one :image, as: :imageable
275
+ end
276
+
277
+ refute_problems
278
+ end
279
+
280
+ def test_polymorphic_delete_reported_when_some_associations_not_deletable
281
+ create_table(:images) do |t|
282
+ t.bigint :imageable_id, null: false
283
+ t.string :imageable_type, null: true
284
+ end.create_model do
285
+ belongs_to :imageable, polymorphic: true, dependent: :delete
286
+ end
287
+
288
+ create_table(:users) do
289
+ end.create_model do
290
+ has_one :image, as: :imageable
291
+
292
+ before_destroy :log
293
+
294
+ def log
295
+ end
296
+ end
297
+
298
+ create_table(:companies) do
299
+ end.create_model do
300
+ has_one :image, as: :imageable
301
+ end
302
+
303
+ assert_problems(<<~OUTPUT)
304
+ use `dependent: :destroy` or similar on ModelFactory::Models::Image.imageable - the associated model ModelFactory::Models::User has callbacks that are currently skipped
305
+ OUTPUT
306
+ end
307
+
308
+ def test_polymorphic_delete_not_reported_when_all_associations_deletable
309
+ create_table(:images) do |t|
310
+ t.bigint :imageable_id, null: false
311
+ t.string :imageable_type, null: true
312
+ end.create_model do
313
+ belongs_to :imageable, polymorphic: true, dependent: :delete
314
+ end
315
+
316
+ create_table(:users) do
317
+ end.create_model do
318
+ has_one :image, as: :imageable
319
+ end
320
+
321
+ create_table(:companies) do
322
+ end.create_model do
323
+ has_one :image, as: :imageable
324
+ end
325
+
326
+ refute_problems
327
+ end
328
+
231
329
  def test_config_ignore_models
232
330
  create_table(:companies) do
233
331
  end.create_model do
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::IncorrectLengthValidationTest < Minitest::Test
4
+ def test_validation_and_limit_equal_is_ok
5
+ create_table(:users) do |t|
6
+ t.string :email, limit: 64
7
+ end.create_model do
8
+ validates :email, length: { maximum: 64 }
9
+ end
10
+
11
+ refute_problems
12
+ end
13
+
14
+ def test_validation_and_limit_different_is_error
15
+ create_table(:users) do |t|
16
+ t.string :email, limit: 64
17
+ end.create_model do
18
+ validates :email, length: { maximum: 32 }
19
+ end
20
+
21
+ assert_problems(<<~OUTPUT)
22
+ the schema limits users.email to 64 characters but the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters - set both limits to the same value or remove both
23
+ OUTPUT
24
+ end
25
+
26
+ def test_validation_and_no_limit_is_error
27
+ skip("MySQL always sets a limit on text columns") if mysql?
28
+
29
+ create_table(:users) do |t|
30
+ t.string :email
31
+ end.create_model do
32
+ validates :email, length: { maximum: 32 }
33
+ end
34
+
35
+ assert_problems(<<~OUTPUT)
36
+ the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters but there's no schema limit on users.email - remove the validator or the schema length limit
37
+ OUTPUT
38
+ end
39
+
40
+ def test_no_validation_and_limit_is_error
41
+ create_table(:users) do |t|
42
+ t.string :email, limit: 64
43
+ end.create_model do
44
+ end
45
+
46
+ assert_problems(<<~OUTPUT)
47
+ the schema limits users.email to 64 characters but there's no length validator on ModelFactory::Models::User.email - remove the database limit or add the validator
48
+ OUTPUT
49
+ end
50
+
51
+ def test_no_validation_and_no_limit_is_ok
52
+ skip("MySQL always sets a limit on text columns") if mysql?
53
+
54
+ create_table(:users) do |t|
55
+ t.string :email
56
+ end.create_model do
57
+ end
58
+
59
+ refute_problems
60
+ end
61
+
62
+ def test_config_ignore_models
63
+ create_table(:users) do |t|
64
+ t.string :email, limit: 64
65
+ end.create_model
66
+
67
+ config_file(<<-CONFIG)
68
+ ActiveRecordDoctor.configure do |config|
69
+ config.detector :incorrect_length_validation,
70
+ ignore_models: ["ModelFactory::Models::User"]
71
+ end
72
+ CONFIG
73
+
74
+ refute_problems
75
+ end
76
+
77
+ def test_global_ignore_models
78
+ create_table(:users) do |t|
79
+ t.string :email, limit: 64
80
+ end.create_model
81
+
82
+ config_file(<<-CONFIG)
83
+ ActiveRecordDoctor.configure do |config|
84
+ config.global :ignore_models, ["ModelFactory::Models::User"]
85
+ end
86
+ CONFIG
87
+
88
+ refute_problems
89
+ end
90
+
91
+ def test_config_ignore_attributes
92
+ create_table(:users) do |t|
93
+ t.string :email, limit: 64
94
+ end.create_model
95
+
96
+ config_file(<<-CONFIG)
97
+ ActiveRecordDoctor.configure do |config|
98
+ config.detector :incorrect_length_validation,
99
+ ignore_attributes: ["ModelFactory::Models::User.email"]
100
+ end
101
+ CONFIG
102
+
103
+ refute_problems
104
+ end
105
+ end