schema_expectations 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 18218491d35e976cf13367e3d30d60e952886de6
4
- data.tar.gz: fa58281615fdda22fefb031a987f85001c8fe672
3
+ metadata.gz: bd5eeeafd64927fadc0781314d97e9c104efcc63
4
+ data.tar.gz: 67ea5f0dec0ac25d66dadc768a233e3446f961a1
5
5
  SHA512:
6
- metadata.gz: 1eb5dc8161b1b91c08d40225bbd03d58e4153a6ea51af88420afbb91549837ca861849e486bb8e41f703097e5566ff6a344785829ac772a24c5a36995aa3992d
7
- data.tar.gz: 8cc5ba8420797a7d5b10e8c25ca2ca309b2b8ecd7204f804296960988d7b835fc99ced2ff607f8b32c384ab5f407de6cf1d37cd3b103cc53fc1c23e0ba2052a9
6
+ metadata.gz: 746d0312374307e14a451bd7186d6561c99de12d7247728fcdec00e8bda23976ee330046c33b90ef90fdd24ae03c09733a9933d0e62be52e0e79ef18512e20cc
7
+ data.tar.gz: 1cec0c037a5ea1fbb5dd6fa0b21c5f6914f104b55cb9a38a45dde97895247c501ba3acd269f20a635cfd8b1958819c0a282b9c474e385803ed00641e6ab422bd
data/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Schema Expectations Changelog
2
2
 
3
3
  ### git master
4
+ - added `validate_schema_uniqueness`
4
5
 
5
6
  ### 0.2.0 (Febuary 18, 2015)
6
7
 
File without changes
data/README.md CHANGED
@@ -4,8 +4,98 @@
4
4
  [![Test Coverage](https://codeclimate.com/github/emma-borhanian/schema_expectations/badges/coverage.svg)](https://codeclimate.com/github/emma-borhanian/schema_expectations)
5
5
  [![Dependency Status](https://gemnasium.com/emma-borhanian/schema_expectations.svg)](https://gemnasium.com/emma-borhanian/schema_expectations)
6
6
 
7
- # Work in progress
8
-
9
7
  # Schema Expectations
10
8
 
11
9
  Allows you to test whether your database schema matches the validations in your ActiveRecord models.
10
+
11
+ # Installation
12
+
13
+ Add `schema_expectations` to your Gemfile:
14
+
15
+ ```ruby
16
+ group :test do
17
+ gem 'schema_expectations'
18
+ end
19
+ ```
20
+
21
+ # Usage with RSpec
22
+
23
+ ## Validating uniqueness constraints
24
+
25
+ The `validate_schema_uniqueness` matcher tests that an ActiveRecord model
26
+ has uniqueness validation on columns with database uniqueness constraints,
27
+ and vice versa.
28
+
29
+ For example, we can assert that the model and database are consistent
30
+ on whether `record_type` and `record_id` should be unique:
31
+
32
+ ```ruby
33
+ create_table :records do |t|
34
+ t.integer :record_type
35
+ t.integer :record_id
36
+ t.index [:record_type, :record_id], unique: true
37
+ end
38
+
39
+ class Record < ActiveRecord::Base
40
+ validates :record_type, uniqueness: { scope: :record_id }
41
+ end
42
+
43
+ # RSpec
44
+ describe Record do
45
+ it { should validate_schema_uniqueness }
46
+ end
47
+ ```
48
+
49
+ You can restrict the columns tested:
50
+
51
+ ```ruby
52
+ # RSpec
53
+ describe Record do
54
+ it { should validate_schema_uniqueness.only(:record_id, :record_type) }
55
+ it { should validate_schema_uniqueness.except(:record_id, :record_type) }
56
+ end
57
+ ```
58
+
59
+ note: if you exclude a column, then every unique scope which includes it will be completely ignored,
60
+ regardless of whether that scope includes other non-excluded columns. Only works similarly, in
61
+ that it will ignore any scope which contains columns not in the list
62
+
63
+ ## Validating presence constraints
64
+
65
+ The `validate_schema_nullable` matcher tests that an ActiveRecord model
66
+ has unconditional presence validation on columns with `NOT NULL` constraints,
67
+ and vice versa.
68
+
69
+ For example, we can assert that the model and database are consistent
70
+ on whether `Record#name` should be present:
71
+
72
+ ```ruby
73
+ create_table :records do |t|
74
+ t.string :name, null: false
75
+ end
76
+
77
+ class Record < ActiveRecord::Base
78
+ validates :name, presence: true
79
+ end
80
+
81
+ # RSpec
82
+ describe Record do
83
+ it { should validate_schema_nullable }
84
+ end
85
+ ```
86
+
87
+ You can restrict the columns tested:
88
+
89
+ ```ruby
90
+ # RSpec
91
+ describe Record do
92
+ it { should validate_schema_nullable.only(:name) }
93
+ it { should validate_schema_nullable.except(:name) }
94
+ end
95
+ ```
96
+
97
+ The primary key and timestamp columns are automatically skipped.
98
+
99
+ # License
100
+
101
+ [MIT License](MIT-LICENSE)
@@ -24,6 +24,10 @@ module SchemaExpectations
24
24
  new_with_columns @columns.reject(&method(:present_default_column?))
25
25
  end
26
26
 
27
+ def unique_scopes
28
+ unique_indexes.map { |index| index.columns.map(&:to_sym).sort }.sort
29
+ end
30
+
27
31
  private
28
32
 
29
33
  def new_with_columns(columns)
@@ -87,6 +91,11 @@ module SchemaExpectations
87
91
  def all_timestamp_attributes
88
92
  @all_timestamp_attributes ||= Record.new.send(:all_timestamp_attributes).map(&:to_sym)
89
93
  end
94
+
95
+ def unique_indexes
96
+ indexes = @model.connection.indexes(@model.table_name)
97
+ indexes.select(&:unique)
98
+ end
90
99
  end
91
100
  end
92
101
  end
@@ -3,7 +3,9 @@ require 'schema_expectations/util'
3
3
  module SchemaExpectations
4
4
  module ActiveRecord
5
5
  class ValidationReflector # :nodoc:
6
- CONDITIONAL_OPTIONS = %i(on if unless allow_nil allow_blank)
6
+ CONDITIONAL_OPTIONS = %i(on if unless)
7
+ ALLOW_NIL_OPTIONS = %i(allow_nil allow_blank)
8
+ ALLOW_EMPTY_OPTIONS = %i(allow_blank)
7
9
 
8
10
  def initialize(model, validators = nil)
9
11
  @model = model
@@ -14,10 +16,21 @@ module SchemaExpectations
14
16
  @validators.flat_map(&:attributes).uniq
15
17
  end
16
18
 
19
+ def unique_scopes
20
+ validators = validators_with_kind :uniqueness
21
+ validators.flat_map(&method(:validator_scopes))
22
+ end
23
+
17
24
  def conditions_for_attribute(attribute)
18
- validators = validators_for_attribute(attribute)
19
- validators -= validators_without_options CONDITIONAL_OPTIONS
20
- Util.slice_hash(validators.first.options, *CONDITIONAL_OPTIONS) if validators.first
25
+ options_for_attribute attribute, CONDITIONAL_OPTIONS
26
+ end
27
+
28
+ def allow_nil_conditions_for_attribute(attribute)
29
+ options_for_attribute attribute, ALLOW_NIL_OPTIONS
30
+ end
31
+
32
+ def allow_empty_conditions_for_attribute(attribute)
33
+ options_for_attribute attribute, ALLOW_EMPTY_OPTIONS
21
34
  end
22
35
 
23
36
  def presence
@@ -28,12 +41,35 @@ module SchemaExpectations
28
41
  new_with_validators validators_without_options CONDITIONAL_OPTIONS
29
42
  end
30
43
 
44
+ def disallow_nil
45
+ new_with_validators validators_without_options ALLOW_NIL_OPTIONS
46
+ end
47
+
48
+ def disallow_empty
49
+ new_with_validators validators_without_options ALLOW_EMPTY_OPTIONS
50
+ end
51
+
52
+ def for_unique_scope(scope)
53
+ scope = scope.sort
54
+ validators = validators_with_kind :uniqueness
55
+ validators.select! do |validator|
56
+ validator_scopes(validator).include? scope
57
+ end
58
+ new_with_validators validators
59
+ end
60
+
31
61
  private
32
62
 
33
63
  def new_with_validators(validators)
34
64
  ValidationReflector.new(@model, validators)
35
65
  end
36
66
 
67
+ def options_for_attribute(attribute, option_keys)
68
+ validators = validators_for_attribute(attribute)
69
+ validators -= validators_without_options option_keys
70
+ Util.slice_hash(validators.first.options, *option_keys) if validators.first
71
+ end
72
+
37
73
  def validators_with_kind(kind)
38
74
  @validators.select do |validator|
39
75
  validator.kind == kind
@@ -54,6 +90,14 @@ module SchemaExpectations
54
90
  validator.attributes.include? attribute
55
91
  end
56
92
  end
93
+
94
+ def validator_scopes(validator)
95
+ scopes = validator.attributes.map do |attribute|
96
+ scope = [attribute] + Array(validator.options[:scope])
97
+ scope.map(&:to_sym).sort
98
+ end
99
+ scopes.sort
100
+ end
57
101
  end
58
102
  end
59
103
  end
@@ -57,7 +57,9 @@ module SchemaExpectations
57
57
  end
58
58
 
59
59
  (@not_null_column_names - @present_column_names).each do |column_name|
60
- if conditions = validator_conditions_for_column_name(column_name)
60
+ conditions = validator_allow_nil_conditions_for_column_name(column_name) ||
61
+ validator_conditions_for_column_name(column_name)
62
+ if conditions
61
63
  errors << "#{column_name} is NOT NULL but its presence validator was conditional: #{conditions.inspect}"
62
64
  else
63
65
  errors << "#{column_name} is NOT NULL but has no presence validation"
@@ -106,7 +108,8 @@ module SchemaExpectations
106
108
  end
107
109
 
108
110
  def present_attributes
109
- @validation_reflector.presence.unconditional.attributes
111
+ @validation_reflector.presence.
112
+ unconditional.disallow_nil.attributes
110
113
  end
111
114
 
112
115
  def present_column_names
@@ -132,6 +135,10 @@ module SchemaExpectations
132
135
  column_names
133
136
  end
134
137
 
138
+ def validator_allow_nil_conditions_for_column_name(column_name)
139
+ @validation_reflector.allow_nil_conditions_for_attribute column_name_to_attribute column_name
140
+ end
141
+
135
142
  def validator_conditions_for_column_name(column_name)
136
143
  @validation_reflector.conditions_for_attribute column_name_to_attribute column_name
137
144
  end
@@ -0,0 +1,153 @@
1
+ require 'rspec/expectations'
2
+ require 'schema_expectations/active_record/validation_reflector'
3
+ require 'schema_expectations/active_record/column_reflector'
4
+
5
+ module SchemaExpectations
6
+ module RSpecMatchers
7
+ # The `validate_schema_uniqueness` matcher tests that an ActiveRecord model
8
+ # has uniqueness validation on columns with database uniqueness constraints,
9
+ # and vice versa.
10
+ #
11
+ # For example, we can assert that the model and database are consistent
12
+ # on whether `record_type` and `record_id` should be unique:
13
+ #
14
+ # create_table :records do |t|
15
+ # t.integer :record_type
16
+ # t.integer :record_id
17
+ # t.index [:record_type, :record_id], unique: true
18
+ # end
19
+
20
+ # class Record < ActiveRecord::Base
21
+ # validates :record_type, uniqueness: { scope: :record_id }
22
+ # end
23
+ #
24
+ # # RSpec
25
+ # describe Record do
26
+ # it { should validate_schema_uniqueness }
27
+ # end
28
+ #
29
+ # You can restrict the columns tested:
30
+ #
31
+ # # RSpec
32
+ # describe Record do
33
+ # it { should validate_schema_uniqueness.only(:record_id, :record_type) }
34
+ # it { should validate_schema_uniqueness.except(:record_id, :record_type) }
35
+ # end
36
+ #
37
+ # note: if you exclude a column, then every unique scope which includes it will be completely ignored,
38
+ # regardless of whether that scope includes other non-excluded columns. Only works similarly, in
39
+ # that it will ignore any scope which contains columns not in the list
40
+ #
41
+ # @return [ValidateSchemaUniquenessMatcher]
42
+ def validate_schema_uniqueness
43
+ ValidateSchemaUniquenessMatcher.new
44
+ end
45
+
46
+ class ValidateSchemaUniquenessMatcher
47
+ def matches?(model)
48
+ @model = cast_model model
49
+ @validation_reflector = ActiveRecord::ValidationReflector.new(@model)
50
+ @column_reflector = ActiveRecord::ColumnReflector.new(@model)
51
+ @validator_unique_scopes = filter_scopes(validator_unique_scopes).map(&:sort).sort
52
+ @schema_unique_scopes = filter_scopes(schema_unique_scopes).map(&:sort).sort
53
+ @validator_unique_scopes == @schema_unique_scopes
54
+ end
55
+
56
+ def failure_message
57
+ errors = []
58
+
59
+ (@validator_unique_scopes - @schema_unique_scopes).each do |scope|
60
+ errors << "scope #{scope.inspect} has unconditional uniqueness validation but is missing a unique database index"
61
+ end
62
+
63
+ (@schema_unique_scopes - @validator_unique_scopes).each do |scope|
64
+ conditions = validator_conditions_for_scope(scope) ||
65
+ validator_allow_empty_conditions_for_scope(scope)
66
+ if conditions
67
+ errors << "scope #{scope.inspect} has a unique index but its uniqueness validator was conditional: #{conditions.inspect}"
68
+ else
69
+ errors << "scope #{scope.inspect} has a unique index but no uniqueness validation"
70
+ end
71
+ end
72
+
73
+ errors.join(', ')
74
+ end
75
+
76
+ def failure_message_when_negated
77
+ 'should not match unique indexes with its uniqueness validation but does'
78
+ end
79
+
80
+ def description
81
+ 'validate unique indexes have uniqueness validation'
82
+ end
83
+
84
+ # Specifies a list of columns to restrict matcher
85
+ #
86
+ # Any unique scope which includes a column not in this list will be ignored
87
+ #
88
+ # @return [ValidateSchemaUniquenessMatcher] self
89
+ def only(*args)
90
+ fail 'cannot use only and except' if @except
91
+ @only = Array(args)
92
+ fail 'empty only list' if @only.empty?
93
+ self
94
+ end
95
+
96
+ # Specifies a list of columns for matcher to ignore
97
+ #
98
+ # Any unique scope which includes one of these columns will be ignored
99
+ #
100
+ # @return [ValidateSchemaUniquenessMatcher] self
101
+ def except(*args)
102
+ fail 'cannot use only and except' if @only
103
+ @except = Array(args)
104
+ fail 'empty except list' if @except.empty?
105
+ self
106
+ end
107
+
108
+ private
109
+
110
+ def cast_model(model)
111
+ model = model.class if model.is_a?(::ActiveRecord::Base)
112
+ unless model.is_a?(Class) && model.ancestors.include?(::ActiveRecord::Base)
113
+ fail "#{model.inspect} does not inherit from ActiveRecord::Base"
114
+ end
115
+ model
116
+ end
117
+
118
+ def validator_unique_scopes
119
+ @validation_reflector.unconditional.disallow_empty.unique_scopes
120
+ end
121
+
122
+ def schema_unique_scopes
123
+ @column_reflector.unique_scopes
124
+ end
125
+
126
+ def filter_scopes(scopes)
127
+ if @only
128
+ scopes.select { |scope| (scope - @only).empty? }
129
+ elsif @except
130
+ scopes.select { |scope| (scope & @except).empty? }
131
+ else
132
+ scopes
133
+ end
134
+ end
135
+
136
+ def validator_conditions_for_scope(scope)
137
+ reflector = @validation_reflector.for_unique_scope(scope)
138
+ conditions = reflector.attributes.map do |attribute|
139
+ reflector.conditions_for_attribute attribute
140
+ end
141
+ conditions.compact.first
142
+ end
143
+
144
+ def validator_allow_empty_conditions_for_scope(scope)
145
+ reflector = @validation_reflector.for_unique_scope(scope)
146
+ conditions = reflector.attributes.map do |attribute|
147
+ reflector.allow_empty_conditions_for_attribute attribute
148
+ end
149
+ conditions.compact.first
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1 +1,2 @@
1
1
  require 'schema_expectations/rspec_matchers/validate_schema_nullable'
2
+ require 'schema_expectations/rspec_matchers/validate_schema_uniqueness'
@@ -1,3 +1,3 @@
1
1
  module SchemaExpectations
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -145,6 +145,33 @@ module SchemaExpectations
145
145
  expect(column_reflector.for_attributes(*%i(record missing other)).column_names).to eq %i(record_id other)
146
146
  end
147
147
  end
148
+
149
+ specify '#unique_scopes' do
150
+ create_table :records do |t|
151
+ t.string :a
152
+ t.string :b
153
+ t.string :c
154
+ t.string :d
155
+ t.string :not_unique
156
+ t.string :other
157
+ end
158
+
159
+ add_index :records, :not_unique
160
+ add_index :records, :a, unique: true
161
+ add_index :records, %i(a b), unique: true
162
+ add_index :records, %i(c a b), unique: true
163
+ add_index :records, %i(d b), unique: true
164
+ add_index :records, %i(a not_unique)
165
+
166
+ stub_const('Record', Class.new(::ActiveRecord::Base))
167
+
168
+ expect(column_reflector.unique_scopes).to eq [
169
+ %i(a),
170
+ %i(a b),
171
+ %i(a b c),
172
+ %i(b d)
173
+ ]
174
+ end
148
175
  end
149
176
  end
150
177
  end
@@ -27,35 +27,97 @@ module SchemaExpectations
27
27
  present not_present conditional_1 conditional_2
28
28
  conditional_3 conditional_4 conditional_5)
29
29
 
30
- expect(validation_reflector.unconditional.attributes).to eq %i(present not_present)
30
+ expect(validation_reflector.unconditional.attributes).to eq %i(present not_present conditional_4 conditional_5)
31
+
32
+ expect(validation_reflector.disallow_nil.attributes).to eq %i(present not_present conditional_1 conditional_2 conditional_3)
33
+
34
+ expect(validation_reflector.disallow_empty.attributes).to eq %i(present not_present conditional_1 conditional_2 conditional_3 conditional_4)
31
35
 
32
36
  expect(validation_reflector.presence.attributes).to eq %i(
33
37
  present conditional_1 conditional_2
34
38
  conditional_3 conditional_4 conditional_5)
35
39
 
36
- expect(validation_reflector.unconditional.presence.attributes).to eq %i(present)
37
- expect(validation_reflector.presence.unconditional.attributes).to eq %i(present)
40
+ expect(validation_reflector.unconditional.presence.disallow_nil.attributes).to eq %i(present)
41
+ expect(validation_reflector.presence.unconditional.disallow_nil.attributes).to eq %i(present)
38
42
  end
39
43
 
40
- specify '#conditions_for_attribute' do
44
+ specify '#for_unique_scope' do
41
45
  Record.instance_eval do
42
- validates :present, presence: true
43
- validates :not_present, length: { minimum: 1 }
44
- validates :conditional_1, presence: true, on: :create
45
- validates :conditional_2, presence: true, if: ->{ false }
46
- validates :conditional_3, presence: true, unless: ->{ false }
47
- validates :conditional_4, presence: true, allow_nil: true
48
- validates :conditional_5, presence: true, allow_blank: true
46
+ validates :a, uniqueness: true
47
+ validates :a, :d, uniqueness: { scope: %i(b) }
48
+ validates :c, uniqueness: { scope: %i(a b) }
49
49
  end
50
50
 
51
- expect(validation_reflector.conditions_for_attribute(:missing)).to be_nil
52
- expect(validation_reflector.conditions_for_attribute(:present)).to be_nil
53
- expect(validation_reflector.conditions_for_attribute(:not_present)).to be_nil
54
- expect(validation_reflector.conditions_for_attribute(:conditional_1)).to eq(on: :create)
55
- expect(validation_reflector.conditions_for_attribute(:conditional_2).keys).to eq [:if]
56
- expect(validation_reflector.conditions_for_attribute(:conditional_3).keys).to eq [:unless]
57
- expect(validation_reflector.conditions_for_attribute(:conditional_4)).to eq(allow_nil: true)
58
- expect(validation_reflector.conditions_for_attribute(:conditional_5)).to eq(allow_blank: true)
51
+ expect(validation_reflector.for_unique_scope(%i(missing)).attributes).to be_empty
52
+ expect(validation_reflector.for_unique_scope(%i(a)).attributes).to eq %i(a)
53
+ expect(validation_reflector.for_unique_scope(%i(a b)).attributes).to eq %i(a d)
54
+ expect(validation_reflector.for_unique_scope(%i(b a)).attributes).to eq %i(a d)
55
+ expect(validation_reflector.for_unique_scope(%i(b d)).attributes).to eq %i(a d)
56
+ expect(validation_reflector.for_unique_scope(%i(a b c)).attributes).to eq %i(c)
57
+ expect(validation_reflector.for_unique_scope(%i(c b a)).attributes).to eq %i(c)
58
+ end
59
+
60
+ specify '#unique_scopes' do
61
+ Record.instance_eval do
62
+ validates :a, uniqueness: true
63
+ validates :b, uniqueness: { scope: %i(a) }
64
+ validates :c, uniqueness: { scope: %i(a b) }
65
+ validates :d, uniqueness: { scope: %i(b) }
66
+ end
67
+
68
+ expect(validation_reflector.unique_scopes).to eq [
69
+ %i(a),
70
+ %i(a b),
71
+ %i(a b c),
72
+ %i(b d)
73
+ ]
74
+ end
75
+
76
+ context 'conditions' do
77
+ before do
78
+ Record.instance_eval do
79
+ validates :present, presence: true
80
+ validates :not_present, length: { minimum: 1 }
81
+ validates :conditional_1, presence: true, on: :create
82
+ validates :conditional_2, presence: true, if: ->{ false }
83
+ validates :conditional_3, presence: true, unless: ->{ false }
84
+ validates :conditional_4, presence: true, allow_nil: true
85
+ validates :conditional_5, presence: true, allow_blank: true
86
+ end
87
+ end
88
+
89
+ specify '#conditions_for_attribute' do
90
+ expect(validation_reflector.conditions_for_attribute(:missing)).to be_nil
91
+ expect(validation_reflector.conditions_for_attribute(:present)).to be_nil
92
+ expect(validation_reflector.conditions_for_attribute(:not_present)).to be_nil
93
+ expect(validation_reflector.conditions_for_attribute(:conditional_1)).to eq(on: :create)
94
+ expect(validation_reflector.conditions_for_attribute(:conditional_2).keys).to eq [:if]
95
+ expect(validation_reflector.conditions_for_attribute(:conditional_3).keys).to eq [:unless]
96
+ expect(validation_reflector.conditions_for_attribute(:conditional_4)).to be_nil
97
+ expect(validation_reflector.conditions_for_attribute(:conditional_5)).to be_nil
98
+ end
99
+
100
+ specify '#allow_nil_conditions_for_attribute' do
101
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:missing)).to be_nil
102
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:present)).to be_nil
103
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:not_present)).to be_nil
104
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:conditional_1)).to be_nil
105
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:conditional_2)).to be_nil
106
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:conditional_3)).to be_nil
107
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:conditional_4)).to eq(allow_nil: true)
108
+ expect(validation_reflector.allow_nil_conditions_for_attribute(:conditional_5)).to eq(allow_blank: true)
109
+ end
110
+
111
+ specify '#allow_empty_conditions_for_column_name' do
112
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:missing)).to be_nil
113
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:present)).to be_nil
114
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:not_present)).to be_nil
115
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:conditional_1)).to be_nil
116
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:conditional_2)).to be_nil
117
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:conditional_3)).to be_nil
118
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:conditional_4)).to be_nil
119
+ expect(validation_reflector.allow_empty_conditions_for_attribute(:conditional_5)).to eq(allow_blank: true)
120
+ end
59
121
  end
60
122
  end
61
123
  end
@@ -0,0 +1,199 @@
1
+ require 'spec_helper'
2
+
3
+ describe SchemaExpectations::RSpecMatchers::ValidateSchemaUniquenessMatcher, :active_record do
4
+ shared_examples_for 'Record' do
5
+ def validates(*args)
6
+ Record.instance_eval do
7
+ validates *args
8
+ end
9
+ end
10
+
11
+ let(:unique_scope) { [:unique_1_of_1] }
12
+
13
+ before do
14
+ create_table :records do |t|
15
+ t.string :no_index
16
+ t.string :index_not_unique
17
+
18
+ unique_scope.each do |column_name|
19
+ t.string column_name
20
+ end
21
+ end
22
+
23
+ add_index :records, :index_not_unique
24
+ add_index :records, unique_scope, unique: true
25
+
26
+ stub_const('Record', Class.new(ActiveRecord::Base))
27
+ end
28
+
29
+ subject(:record) { Record }
30
+
31
+ context 'with no validations' do
32
+ it { is_expected.to_not validate_schema_uniqueness }
33
+
34
+ specify 'error messages' do
35
+ expect { is_expected.to validate_schema_uniqueness }.to(
36
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
37
+ "scope #{unique_scope.inspect} has a unique index but no uniqueness validation")
38
+ )
39
+ end
40
+
41
+ specify '#only' do
42
+ is_expected.to validate_schema_uniqueness.only(unique_scope.first) if unique_scope.size > 1
43
+ is_expected.to_not validate_schema_uniqueness.only(*unique_scope)
44
+ is_expected.to validate_schema_uniqueness.only(:no_index, :index_not_unique)
45
+ is_expected.to_not validate_schema_uniqueness.only(*unique_scope, :no_index, :index_not_unique)
46
+ end
47
+
48
+ specify '#except' do
49
+ is_expected.to validate_schema_uniqueness.except(unique_scope.first)
50
+ is_expected.to validate_schema_uniqueness.except(*unique_scope)
51
+ is_expected.to_not validate_schema_uniqueness.except(:no_index, :index_not_unique)
52
+ is_expected.to validate_schema_uniqueness.except(*unique_scope, :no_index, :index_not_unique)
53
+ end
54
+ end
55
+
56
+ context 'validating uniqueness on unique index' do
57
+ before { validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) } }
58
+
59
+ it { is_expected.to validate_schema_uniqueness }
60
+
61
+ specify '#only' do
62
+ is_expected.to validate_schema_uniqueness.only(*unique_scope)
63
+ is_expected.to validate_schema_uniqueness.only(:no_index, :index_not_unique)
64
+ is_expected.to validate_schema_uniqueness.only(*unique_scope, :no_index, :index_not_unique)
65
+ end
66
+
67
+ specify '#except' do
68
+ is_expected.to validate_schema_uniqueness.except(*unique_scope)
69
+ is_expected.to validate_schema_uniqueness.except(:no_index, :index_not_unique)
70
+ end
71
+ end
72
+
73
+ context 'validating uniqueness on not-unique index' do
74
+ before do
75
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }
76
+ validates :index_not_unique, uniqueness: true
77
+ end
78
+
79
+ it { is_expected.to_not validate_schema_uniqueness }
80
+
81
+ specify 'error messages' do
82
+ expect { is_expected.to validate_schema_uniqueness }.to(
83
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
84
+ "scope [:index_not_unique] has unconditional uniqueness validation but is missing a unique database index")
85
+ )
86
+ end
87
+
88
+ specify '#only' do
89
+ is_expected.to_not validate_schema_uniqueness.only(:index_not_unique)
90
+ is_expected.to validate_schema_uniqueness.only(*unique_scope, :no_index)
91
+ end
92
+
93
+ specify '#except' do
94
+ is_expected.to_not validate_schema_uniqueness.except(*unique_scope, :no_index)
95
+ is_expected.to validate_schema_uniqueness.except(:index_not_unique)
96
+ end
97
+ end
98
+
99
+ context 'validating uniqueness on column without index' do
100
+ before do
101
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }
102
+ validates :no_index, uniqueness: true
103
+ end
104
+
105
+ it { is_expected.to_not validate_schema_uniqueness }
106
+
107
+ specify 'error messages' do
108
+ expect { is_expected.to validate_schema_uniqueness }.to(
109
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
110
+ "scope [:no_index] has unconditional uniqueness validation but is missing a unique database index")
111
+ )
112
+ end
113
+
114
+ specify '#only' do
115
+ is_expected.to_not validate_schema_uniqueness.only(:no_index)
116
+ is_expected.to validate_schema_uniqueness.only(*unique_scope, :index_not_unique)
117
+ end
118
+
119
+ specify '#except' do
120
+ is_expected.to_not validate_schema_uniqueness.except(*unique_scope, :index_not_unique)
121
+ is_expected.to validate_schema_uniqueness.except(:no_index)
122
+ end
123
+ end
124
+
125
+ specify '#failure_message_when_negated' do
126
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }
127
+
128
+ expect do
129
+ is_expected.to_not validate_schema_uniqueness
130
+ end.to raise_error 'should not match unique indexes with its uniqueness validation but does'
131
+ end
132
+
133
+ specify 'allows validators with allow_nil: true' do
134
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }, allow_nil: true
135
+
136
+ is_expected.to validate_schema_uniqueness
137
+ end
138
+
139
+ context 'ignores validators with' do
140
+ specify 'on: :create' do
141
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }, on: :create
142
+
143
+ expect { is_expected.to validate_schema_uniqueness }.to(
144
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
145
+ "scope #{unique_scope.inspect} has a unique index but its uniqueness validator was conditional: {:on=>:create}")
146
+ )
147
+ end
148
+
149
+ specify 'if: proc' do
150
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }, if: ->{ false }
151
+
152
+ expect { is_expected.to validate_schema_uniqueness }.to(
153
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
154
+ /\Ascope #{Regexp.escape(unique_scope.inspect)} has a unique index but its uniqueness validator was conditional: {:if=>\#<Proc:.*>}\z/)
155
+ )
156
+ end
157
+
158
+ specify 'unless: proc' do
159
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }, unless: ->{ false }
160
+
161
+ expect { is_expected.to validate_schema_uniqueness }.to(
162
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
163
+ /\Ascope #{Regexp.escape(unique_scope.inspect)} has a unique index but its uniqueness validator was conditional: {:unless=>\#<Proc:.*>}\z/)
164
+ )
165
+ end
166
+
167
+ specify 'allow_blank: true' do
168
+ validates unique_scope.first, uniqueness: { scope: unique_scope.drop(1) }, allow_blank: true
169
+
170
+ expect { is_expected.to validate_schema_uniqueness }.to(
171
+ raise_error(RSpec::Expectations::ExpectationNotMetError,
172
+ "scope #{unique_scope.inspect} has a unique index but its uniqueness validator was conditional: {:allow_blank=>true}")
173
+ )
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'called on class' do
179
+ include_examples 'Record' do
180
+ subject(:record) { Record }
181
+ end
182
+ end
183
+
184
+ context 'called on instance' do
185
+ include_examples 'Record' do
186
+ subject(:record) { Record.new }
187
+ end
188
+ end
189
+
190
+ context 'with two columns' do
191
+ include_examples 'Record' do
192
+ let(:unique_scope) { [:unique_1_of_2, :unique_2_of_2] }
193
+ end
194
+ end
195
+ specify 'called on unrecognized object' do
196
+ expect { expect(double('object')).to validate_schema_uniqueness }.
197
+ to raise_error /#<RSpec::Mocks::Double:0x\h* @name="object"> does not inherit from ActiveRecord::Base/
198
+ end
199
+ end
@@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/keys'
3
3
  module ActiveRecordHelpers
4
4
  extend Forwardable
5
5
 
6
- CONNECTION_DELEGATES = %i(create_table execute)
6
+ CONNECTION_DELEGATES = %i(create_table add_index execute)
7
7
 
8
8
  def connection
9
9
  ActiveRecord::Base.connection
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema_expectations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emma Borhanian
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-19 00:00:00.000000000 Z
11
+ date: 2015-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -259,7 +259,7 @@ files:
259
259
  - CHANGELOG.md
260
260
  - Gemfile
261
261
  - Guardfile
262
- - LICENSE
262
+ - MIT-LICENSE
263
263
  - README.md
264
264
  - Rakefile
265
265
  - gemfiles/activerecord_3.1.gemfile
@@ -278,6 +278,7 @@ files:
278
278
  - lib/schema_expectations/rspec.rb
279
279
  - lib/schema_expectations/rspec_matchers.rb
280
280
  - lib/schema_expectations/rspec_matchers/validate_schema_nullable.rb
281
+ - lib/schema_expectations/rspec_matchers/validate_schema_uniqueness.rb
281
282
  - lib/schema_expectations/util.rb
282
283
  - lib/schema_expectations/version.rb
283
284
  - schema_expectations.gemspec
@@ -286,6 +287,7 @@ files:
286
287
  - spec/lib/schema_expectations/active_record/validation_reflector_spec.rb
287
288
  - spec/lib/schema_expectations/config_spec.rb
288
289
  - spec/lib/schema_expectations/rspec_matchers/validate_schema_nullable_spec.rb
290
+ - spec/lib/schema_expectations/rspec_matchers/validate_schema_uniqueness_spec.rb
289
291
  - spec/lib/schema_expectations/util_spec.rb
290
292
  - spec/meta_spec.rb
291
293
  - spec/spec_helper.rb
@@ -322,6 +324,7 @@ test_files:
322
324
  - spec/lib/schema_expectations/active_record/validation_reflector_spec.rb
323
325
  - spec/lib/schema_expectations/config_spec.rb
324
326
  - spec/lib/schema_expectations/rspec_matchers/validate_schema_nullable_spec.rb
327
+ - spec/lib/schema_expectations/rspec_matchers/validate_schema_uniqueness_spec.rb
325
328
  - spec/lib/schema_expectations/util_spec.rb
326
329
  - spec/meta_spec.rb
327
330
  - spec/spec_helper.rb