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 +4 -4
- data/CHANGELOG.md +1 -0
- data/{LICENSE → MIT-LICENSE} +0 -0
- data/README.md +92 -2
- data/lib/schema_expectations/active_record/column_reflector.rb +9 -0
- data/lib/schema_expectations/active_record/validation_reflector.rb +48 -4
- data/lib/schema_expectations/rspec_matchers/validate_schema_nullable.rb +9 -2
- data/lib/schema_expectations/rspec_matchers/validate_schema_uniqueness.rb +153 -0
- data/lib/schema_expectations/rspec_matchers.rb +1 -0
- data/lib/schema_expectations/version.rb +1 -1
- data/spec/lib/schema_expectations/active_record/column_reflector_spec.rb +27 -0
- data/spec/lib/schema_expectations/active_record/validation_reflector_spec.rb +81 -19
- data/spec/lib/schema_expectations/rspec_matchers/validate_schema_uniqueness_spec.rb +199 -0
- data/spec/support/active_record_helpers.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd5eeeafd64927fadc0781314d97e9c104efcc63
|
4
|
+
data.tar.gz: 67ea5f0dec0ac25d66dadc768a233e3446f961a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 746d0312374307e14a451bd7186d6561c99de12d7247728fcdec00e8bda23976ee330046c33b90ef90fdd24ae03c09733a9933d0e62be52e0e79ef18512e20cc
|
7
|
+
data.tar.gz: 1cec0c037a5ea1fbb5dd6fa0b21c5f6914f104b55cb9a38a45dde97895247c501ba3acd269f20a635cfd8b1958819c0a282b9c474e385803ed00641e6ab422bd
|
data/CHANGELOG.md
CHANGED
data/{LICENSE → MIT-LICENSE}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -4,8 +4,98 @@
|
|
4
4
|
[](https://codeclimate.com/github/emma-borhanian/schema_expectations)
|
5
5
|
[](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
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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.
|
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
|
@@ -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 '#
|
44
|
+
specify '#for_unique_scope' do
|
41
45
|
Record.instance_eval do
|
42
|
-
validates :
|
43
|
-
validates :
|
44
|
-
validates :
|
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.
|
52
|
-
expect(validation_reflector.
|
53
|
-
expect(validation_reflector.
|
54
|
-
expect(validation_reflector.
|
55
|
-
expect(validation_reflector.
|
56
|
-
expect(validation_reflector.
|
57
|
-
expect(validation_reflector.
|
58
|
-
|
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
|
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.
|
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-
|
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
|