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 +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
|
[![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
|
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
|