schema_expectations 0.2.0 → 0.3.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
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