active_record_doctor 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 01e06f004eba174cadece575f77fe83381f53133
4
- data.tar.gz: d835060741b338b07a3f87aef33eff707ef32f98
2
+ SHA256:
3
+ metadata.gz: e519565b3322adffcd6b6a82a3c00289f1ad0169bb411c45292bb97306c31ae4
4
+ data.tar.gz: fc8c79e4ba609b97755a59d9c0b0cc86d7dbd9697bd037ecb045187af0d0d34d
5
5
  SHA512:
6
- metadata.gz: e07f92a2fd4b200fd5d9e0ef50e1b5a516347b8216080071648bca31c6e72683ceb01d85e125002e4aa0cd5f48a8020ca43097488a67906c15be443e68783da4
7
- data.tar.gz: 2faca055adba20d77aac44658adb35d40243f491b533a088e5ca44615d16f36e4cee7deca99b1a37607127c5f2bc276e5dac0aa56c34f0b5aa87b611667afd3e
6
+ metadata.gz: 1a9e7ecb7a194938ab9cb9540dfd6d19a6b8092b23da2374bafa2146164184d29e3fd3877aef6c6709e531c514055bf2bf6be1a268bb4023bb16889f89f08f17
7
+ data.tar.gz: 545dc4d6ae159b3619d0a0712d31e09aa40ea8ddc104401801791b4d74f3fae59b6cfde609cebea6d5718755f2b488c754524335e53bcf2d9e32a9d7242a6e60
data/README.md CHANGED
@@ -11,6 +11,7 @@ can:
11
11
  * detect uniqueness validations not backed by an unique index
12
12
  * detect missing non-`NULL` constraints
13
13
  * detect missing presence validations
14
+ * detect incorrect presence validations on boolean columns
14
15
 
15
16
  More features coming soon!
16
17
 
@@ -170,7 +171,12 @@ cases where the name can be wrong (e.g. you forgot to commit a migration or
170
171
  changed the table name). Active Record Doctor can help you identify these cases
171
172
  before they hit production.
172
173
 
173
- The only think you need to do is run:
174
+ **IMPORTANT**. Models backed by views are supported only in:
175
+
176
+ * Rails 5+ and _any_ database or
177
+ * Rails 4.2 with PostgreSQL.
178
+
179
+ The only think you need to do is run:
174
180
 
175
181
  ```
176
182
  bundle exec rake active_record_doctor:undefined_table_references
@@ -251,6 +257,27 @@ The following models and columns should have presence validations:
251
257
 
252
258
  This means `User` should have a presence validator on `email` and `name`.
253
259
 
260
+ ### Detecting Incorrect Presence Validations on Boolean Columns
261
+
262
+ A boolean column's presence should be validated using inclusion or exclusion
263
+ validators instead of the usual presence validator.
264
+
265
+ In order to detect boolean columns whose presence is validated incorrectly run:
266
+
267
+ ```
268
+ bundle exec rake active_record_doctor:incorrect_boolean_presence_validation
269
+ ```
270
+
271
+ The output of the command looks like this:
272
+
273
+ ```
274
+ The presence of the following boolean columns is validated incorrectly:
275
+ User: active
276
+ ```
277
+
278
+ This means `active` is validated with `presence: true` instead of
279
+ `inclusion: { in: [true, false] }` or `exclusion: { in: [nil] }`.
280
+
254
281
  ## Author
255
282
 
256
283
  This gem is developed and maintained by [Greg Navis](http://www.gregnavis.com).
@@ -36,12 +36,21 @@ module ActiveRecordDoctor
36
36
  end.join("\n"))
37
37
  end
38
38
 
39
- def undefined_table_references(models)
39
+ def undefined_table_references((models, views_checked))
40
40
  return if models.empty?
41
41
 
42
+ unless views_checked
43
+ @io.puts(<<EOS)
44
+ WARNING: Models backed by database views are supported only in Rails 5+ OR
45
+ Rails 4.2 + PostgreSQL. It seems this is NOT your setup. Therefore, such models
46
+ will be erroneously reported below as not having their underlying tables/views.
47
+ Consider upgrading Rails or disabling this task temporarily.
48
+ EOS
49
+ end
50
+
42
51
  @io.puts('The following models reference undefined tables:')
43
- models.each do |model|
44
- @io.puts(" #{model.name} (the table #{model.table_name} is undefined)")
52
+ models.each do |model_name, table_name|
53
+ @io.puts(" #{model_name} (the table #{table_name} is undefined)")
45
54
  end
46
55
  end
47
56
 
@@ -82,6 +91,15 @@ module ActiveRecordDoctor
82
91
  @io.puts(" #{table}: #{columns.join(', ')}")
83
92
  end
84
93
  end
94
+
95
+ def presence_true_on_boolean(presence_true_on_booleans)
96
+ return if presence_true_on_booleans.empty?
97
+
98
+ @io.puts('The presence of the following boolean columns is validated incorrectly:')
99
+ presence_true_on_booleans.each do |table, columns|
100
+ @io.puts(" #{table}: #{columns.join(', ')}")
101
+ end
102
+ end
85
103
  end
86
104
  end
87
105
  end
@@ -34,6 +34,20 @@ module ActiveRecordDoctor
34
34
  end
35
35
  end
36
36
 
37
+ def views
38
+ @views ||=
39
+ if Rails::VERSION::MAJOR == 5
40
+ connection.views
41
+ elsif connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
42
+ ActiveRecord::Base.connection.execute(<<-SQL).map { |tuple| tuple.fetch("relname") }
43
+ SELECT c.relname FROM pg_class c WHERE c.relkind IN ('m', 'v')
44
+ SQL
45
+ else
46
+ # We don't support this Rails/database combination yet.
47
+ nil
48
+ end
49
+ end
50
+
37
51
  def hash_from_pairs(pairs)
38
52
  Hash[*pairs.flatten(1)]
39
53
  end
@@ -0,0 +1,33 @@
1
+ require "active_record_doctor/tasks/base"
2
+
3
+ module ActiveRecordDoctor
4
+ module Tasks
5
+ class IncorrectBooleanPresenceValidation < Base
6
+ def run
7
+ eager_load!
8
+
9
+ success(hash_from_pairs(models.reject do |model|
10
+ model.table_name.nil? || model.table_name == 'schema_migrations'
11
+ end.map do |model|
12
+ [
13
+ model.name,
14
+ connection.columns(model.table_name).select do |column|
15
+ column.type == :boolean &&
16
+ has_presence_validator?(model, column)
17
+ end.map(&:name)
18
+ ]
19
+ end.reject do |model_name, columns|
20
+ columns.empty?
21
+ end))
22
+ end
23
+
24
+ private
25
+
26
+ def has_presence_validator?(model, column)
27
+ model.validators.any? do |validator|
28
+ validator.kind == :presence && validator.attributes.include?(column.name.to_sym)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -29,9 +29,16 @@ module ActiveRecordDoctor
29
29
  end
30
30
 
31
31
  def has_mandatory_presence_validator?(model, column)
32
+ allowed_attributes = [column.name.to_sym]
33
+
34
+ belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
35
+ reflection.foreign_key == column.name
36
+ end
37
+ allowed_attributes << belongs_to.name.to_sym if belongs_to
38
+
32
39
  model.validators.any? do |validator|
33
40
  validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
34
- validator.attributes.include?(column.name.to_sym) &&
41
+ (validator.attributes && allowed_attributes).present? &&
35
42
  !validator.options[:allow_nil] &&
36
43
  !validator.options[:if] &&
37
44
  !validator.options[:unless]
@@ -13,8 +13,7 @@ module ActiveRecordDoctor
13
13
  model.name,
14
14
  connection.columns(model.table_name).select do |column|
15
15
  validator_needed?(model, column) &&
16
- !column.null &&
17
- !has_presence_validator?(model, column)
16
+ !validator_present?(model, column)
18
17
  end.map(&:name)
19
18
  ]
20
19
  end.reject do |model_name, columns|
@@ -25,13 +24,46 @@ module ActiveRecordDoctor
25
24
  private
26
25
 
27
26
  def validator_needed?(model, column)
28
- ![model.primary_key, 'created_at', 'updated_at'].include?(column.name)
27
+ ![model.primary_key, 'created_at', 'updated_at'].include?(column.name) &&
28
+ !column.null
29
29
  end
30
30
 
31
- def has_presence_validator?(model, column)
31
+ def validator_present?(model, column)
32
+ if column.type == :boolean
33
+ inclusion_validator_present?(model, column) ||
34
+ exclusion_validator_present?(model, column)
35
+ else
36
+ presence_validator_present?(model, column)
37
+ end
38
+ end
39
+
40
+ def inclusion_validator_present?(model, column)
41
+ model.validators.any? do |validator|
42
+ validator.is_a?(ActiveModel::Validations::InclusionValidator) &&
43
+ validator.attributes.include?(column.name.to_sym) &&
44
+ !validator.options.fetch(:in, []).include?(nil)
45
+ end
46
+ end
47
+
48
+ def exclusion_validator_present?(model, column)
49
+ model.validators.any? do |validator|
50
+ validator.is_a?(ActiveModel::Validations::ExclusionValidator) &&
51
+ validator.attributes.include?(column.name.to_sym) &&
52
+ validator.options.fetch(:in, []).include?(nil)
53
+ end
54
+ end
55
+
56
+ def presence_validator_present?(model, column)
57
+ allowed_attributes = [column.name.to_sym]
58
+
59
+ belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
60
+ reflection.foreign_key == column.name
61
+ end
62
+ allowed_attributes << belongs_to.name.to_sym if belongs_to
63
+
32
64
  model.validators.any? do |validator|
33
65
  validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
34
- validator.attributes.include?(column.name.to_sym)
66
+ (validator.attributes & allowed_attributes).present?
35
67
  end
36
68
  end
37
69
  end
@@ -36,7 +36,12 @@ module ActiveRecordDoctor
36
36
  validator.options[:if].nil? &&
37
37
  validator.options[:unless].nil? &&
38
38
  validator.options[:conditions].nil? &&
39
- validator.options[:case_sensitive]
39
+
40
+ # In Rails 6, default option values are no longer explicitly set on
41
+ # options so if the key is absent we must fetch the default value
42
+ # ourselves. case_sensitive is the default in 4.2+ so it's safe to
43
+ # put true literally.
44
+ validator.options.fetch(:case_sensitive, true)
40
45
  end
41
46
 
42
47
  def unique_index?(table_name, columns, scope)
@@ -6,12 +6,27 @@ module ActiveRecordDoctor
6
6
  def run
7
7
  eager_load!
8
8
 
9
+ # If we can't list views due to old Rails version or unsupported
10
+ # database then existing_views is nil. We inform the caller we haven't
11
+ # consulted views so that it can display an appropriate warning.
12
+ existing_views = views
13
+
9
14
  offending_models = models.select do |model|
10
15
  model.table_name.present? &&
11
- !model.connection.tables.include?(model.table_name)
16
+ !tables.include?(model.table_name) &&
17
+ existing_views &&
18
+ !existing_views.include?(model.table_name)
19
+ end.map do |model|
20
+ [model.name, model.table_name]
12
21
  end
13
22
 
14
- [offending_models, offending_models.blank?]
23
+ [
24
+ [
25
+ offending_models, # Actual results
26
+ !existing_views.nil? # true if views were checked, false otherwise
27
+ ],
28
+ offending_models.blank?
29
+ ]
15
30
  end
16
31
  end
17
32
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordDoctor
2
- VERSION = "1.6.0"
2
+ VERSION = "1.7.0"
3
3
  end
@@ -7,6 +7,7 @@ require "active_record_doctor/tasks/unindexed_deleted_at"
7
7
  require "active_record_doctor/tasks/missing_unique_indexes"
8
8
  require "active_record_doctor/tasks/missing_presence_validation"
9
9
  require "active_record_doctor/tasks/missing_non_null_constraint"
10
+ require "active_record_doctor/tasks/incorrect_boolean_presence_validation"
10
11
 
11
12
  namespace :active_record_doctor do
12
13
  def mount(task_class)
@@ -0,0 +1,33 @@
1
+ require 'test_helper'
2
+
3
+ require 'active_record_doctor/tasks/incorrect_boolean_presence_validation'
4
+
5
+ class ActiveRecordDoctor::Tasks::IncorrectBooleanPresenceValidationTest < ActiveSupport::TestCase
6
+ def test_presence_true_is_reported_on_boolean_only
7
+ Temping.create(:users, temporary: false) do
8
+ # email is a non-boolean column whose presence CAN be validated in the
9
+ # usual way. We include it in the test model to ensure the task reports
10
+ # only boolean columns.
11
+ validates :email, :active, presence: true
12
+
13
+ with_columns do |t|
14
+ t.string :email, null: false
15
+ t.boolean :active, null: false
16
+ end
17
+ end
18
+
19
+ assert_equal({ 'User' => ['active'] }, run_task)
20
+ end
21
+
22
+ def test_inclusion_is_not_reported
23
+ Temping.create(:users, temporary: false) do
24
+ validates :active, inclusion: { in: [true, false] }
25
+
26
+ with_columns do |t|
27
+ t.boolean :active, null: false
28
+ end
29
+ end
30
+
31
+ assert_equal({}, run_task)
32
+ end
33
+ end
@@ -15,6 +15,19 @@ class ActiveRecordDoctor::Tasks::MissingNonNullConstraintTest < ActiveSupport::T
15
15
  assert_equal({ 'users' => ['name'] }, run_task)
16
16
  end
17
17
 
18
+ def test_association_presence_true_and_null_true
19
+ Temping.create(:companies, temporary: false)
20
+ Temping.create(:users, temporary: false) do
21
+ belongs_to :company, required: true
22
+
23
+ with_columns do |t|
24
+ t.references :company
25
+ end
26
+ end
27
+
28
+ assert_equal({ 'users' => ['company_id'] }, run_task)
29
+ end
30
+
18
31
  def test_presence_true_and_null_false
19
32
  Temping.create(:users, temporary: false) do
20
33
  validates :name, presence: true
@@ -35,6 +35,67 @@ class ActiveRecordDoctor::Tasks::MissingPresenceValidationTest < ActiveSupport::
35
35
  assert_equal({}, run_task)
36
36
  end
37
37
 
38
+ def test_non_null_column_is_not_reported_if_association_validation_present
39
+ Temping.create(:companies, temporary: false)
40
+ Temping.create(:users, temporary: false) do
41
+ belongs_to :company, required: true
42
+
43
+ with_columns do |t|
44
+ t.references :company, null: false
45
+ end
46
+ end
47
+
48
+ assert_equal({}, run_task)
49
+ end
50
+
51
+ def test_non_null_boolean_is_reported_if_nil_included
52
+ Temping.create(:users, temporary: false) do
53
+ validates :active, inclusion: { in: [nil, true, false] }
54
+
55
+ with_columns do |t|
56
+ t.boolean :active, null: false
57
+ end
58
+ end
59
+
60
+ assert_equal({ 'User' => ['active'] }, run_task)
61
+ end
62
+
63
+ def test_non_null_boolean_is_not_reported_if_nil_not_included
64
+ Temping.create(:users, temporary: false) do
65
+ validates :active, inclusion: { in: [true, false] }
66
+
67
+ with_columns do |t|
68
+ t.boolean :active, null: false
69
+ end
70
+ end
71
+
72
+ assert_equal({}, run_task)
73
+ end
74
+
75
+ def test_non_null_boolean_is_not_reported_if_nil_excluded
76
+ Temping.create(:users, temporary: false) do
77
+ validates :active, exclusion: { in: [nil] }
78
+
79
+ with_columns do |t|
80
+ t.boolean :active, null: false
81
+ end
82
+ end
83
+
84
+ assert_equal({}, run_task)
85
+ end
86
+
87
+ def test_non_null_boolean_is_reported_if_nil_not_excluded
88
+ Temping.create(:users, temporary: false) do
89
+ validates :active, exclusion: { in: [false] }
90
+
91
+ with_columns do |t|
92
+ t.boolean :active, null: false
93
+ end
94
+ end
95
+
96
+ assert_equal({ 'User' => ['active'] }, run_task)
97
+ end
98
+
38
99
  def test_timestamps_are_not_reported
39
100
  Temping.create(:users, temporary: false) do
40
101
  validates :name, presence: true
@@ -0,0 +1,51 @@
1
+ require 'test_helper'
2
+
3
+ require 'active_record_doctor/tasks/undefined_table_references'
4
+
5
+ class ActiveRecordDoctor::Tasks::UndefinedTableReferencesTest < ActiveSupport::TestCase
6
+ def test_table_exists
7
+ # No columns needed, just the table.
8
+ Temping.create(:users, temporary: false)
9
+
10
+ assert_equal([[], true], run_task)
11
+ end
12
+
13
+ def test_table_does_not_exist
14
+ # No columns needed, just the table.
15
+ Temping.create(:users, temporary: false)
16
+
17
+ # We drop the underlying table to make the model invalid.
18
+ ActiveRecord::Base.connection.drop_table(User.table_name)
19
+
20
+ # We wrap the assertion in begin/ensure because we must recreate the
21
+ # table as otherwise Temping will raise an error. Assertion errors are
22
+ # signalled via exceptions which we shouldn't swallow if we don't want to
23
+ # break the test suite hence the choice of begin/ensure.
24
+ begin
25
+ assert_equal([[[User.name, User.table_name]], true], run_task)
26
+ ensure
27
+ ActiveRecord::Base.connection.create_table(User.table_name)
28
+ end
29
+ end
30
+
31
+ def test_view_instead_of_table
32
+ # No columns needed, just the table.
33
+ Temping.create(:users, temporary: false)
34
+
35
+ # We replace the underlying table with a view. The view doesn't have to be
36
+ # backed by an actual table - it can simply return a predefined tuple.
37
+ ActiveRecord::Base.connection.drop_table(User.table_name)
38
+ ActiveRecord::Base.connection.execute("CREATE VIEW users AS SELECT 1")
39
+
40
+ # We wrap the assertion in begin/ensure because we must recreate the
41
+ # table as otherwise Temping will raise an error. Assertion errors are
42
+ # signalled via exceptions which we shouldn't swallow if we don't want to
43
+ # break the test suite hence the choice of begin/ensure.
44
+ begin
45
+ assert_equal([[], true], run_task)
46
+ ensure
47
+ ActiveRecord::Base.connection.execute("DROP VIEW users")
48
+ ActiveRecord::Base.connection.create_table(User.table_name)
49
+ end
50
+ end
51
+ end