nobrainer-rspec 1.0.1
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 +7 -0
- data/CHANGELOG.md +37 -0
- data/Gemfile +5 -0
- data/LICENSE +20 -0
- data/README.md +61 -0
- data/Rakefile +8 -0
- data/bin/console +19 -0
- data/bin/setup +7 -0
- data/lib/matchers/associations.rb +287 -0
- data/lib/matchers/be_nobrainer_document.rb +28 -0
- data/lib/matchers/have_field.rb +484 -0
- data/lib/matchers/have_index_for.rb +164 -0
- data/lib/matchers/have_timestamps.rb +38 -0
- data/lib/matchers/validations.rb +80 -0
- data/lib/matchers/validations/acceptance_of.rb +11 -0
- data/lib/matchers/validations/confirmation_of.rb +17 -0
- data/lib/matchers/validations/exclusion_of.rb +51 -0
- data/lib/matchers/validations/format_of.rb +73 -0
- data/lib/matchers/validations/inclusion_of.rb +51 -0
- data/lib/matchers/validations/length_of.rb +127 -0
- data/lib/matchers/validations/numericality_of.rb +92 -0
- data/lib/matchers/validations/presence_of.rb +11 -0
- data/lib/matchers/validations/uniqueness_of.rb +53 -0
- data/lib/nobrainer-rspec.rb +1 -0
- data/lib/nobrainer/rspec.rb +31 -0
- data/lib/nobrainer/rspec/version.rb +7 -0
- data/nobrainer-rspec.gemspec +47 -0
- metadata +158 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
# The `have_index_for` matcher tests that the table that backs your model
|
6
|
+
# has a specific index.
|
7
|
+
#
|
8
|
+
# class User
|
9
|
+
# include NoBrainer::Document
|
10
|
+
#
|
11
|
+
# field :name, index: true
|
12
|
+
# field :lastname
|
13
|
+
#
|
14
|
+
# index :lastname
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # RSpec
|
18
|
+
# RSpec.describe User, type: :model do
|
19
|
+
# it { is_expected.to have_index_for(:name) }
|
20
|
+
# it { is_expected.to have_index_for(:lastname) }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# #### Qualifiers
|
24
|
+
#
|
25
|
+
# ##### with_multi
|
26
|
+
#
|
27
|
+
# Use `with_multi` to assert that an index is defined as multi for a certain
|
28
|
+
# field.
|
29
|
+
#
|
30
|
+
# class User
|
31
|
+
# include NoBrainer::Document
|
32
|
+
#
|
33
|
+
# field :name, index: :multi
|
34
|
+
# field :lastname
|
35
|
+
#
|
36
|
+
# index :lastname, multi: true
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # RSpec
|
40
|
+
# RSpec.describe User, type: :model do
|
41
|
+
# it do
|
42
|
+
# it { is_expected.to have_index_for(:name).with_multi }
|
43
|
+
# it { is_expected.to have_index_for(:lastname).with_multi }
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# ##### named
|
48
|
+
#
|
49
|
+
# Use `named` to assert that an index has a certain name.
|
50
|
+
#
|
51
|
+
# class User
|
52
|
+
# include NoBrainer::Document
|
53
|
+
#
|
54
|
+
# field :firstname
|
55
|
+
# field :lastname
|
56
|
+
#
|
57
|
+
# index :full_name_compound, %i[firstname lastname]
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # RSpec
|
61
|
+
# RSpec.describe User, type: :model do
|
62
|
+
# it do
|
63
|
+
# is_expected.to have_index_for(%i[firstname lastname])
|
64
|
+
# .named(:full_name_compound)
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
class HaveIndexFor # :nodoc:
|
69
|
+
def initialize(*attrs)
|
70
|
+
@attributes = attrs.collect do |attributes|
|
71
|
+
if attributes.is_a?(Array)
|
72
|
+
attributes.collect(&:to_sym)
|
73
|
+
else
|
74
|
+
attributes.to_sym
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_multi(multi = true)
|
80
|
+
@multi = multi
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def named(index_name)
|
85
|
+
@index_name = index_name
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def matches?(klass)
|
90
|
+
@klass = klass.is_a?(Class) ? klass : klass.class
|
91
|
+
@errors = []
|
92
|
+
@attributes.each do |attr|
|
93
|
+
if attr.is_a?(Array)
|
94
|
+
missing_fields = attr - @klass.fields.keys
|
95
|
+
if missing_fields.empty?
|
96
|
+
index_key = @index_name || attr.join('_').to_sym
|
97
|
+
if @klass.indexes.include?(index_key)
|
98
|
+
error = ''
|
99
|
+
|
100
|
+
# Checking multi
|
101
|
+
if @multi && (@klass.indexes[index_key][:multi] != @multi)
|
102
|
+
error += " with multi #{@klass.indexes[index_key][:multi].inspect}"
|
103
|
+
end
|
104
|
+
|
105
|
+
@errors.push("field #{attr.inspect}" + error) unless error.blank?
|
106
|
+
else
|
107
|
+
if @index_name
|
108
|
+
@errors.push "no compound index named #{@index_name}"
|
109
|
+
else
|
110
|
+
@errors.push "no compound index for fields #{attr.to_sentence}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else
|
114
|
+
@errors.push "the field#{'s' if missing_fields.size > 1} " \
|
115
|
+
"#{missing_fields.to_sentence} " \
|
116
|
+
"#{missing_fields.size > 1 ? 'are' : 'is'} missing"
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if @klass.fields.include?(attr)
|
120
|
+
if @klass.indexes.include?(attr)
|
121
|
+
error = ''
|
122
|
+
|
123
|
+
# Checking multi
|
124
|
+
if @multi && (@klass.indexes[attr][:multi] != @multi)
|
125
|
+
error += " with multi #{@klass.indexes[attr][:multi].inspect}"
|
126
|
+
end
|
127
|
+
|
128
|
+
@errors.push("field #{attr.inspect}" + error) unless error.blank?
|
129
|
+
|
130
|
+
else
|
131
|
+
@errors.push "no index for field #{attr.inspect}"
|
132
|
+
end
|
133
|
+
else
|
134
|
+
@errors.push "no field named #{attr.inspect}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
@errors.empty?
|
139
|
+
end
|
140
|
+
|
141
|
+
def failure_message_for_should
|
142
|
+
"Expected #{@klass.inspect} to #{description}, got #{@errors.to_sentence}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def failure_message_for_should_not
|
146
|
+
"Expected #{@klass.inspect} to not #{description}, got #{@klass.inspect} to #{description}"
|
147
|
+
end
|
148
|
+
|
149
|
+
alias failure_message failure_message_for_should
|
150
|
+
alias failure_message_when_negated failure_message_for_should_not
|
151
|
+
|
152
|
+
def description
|
153
|
+
desc = "have index for #{@attributes.collect(&:inspect).to_sentence}"
|
154
|
+
desc += ' to be multi' if @multi
|
155
|
+
desc += " named #{@index_name}" if @index_name
|
156
|
+
desc
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def have_index_for(*args)
|
161
|
+
HaveIndexFor.new(*args)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
def have_timestamps
|
6
|
+
HaveTimestamps.new
|
7
|
+
end
|
8
|
+
|
9
|
+
class HaveTimestamps
|
10
|
+
def initialize
|
11
|
+
@root_module = 'NoBrainer::Document::Timestamps'
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches?(actual)
|
15
|
+
@model = actual.is_a?(Class) ? actual : actual.class
|
16
|
+
@model.included_modules.include?(expected_module)
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
'be a NoBrainer document with timestamps'
|
21
|
+
end
|
22
|
+
|
23
|
+
def failure_message
|
24
|
+
"Expected #{@model.inspect} class to #{description}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_message_when_negated
|
28
|
+
"Expected #{@model.inspect} class to not #{description}"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def expected_module
|
34
|
+
@root_module.constantize
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
module Validations
|
6
|
+
class HaveValidationMatcher
|
7
|
+
def initialize(field, validation_type)
|
8
|
+
@field = field.to_s
|
9
|
+
@type = validation_type.to_s
|
10
|
+
@options = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(actual)
|
14
|
+
@klass = actual.is_a?(Class) ? actual : actual.class
|
15
|
+
|
16
|
+
@validator = @klass.validators_on(@field).detect do |v|
|
17
|
+
(v.kind.to_s == @type) && (!v.options[:on] || on_options_matches?(v))
|
18
|
+
end
|
19
|
+
|
20
|
+
if @validator
|
21
|
+
@negative_result_message = "#{@type.inspect} validator on #{@field.inspect}"
|
22
|
+
@positive_result_message = "#{@type.inspect} validator on #{@field.inspect}"
|
23
|
+
else
|
24
|
+
@negative_result_message = "no #{@type.inspect} validator on #{@field.inspect}"
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure_message_for_should
|
32
|
+
"Expected #{@klass.inspect} to #{description}; instead got #{@negative_result_message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure_message_for_should_not
|
36
|
+
"Expected #{@klass.inspect} to not #{description}; instead got #{@positive_result_message}"
|
37
|
+
end
|
38
|
+
|
39
|
+
alias failure_message failure_message_for_should
|
40
|
+
alias failure_message_when_negated failure_message_for_should_not
|
41
|
+
|
42
|
+
def description
|
43
|
+
desc = "have #{@type.inspect} validator on #{@field.inspect}"
|
44
|
+
desc += " on #{@options[:on]}" if @options[:on]
|
45
|
+
desc
|
46
|
+
end
|
47
|
+
|
48
|
+
def on(*on_method)
|
49
|
+
@options[:on] = on_method.flatten
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def check_on
|
56
|
+
validator_on_methods = [@validator.options[:on]].flatten
|
57
|
+
|
58
|
+
if validator_on_methods.any?
|
59
|
+
message = " on methods: #{validator_on_methods}"
|
60
|
+
|
61
|
+
if on_options_covered_by?(@validator)
|
62
|
+
@positive_result_message += message
|
63
|
+
else
|
64
|
+
@negative_result_message += message
|
65
|
+
@result = false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def on_options_matches?(validator)
|
71
|
+
@options[:on] && validator.options[:on] && on_options_covered_by?(validator)
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_options_covered_by?(validator)
|
75
|
+
([@options[:on]].flatten - [validator.options[:on]].flatten).empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
module Validations
|
6
|
+
class ValidateConfirmationOfMatcher < HaveValidationMatcher
|
7
|
+
def initialize(name)
|
8
|
+
super(name, :confirmation)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_confirmation_of(field)
|
13
|
+
ValidateConfirmationOfMatcher.new(field)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
module Validations
|
6
|
+
class ValidateExclusionOfMatcher < HaveValidationMatcher
|
7
|
+
def initialize(name)
|
8
|
+
super(name, :exclusion)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_not_allow(*values)
|
12
|
+
@not_allowed_values = [values].flatten
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(actual)
|
17
|
+
return false unless result = super(actual)
|
18
|
+
|
19
|
+
if @not_allowed_values
|
20
|
+
raw_validator_not_allowed_values = @validator.options[:in]
|
21
|
+
|
22
|
+
validator_not_allowed_values = case raw_validator_not_allowed_values
|
23
|
+
when Range then raw_validator_not_allowed_values.to_a
|
24
|
+
when Proc then raw_validator_not_allowed_values.call(actual)
|
25
|
+
else raw_validator_not_allowed_values end
|
26
|
+
|
27
|
+
allowed_values = @not_allowed_values - validator_not_allowed_values
|
28
|
+
if allowed_values.empty?
|
29
|
+
@positive_result_message = @positive_result_message += ' not allowing all values mentioned'
|
30
|
+
else
|
31
|
+
@negative_result_message = @negative_result_message += " allowing the following the ff. values: #{allowed_values.inspect}"
|
32
|
+
result = false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
options_desc = []
|
41
|
+
options_desc << " not allowing the ff. values: #{@not_allowed_values}" if @not_allowed_values
|
42
|
+
"#{super}#{options_desc.to_sentence}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_exclusion_of(field)
|
47
|
+
ValidateExclusionOfMatcher.new(field)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
module Validations
|
6
|
+
class ValidateFormatOfMatcher < HaveValidationMatcher
|
7
|
+
def initialize(field)
|
8
|
+
super(field, :format)
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_format(format)
|
12
|
+
@format = format
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_allow(valid_value)
|
17
|
+
@valid_value = valid_value
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def not_to_allow(invalid_value)
|
22
|
+
@invalid_value = invalid_value
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches?(actual)
|
27
|
+
return false unless result = super(actual)
|
28
|
+
|
29
|
+
if @format
|
30
|
+
if @validator.options[:with] == @format
|
31
|
+
@positive_result_message = @positive_result_message += " with format #{@validator.options[:format].inspect}"
|
32
|
+
else
|
33
|
+
@negative_result_message = @negative_result_message += " with format #{@validator.options[:format].inspect}"
|
34
|
+
result = false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if @valid_value
|
39
|
+
if @validator.options[:with] =~ @valid_value
|
40
|
+
@positive_result_message = @positive_result_message += " with #{@valid_value.inspect} as a valid value"
|
41
|
+
else
|
42
|
+
@negative_result_message = @negative_result_message += " with #{@valid_value.inspect} as an invalid value"
|
43
|
+
result = false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if @invalid_value
|
48
|
+
if @invalid_value !~ @validator.options[:with]
|
49
|
+
@positive_result_message = @positive_result_message += " with #{@invalid_value.inspect} as an invalid value"
|
50
|
+
else
|
51
|
+
@negative_result_message = @negative_result_message += " with #{@invalid_value.inspect} as a valid value"
|
52
|
+
result = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def description
|
60
|
+
options_desc = []
|
61
|
+
options_desc << " with format #{@format.inspect}" if @format
|
62
|
+
options_desc << " allowing the value #{@valid_value.inspect}" if @valid_value
|
63
|
+
options_desc << " not allowing the value #{@invalid_value.inspect}" if @invalid_value
|
64
|
+
"#{super}#{options_desc.to_sentence}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_format_of(field)
|
69
|
+
ValidateFormatOfMatcher.new(field)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Matchers
|
5
|
+
module Validations
|
6
|
+
class ValidateInclusionOfMatcher < HaveValidationMatcher
|
7
|
+
def initialize(name)
|
8
|
+
super(name, :inclusion)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_allow(*values)
|
12
|
+
@allowed_values = values.map { |v| v.respond_to?(:to_a) ? v.to_a : v }.flatten
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(actual)
|
17
|
+
return false unless result = super(actual)
|
18
|
+
|
19
|
+
if @allowed_values
|
20
|
+
raw_validator_allowed_values = @validator.options[:in]
|
21
|
+
|
22
|
+
validator_allowed_values = case raw_validator_allowed_values
|
23
|
+
when Range then raw_validator_allowed_values.to_a
|
24
|
+
when Proc then raw_validator_allowed_values.call(actual)
|
25
|
+
else raw_validator_allowed_values end
|
26
|
+
|
27
|
+
not_allowed_values = @allowed_values - validator_allowed_values
|
28
|
+
if not_allowed_values.empty?
|
29
|
+
@positive_result_message = @positive_result_message += ' allowing all values mentioned'
|
30
|
+
else
|
31
|
+
@negative_result_message = @negative_result_message += " not allowing these values: #{not_allowed_values.inspect}"
|
32
|
+
result = false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
options_desc = []
|
41
|
+
options_desc << " allowing these values: #{@allowed_values}" if @allowed_values
|
42
|
+
"#{super}#{options_desc.to_sentence}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_inclusion_of(field)
|
47
|
+
ValidateInclusionOfMatcher.new(field)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|