mcmire-shoulda-matchers 2.5.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 +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +32 -0
- data/.yardopts +7 -0
- data/Appraisals +45 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +31 -0
- data/Gemfile.lock +166 -0
- data/MIT-LICENSE +22 -0
- data/NEWS.md +299 -0
- data/README.md +163 -0
- data/Rakefile +116 -0
- data/doc_config/gh-pages/index.html.erb +9 -0
- data/doc_config/yard/setup.rb +22 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
- data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
- data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
- data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
- data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
- data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
- data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
- data/features/rails_integration.feature +113 -0
- data/features/step_definitions/rails_steps.rb +162 -0
- data/features/support/env.rb +5 -0
- data/gemfiles/3.0.gemfile +24 -0
- data/gemfiles/3.0.gemfile.lock +150 -0
- data/gemfiles/3.1.gemfile +27 -0
- data/gemfiles/3.1.gemfile.lock +173 -0
- data/gemfiles/3.2.gemfile +27 -0
- data/gemfiles/3.2.gemfile.lock +171 -0
- data/gemfiles/4.0.0.gemfile +28 -0
- data/gemfiles/4.0.0.gemfile.lock +172 -0
- data/gemfiles/4.0.1.gemfile +28 -0
- data/gemfiles/4.0.1.gemfile.lock +172 -0
- data/lib/shoulda-matchers.rb +1 -0
- data/lib/shoulda/matchers.rb +11 -0
- data/lib/shoulda/matchers/action_controller.rb +17 -0
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
- data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
- data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
- data/lib/shoulda/matchers/active_model.rb +30 -0
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
- data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
- data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
- data/lib/shoulda/matchers/active_model/errors.rb +10 -0
- data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
- data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
- data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
- data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
- data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
- data/lib/shoulda/matchers/active_record.rb +22 -0
- data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
- data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
- data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
- data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
- data/lib/shoulda/matchers/assertion_error.rb +19 -0
- data/lib/shoulda/matchers/error.rb +6 -0
- data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
- data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
- data/lib/shoulda/matchers/rails_shim.rb +50 -0
- data/lib/shoulda/matchers/version.rb +6 -0
- data/lib/shoulda/matchers/warn.rb +8 -0
- data/shoulda-matchers.gemspec +23 -0
- data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
- data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
- data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
- data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
- data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
- data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
- data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
- data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
- data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
- data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
- data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
- data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
- data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
- data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
- data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
- data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
- data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
- data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
- data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
- data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
- data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
- data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
- data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
- data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/active_model_versions.rb +13 -0
- data/spec/support/active_resource_builder.rb +29 -0
- data/spec/support/activemodel_helpers.rb +19 -0
- data/spec/support/capture_helpers.rb +19 -0
- data/spec/support/class_builder.rb +42 -0
- data/spec/support/controller_builder.rb +74 -0
- data/spec/support/fail_with_message_including_matcher.rb +33 -0
- data/spec/support/fail_with_message_matcher.rb +32 -0
- data/spec/support/i18n_faker.rb +10 -0
- data/spec/support/mailer_builder.rb +10 -0
- data/spec/support/model_builder.rb +81 -0
- data/spec/support/rails_versions.rb +18 -0
- data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
- data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
- data/spec/support/test_application.rb +120 -0
- data/yard.watchr +5 -0
- metadata +281 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel
|
4
|
+
# @private
|
5
|
+
class ValidationMatcher
|
6
|
+
attr_reader :failure_message
|
7
|
+
|
8
|
+
alias failure_message_for_should failure_message
|
9
|
+
|
10
|
+
def initialize(attribute)
|
11
|
+
@attribute = attribute
|
12
|
+
@strict = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def on(context)
|
16
|
+
@context = context
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def strict
|
21
|
+
@strict = true
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def failure_message_when_negated
|
26
|
+
@failure_message_when_negated || @failure_message
|
27
|
+
end
|
28
|
+
alias failure_message_for_should_not failure_message_when_negated
|
29
|
+
|
30
|
+
def matches?(subject)
|
31
|
+
@subject = subject
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def allows_value_of(value, message = nil, &block)
|
38
|
+
allow = allow_value_matcher(value, message)
|
39
|
+
yield allow if block_given?
|
40
|
+
|
41
|
+
if allow.matches?(@subject)
|
42
|
+
@failure_message_when_negated = allow.failure_message_when_negated
|
43
|
+
true
|
44
|
+
else
|
45
|
+
@failure_message = allow.failure_message
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def disallows_value_of(value, message = nil, &block)
|
51
|
+
disallow = disallow_value_matcher(value, message)
|
52
|
+
yield disallow if block_given?
|
53
|
+
|
54
|
+
if disallow.matches?(@subject)
|
55
|
+
@failure_message_when_negated = disallow.failure_message_when_negated
|
56
|
+
true
|
57
|
+
else
|
58
|
+
@failure_message = disallow.failure_message
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def allow_value_matcher(value, message)
|
64
|
+
matcher = AllowValueMatcher.
|
65
|
+
new(value).
|
66
|
+
for(@attribute).
|
67
|
+
on(@context).
|
68
|
+
with_message(message)
|
69
|
+
|
70
|
+
if strict?
|
71
|
+
matcher.strict
|
72
|
+
else
|
73
|
+
matcher
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def disallow_value_matcher(value, message)
|
78
|
+
matcher = DisallowValueMatcher.
|
79
|
+
new(value).
|
80
|
+
for(@attribute).
|
81
|
+
on(@context).
|
82
|
+
with_message(message)
|
83
|
+
|
84
|
+
if strict?
|
85
|
+
matcher.strict
|
86
|
+
else
|
87
|
+
matcher
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def strict?
|
92
|
+
@strict
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel
|
4
|
+
# @private
|
5
|
+
class ValidationMessageFinder
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
def initialize(instance, attribute, context=nil)
|
9
|
+
@instance = instance
|
10
|
+
@attribute = attribute
|
11
|
+
@context = context
|
12
|
+
end
|
13
|
+
|
14
|
+
def allow_description(allowed_values)
|
15
|
+
"allow #{@attribute} to be set to #{allowed_values}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def expected_message_from(attribute_message)
|
19
|
+
attribute_message
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_messages?
|
23
|
+
errors.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
def source_description
|
27
|
+
'errors'
|
28
|
+
end
|
29
|
+
|
30
|
+
def messages_description
|
31
|
+
if errors.empty?
|
32
|
+
'no errors'
|
33
|
+
else
|
34
|
+
"errors: #{pretty_error_messages(validated_instance)}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def messages
|
39
|
+
Array(messages_for_attribute)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def messages_for_attribute
|
45
|
+
if errors.respond_to?(:[])
|
46
|
+
errors[@attribute]
|
47
|
+
else
|
48
|
+
errors.on(@attribute)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def errors
|
53
|
+
validated_instance.errors
|
54
|
+
end
|
55
|
+
|
56
|
+
def validated_instance
|
57
|
+
@validated_instance ||= validate_instance
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_instance
|
61
|
+
@instance.valid?(*@context)
|
62
|
+
@instance
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'shoulda/matchers/active_record/association_matcher'
|
2
|
+
require 'shoulda/matchers/active_record/association_matchers'
|
3
|
+
require 'shoulda/matchers/active_record/association_matchers/counter_cache_matcher'
|
4
|
+
require 'shoulda/matchers/active_record/association_matchers/order_matcher'
|
5
|
+
require 'shoulda/matchers/active_record/association_matchers/through_matcher'
|
6
|
+
require 'shoulda/matchers/active_record/association_matchers/dependent_matcher'
|
7
|
+
require 'shoulda/matchers/active_record/association_matchers/source_matcher'
|
8
|
+
require 'shoulda/matchers/active_record/association_matchers/model_reflector'
|
9
|
+
require 'shoulda/matchers/active_record/association_matchers/model_reflection'
|
10
|
+
require 'shoulda/matchers/active_record/association_matchers/option_verifier'
|
11
|
+
require 'shoulda/matchers/active_record/have_db_column_matcher'
|
12
|
+
require 'shoulda/matchers/active_record/have_db_index_matcher'
|
13
|
+
require 'shoulda/matchers/active_record/have_readonly_attribute_matcher'
|
14
|
+
require 'shoulda/matchers/active_record/serialize_matcher'
|
15
|
+
require 'shoulda/matchers/active_record/accept_nested_attributes_for_matcher'
|
16
|
+
|
17
|
+
module Shoulda
|
18
|
+
module Matchers
|
19
|
+
module ActiveRecord
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord
|
4
|
+
# The `accept_nested_attributes_for` matcher tests usage of the
|
5
|
+
# `accepts_nested_attributes_for` macro.
|
6
|
+
#
|
7
|
+
# class Car < ActiveRecord::Base
|
8
|
+
# accept_nested_attributes_for :doors
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# # RSpec
|
12
|
+
# describe Car do
|
13
|
+
# it { should accept_nested_attributes_for(:doors) }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Test::Unit (using Shoulda)
|
17
|
+
# class CarTest < ActiveSupport::TestCase
|
18
|
+
# should accept_nested_attributes_for(:doors)
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# #### Qualifiers
|
22
|
+
#
|
23
|
+
# ##### allow_destroy
|
24
|
+
#
|
25
|
+
# Use `allow_destroy` to assert that the `:allow_destroy` option was
|
26
|
+
# specified.
|
27
|
+
#
|
28
|
+
# class Car < ActiveRecord::Base
|
29
|
+
# accept_nested_attributes_for :mirrors, allow_destroy: true
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # RSpec
|
33
|
+
# describe Car do
|
34
|
+
# it { should accept_nested_attributes_for(:mirrors).allow_destroy(true) }
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# # Test::Unit
|
38
|
+
# class CarTest < ActiveSupport::TestCase
|
39
|
+
# should accept_nested_attributes_for(:mirrors).allow_destroy(true)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# ##### limit
|
43
|
+
#
|
44
|
+
# Use `limit` to assert that the `:limit` option was specified.
|
45
|
+
#
|
46
|
+
# class Car < ActiveRecord::Base
|
47
|
+
# accept_nested_attributes_for :windows, limit: 3
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # RSpec
|
51
|
+
# describe Car do
|
52
|
+
# it { should accept_nested_attributes_for(:windows).limit(3) }
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# # Test::Unit
|
56
|
+
# class CarTest < ActiveSupport::TestCase
|
57
|
+
# should accept_nested_attributes_for(:windows).limit(3)
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# ##### update_only
|
61
|
+
#
|
62
|
+
# Use `update_only` to assert that the `:update_only` option was
|
63
|
+
# specified.
|
64
|
+
#
|
65
|
+
# class Car < ActiveRecord::Base
|
66
|
+
# accept_nested_attributes_for :engine, update_only: true
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# # RSpec
|
70
|
+
# describe Car do
|
71
|
+
# it { should accept_nested_attributes_for(:engine).update_only(true) }
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# # Test::Unit
|
75
|
+
# class CarTest < ActiveSupport::TestCase
|
76
|
+
# should accept_nested_attributes_for(:engine).update_only(true)
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# @return [AcceptNestedAttributesForMatcher]
|
80
|
+
#
|
81
|
+
def accept_nested_attributes_for(name)
|
82
|
+
AcceptNestedAttributesForMatcher.new(name)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
class AcceptNestedAttributesForMatcher
|
87
|
+
def initialize(name)
|
88
|
+
@name = name
|
89
|
+
@options = {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def allow_destroy(allow_destroy)
|
93
|
+
@options[:allow_destroy] = allow_destroy
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def limit(limit)
|
98
|
+
@options[:limit] = limit
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_only(update_only)
|
103
|
+
@options[:update_only] = update_only
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def matches?(subject)
|
108
|
+
@subject = subject
|
109
|
+
exists? &&
|
110
|
+
allow_destroy_correct? &&
|
111
|
+
limit_correct? &&
|
112
|
+
update_only_correct?
|
113
|
+
end
|
114
|
+
|
115
|
+
def failure_message
|
116
|
+
"Expected #{expectation} (#{@problem})"
|
117
|
+
end
|
118
|
+
alias failure_message_for_should failure_message
|
119
|
+
|
120
|
+
def failure_message_when_negated
|
121
|
+
"Did not expect #{expectation}"
|
122
|
+
end
|
123
|
+
alias failure_message_for_should_not failure_message_when_negated
|
124
|
+
|
125
|
+
def description
|
126
|
+
description = "accepts_nested_attributes_for :#{@name}"
|
127
|
+
if @options.key?(:allow_destroy)
|
128
|
+
description += " allow_destroy => #{@options[:allow_destroy]}"
|
129
|
+
end
|
130
|
+
if @options.key?(:limit)
|
131
|
+
description += " limit => #{@options[:limit]}"
|
132
|
+
end
|
133
|
+
if @options.key?(:update_only)
|
134
|
+
description += " update_only => #{@options[:update_only]}"
|
135
|
+
end
|
136
|
+
description
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
|
141
|
+
def exists?
|
142
|
+
if config
|
143
|
+
true
|
144
|
+
else
|
145
|
+
@problem = 'is not declared'
|
146
|
+
false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def allow_destroy_correct?
|
151
|
+
failure_message = "#{should_or_should_not(@options[:allow_destroy])} allow destroy"
|
152
|
+
verify_option_is_correct(:allow_destroy, failure_message)
|
153
|
+
end
|
154
|
+
|
155
|
+
def limit_correct?
|
156
|
+
failure_message = "limit should be #{@options[:limit]}, got #{config[:limit]}"
|
157
|
+
verify_option_is_correct(:limit, failure_message)
|
158
|
+
end
|
159
|
+
|
160
|
+
def update_only_correct?
|
161
|
+
failure_message = "#{should_or_should_not(@options[:update_only])} be update only"
|
162
|
+
verify_option_is_correct(:update_only, failure_message)
|
163
|
+
end
|
164
|
+
|
165
|
+
def verify_option_is_correct(option, failure_message)
|
166
|
+
if @options.key?(option)
|
167
|
+
if @options[option] == config[option]
|
168
|
+
true
|
169
|
+
else
|
170
|
+
@problem = failure_message
|
171
|
+
false
|
172
|
+
end
|
173
|
+
else
|
174
|
+
true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def config
|
179
|
+
model_config[@name]
|
180
|
+
end
|
181
|
+
|
182
|
+
def model_config
|
183
|
+
model_class.nested_attributes_options
|
184
|
+
end
|
185
|
+
|
186
|
+
def model_class
|
187
|
+
@subject.class
|
188
|
+
end
|
189
|
+
|
190
|
+
def expectation
|
191
|
+
"#{model_class.name} to accept nested attributes for #{@name}"
|
192
|
+
end
|
193
|
+
|
194
|
+
def should_or_should_not(value)
|
195
|
+
if value
|
196
|
+
'should'
|
197
|
+
else
|
198
|
+
'should not'
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,901 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
3
|
+
module Shoulda
|
4
|
+
module Matchers
|
5
|
+
module ActiveRecord
|
6
|
+
# The `belong_to` matcher is used to ensure that a `belong_to` association
|
7
|
+
# exists on your model.
|
8
|
+
#
|
9
|
+
# class Person < ActiveRecord::Base
|
10
|
+
# belongs_to :organization
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# # RSpec
|
14
|
+
# describe Person do
|
15
|
+
# it { should belong_to(:organization) }
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Test::Unit
|
19
|
+
# class PersonTest < ActiveSupport::TestCase
|
20
|
+
# should belong_to(:organization)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# #### Qualifiers
|
24
|
+
#
|
25
|
+
# ##### conditions
|
26
|
+
#
|
27
|
+
# Use `conditions` if your association is defined with a scope that sets
|
28
|
+
# the `where` clause.
|
29
|
+
#
|
30
|
+
# class Person < ActiveRecord::Base
|
31
|
+
# belongs_to :family, -> { where(everyone_is_perfect: false) }
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # RSpec
|
35
|
+
# describe Person do
|
36
|
+
# it { should belong_to(:family).conditions(everyone_is_perfect: false) }
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# # Test::Unit
|
40
|
+
# class PersonTest < ActiveSupport::TestCase
|
41
|
+
# should belong_to(:family).conditions(everyone_is_perfect: false)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# ##### order
|
45
|
+
#
|
46
|
+
# Use `order` if your association is defined with a scope that sets the
|
47
|
+
# `order` clause.
|
48
|
+
#
|
49
|
+
# class Person < ActiveRecord::Base
|
50
|
+
# belongs_to :previous_company, -> { order('hired_on desc') }
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # RSpec
|
54
|
+
# describe Person do
|
55
|
+
# it { should belong_to(:previous_company).order('hired_on desc') }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# # Test::Unit
|
59
|
+
# class PersonTest < ActiveSupport::TestCase
|
60
|
+
# should belong_to(:previous_company).order('hired_on desc')
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# ##### class_name
|
64
|
+
#
|
65
|
+
# Use `class_name` to test usage of the `:class_name` option. This
|
66
|
+
# asserts that the model you're referring to actually exists.
|
67
|
+
#
|
68
|
+
# class Person < ActiveRecord::Base
|
69
|
+
# belongs_to :ancient_city, class_name: 'City'
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# # RSpec
|
73
|
+
# describe Person do
|
74
|
+
# it { should belong_to(:ancient_city).class_name('City') }
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# # Test::Unit
|
78
|
+
# class PersonTest < ActiveSupport::TestCase
|
79
|
+
# should belong_to(:ancient_city).class_name('City')
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# ##### with_foreign_key
|
83
|
+
#
|
84
|
+
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
85
|
+
#
|
86
|
+
# class Person < ActiveRecord::Base
|
87
|
+
# belongs_to :great_country, foreign_key: 'country_id'
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# # RSpec
|
91
|
+
# describe Person do
|
92
|
+
# it { should belong_to(:great_country).with_foreign_key('country_id') }
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# # Test::Unit
|
96
|
+
# class PersonTest < ActiveSupport::TestCase
|
97
|
+
# should belong_to(:great_country).with_foreign_key('country_id')
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# ##### dependent
|
101
|
+
#
|
102
|
+
# Use `dependent` to assert that the `:dependent` option was specified.
|
103
|
+
#
|
104
|
+
# class Person < ActiveRecord::Base
|
105
|
+
# belongs_to :world, dependent: :destroy
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# # RSpec
|
109
|
+
# describe Person do
|
110
|
+
# it { should belong_to(:world).dependent(:destroy) }
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# # Test::Unit
|
114
|
+
# class PersonTest < ActiveSupport::TestCase
|
115
|
+
# should belong_to(:world).dependent(:destroy)
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# ##### counter_cache
|
119
|
+
#
|
120
|
+
# Use `counter_cache` to assert that the `:counter_cache` option was
|
121
|
+
# specified.
|
122
|
+
#
|
123
|
+
# class Person < ActiveRecord::Base
|
124
|
+
# belongs_to :organization, counter_cache: true
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# # RSpec
|
128
|
+
# describe Person do
|
129
|
+
# it { should belong_to(:organization).counter_cache(true) }
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# # Test::Unit
|
133
|
+
# class PersonTest < ActiveSupport::TestCase
|
134
|
+
# should belong_to(:organization).counter_cache(true)
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# ##### touch
|
138
|
+
#
|
139
|
+
# Use `touch` to assert that the `:touch` option was specified.
|
140
|
+
#
|
141
|
+
# class Person < ActiveRecord::Base
|
142
|
+
# belongs_to :mental_institution, touch: true
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# # RSpec
|
146
|
+
# describe Person do
|
147
|
+
# it { should belong_to(:mental_institution).touch(true) }
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# # Test::Unit
|
151
|
+
# class PersonTest < ActiveSupport::TestCase
|
152
|
+
# should belong_to(:mental_institution).touch(true)
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# @return [AssociationMatcher]
|
156
|
+
#
|
157
|
+
def belong_to(name)
|
158
|
+
AssociationMatcher.new(:belongs_to, name)
|
159
|
+
end
|
160
|
+
|
161
|
+
# The `have_many` matcher is used to test that a `has_many` or `has_many
|
162
|
+
# :through` association exists on your model.
|
163
|
+
#
|
164
|
+
# class Person < ActiveRecord::Base
|
165
|
+
# has_many :friends
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# # RSpec
|
169
|
+
# describe Person do
|
170
|
+
# it { should have_many(:friends) }
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# # Test::Unit
|
174
|
+
# class PersonTest < ActiveSupport::TestCase
|
175
|
+
# should have_many(:friends)
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# #### Qualifiers
|
179
|
+
#
|
180
|
+
# ##### conditions
|
181
|
+
#
|
182
|
+
# Use `conditions` if your association is defined with a scope that sets
|
183
|
+
# the `where` clause.
|
184
|
+
#
|
185
|
+
# class Person < ActiveRecord::Base
|
186
|
+
# has_many :coins, -> { where(quality: 'mint') }
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# # RSpec
|
190
|
+
# describe Person do
|
191
|
+
# it { should have_many(:coins).conditions(quality: 'mint') }
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# # Test::Unit
|
195
|
+
# class PersonTest < ActiveSupport::TestCase
|
196
|
+
# should have_many(:coins).conditions(quality: 'mint')
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# ##### order
|
200
|
+
#
|
201
|
+
# Use `order` if your association is defined with a scope that sets the
|
202
|
+
# `order` clause.
|
203
|
+
#
|
204
|
+
# class Person < ActiveRecord::Base
|
205
|
+
# has_many :shirts, -> { order('color') }
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# # RSpec
|
209
|
+
# describe Person do
|
210
|
+
# it { should have_many(:shirts).order('color') }
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# # Test::Unit
|
214
|
+
# class PersonTest < ActiveSupport::TestCase
|
215
|
+
# should have_many(:shirts).order('color')
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
# ##### class_name
|
219
|
+
#
|
220
|
+
# Use `class_name` to test usage of the `:class_name` option. This
|
221
|
+
# asserts that the model you're referring to actually exists.
|
222
|
+
#
|
223
|
+
# class Person < ActiveRecord::Base
|
224
|
+
# has_many :hopes, class_name: 'Dream'
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# # RSpec
|
228
|
+
# describe Person do
|
229
|
+
# it { should have_many(:hopes).class_name('Dream') }
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# # Test::Unit
|
233
|
+
# class PersonTest < ActiveSupport::TestCase
|
234
|
+
# should have_many(:hopes).class_name('Dream')
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# ##### with_foreign_key
|
238
|
+
#
|
239
|
+
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
240
|
+
#
|
241
|
+
# class Person < ActiveRecord::Base
|
242
|
+
# has_many :worries, foreign_key: 'worrier_id'
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# # RSpec
|
246
|
+
# describe Person do
|
247
|
+
# it { should have_many(:worries).with_foreign_key('worrier_id') }
|
248
|
+
# end
|
249
|
+
#
|
250
|
+
# # Test::Unit
|
251
|
+
# class PersonTest < ActiveSupport::TestCase
|
252
|
+
# should have_many(:worries).with_foreign_key('worrier_id')
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# ##### dependent
|
256
|
+
#
|
257
|
+
# Use `dependent` to assert that the `:dependent` option was specified.
|
258
|
+
#
|
259
|
+
# class Person < ActiveRecord::Base
|
260
|
+
# has_many :secret_documents, dependent: :destroy
|
261
|
+
# end
|
262
|
+
#
|
263
|
+
# # RSpec
|
264
|
+
# describe Person do
|
265
|
+
# it { should have_many(:secret_documents).dependent(:destroy) }
|
266
|
+
# end
|
267
|
+
#
|
268
|
+
# # Test::Unit
|
269
|
+
# class PersonTest < ActiveSupport::TestCase
|
270
|
+
# should have_many(:secret_documents).dependent(:destroy)
|
271
|
+
# end
|
272
|
+
#
|
273
|
+
# ##### through
|
274
|
+
#
|
275
|
+
# Use `through` to test usage of the `:through` option. This asserts that
|
276
|
+
# the association you are going through actually exists.
|
277
|
+
#
|
278
|
+
# class Person < ActiveRecord::Base
|
279
|
+
# has_many :acquaintances, through: :friends
|
280
|
+
# end
|
281
|
+
#
|
282
|
+
# # RSpec
|
283
|
+
# describe Person do
|
284
|
+
# it { should have_many(:acquaintances).through(:friends) }
|
285
|
+
# end
|
286
|
+
#
|
287
|
+
# # Test::Unit
|
288
|
+
# class PersonTest < ActiveSupport::TestCase
|
289
|
+
# should have_many(:acquaintances).through(:friends)
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# ##### source
|
293
|
+
#
|
294
|
+
# Use `source` to test usage of the `:source` option on a `:through`
|
295
|
+
# association.
|
296
|
+
#
|
297
|
+
# class Person < ActiveRecord::Base
|
298
|
+
# has_many :job_offers, through: :friends, source: :opportunities
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# # RSpec
|
302
|
+
# describe Person do
|
303
|
+
# it { should have_many(:job_offers).through(:friends).source(:opportunities) }
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# # Test::Unit
|
307
|
+
# class PersonTest < ActiveSupport::TestCase
|
308
|
+
# should have_many(:job_offers).through(:friends).source(:opportunities)
|
309
|
+
# end
|
310
|
+
#
|
311
|
+
# ##### validate
|
312
|
+
#
|
313
|
+
# Use `validate` to assert that the `:validate` option was specified.
|
314
|
+
#
|
315
|
+
# class Person < ActiveRecord::Base
|
316
|
+
# has_many :ideas, validate: false
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# # RSpec
|
320
|
+
# describe Person do
|
321
|
+
# it { should have_many(:ideas).validate(false) }
|
322
|
+
# end
|
323
|
+
#
|
324
|
+
# # Test::Unit
|
325
|
+
# class PersonTest < ActiveSupport::TestCase
|
326
|
+
# should have_many(:ideas).validate(false)
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# @return [AssociationMatcher]
|
330
|
+
#
|
331
|
+
def have_many(name)
|
332
|
+
AssociationMatcher.new(:has_many, name)
|
333
|
+
end
|
334
|
+
|
335
|
+
# The `have_one` matcher is used to test that a `has_one` or `has_one
|
336
|
+
# :through` association exists on your model.
|
337
|
+
#
|
338
|
+
# class Person < ActiveRecord::Base
|
339
|
+
# has_one :partner
|
340
|
+
# end
|
341
|
+
#
|
342
|
+
# # RSpec
|
343
|
+
# describe Person do
|
344
|
+
# it { should have_one(:partner) }
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# # Test::Unit
|
348
|
+
# class PersonTest < ActiveSupport::TestCase
|
349
|
+
# should have_one(:partner)
|
350
|
+
# end
|
351
|
+
#
|
352
|
+
# #### Qualifiers
|
353
|
+
#
|
354
|
+
# ##### conditions
|
355
|
+
#
|
356
|
+
# Use `conditions` if your association is defined with a scope that sets
|
357
|
+
# the `where` clause.
|
358
|
+
#
|
359
|
+
# class Person < ActiveRecord::Base
|
360
|
+
# has_one :pet, -> { where('weight < 80') }
|
361
|
+
# end
|
362
|
+
#
|
363
|
+
# # RSpec
|
364
|
+
# describe Person do
|
365
|
+
# it { should have_one(:pet).conditions('weight < 80') }
|
366
|
+
# end
|
367
|
+
#
|
368
|
+
# # Test::Unit
|
369
|
+
# class PersonTest < ActiveSupport::TestCase
|
370
|
+
# should have_one(:pet).conditions('weight < 80')
|
371
|
+
# end
|
372
|
+
#
|
373
|
+
# ##### order
|
374
|
+
#
|
375
|
+
# Use `order` if your association is defined with a scope that sets the
|
376
|
+
# `order` clause.
|
377
|
+
#
|
378
|
+
# class Person < ActiveRecord::Base
|
379
|
+
# has_one :focus, -> { order('priority desc') }
|
380
|
+
# end
|
381
|
+
#
|
382
|
+
# # RSpec
|
383
|
+
# describe Person do
|
384
|
+
# it { should have_one(:focus).order('priority desc') }
|
385
|
+
# end
|
386
|
+
#
|
387
|
+
# # Test::Unit
|
388
|
+
# class PersonTest < ActiveSupport::TestCase
|
389
|
+
# should have_one(:focus).order('priority desc')
|
390
|
+
# end
|
391
|
+
#
|
392
|
+
# ##### class_name
|
393
|
+
#
|
394
|
+
# Use `class_name` to test usage of the `:class_name` option. This
|
395
|
+
# asserts that the model you're referring to actually exists.
|
396
|
+
#
|
397
|
+
# class Person < ActiveRecord::Base
|
398
|
+
# has_one :chance, class_name: 'Opportunity'
|
399
|
+
# end
|
400
|
+
#
|
401
|
+
# # RSpec
|
402
|
+
# describe Person do
|
403
|
+
# it { should have_one(:chance).class_name('Opportunity') }
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
# # Test::Unit
|
407
|
+
# class PersonTest < ActiveSupport::TestCase
|
408
|
+
# should have_one(:chance).class_name('Opportunity')
|
409
|
+
# end
|
410
|
+
#
|
411
|
+
# ##### dependent
|
412
|
+
#
|
413
|
+
# Use `dependent` to test that the `:dependent` option was specified.
|
414
|
+
#
|
415
|
+
# class Person < ActiveRecord::Base
|
416
|
+
# has_one :contract, dependent: :nullify
|
417
|
+
# end
|
418
|
+
#
|
419
|
+
# # RSpec
|
420
|
+
# describe Person do
|
421
|
+
# it { should have_one(:contract).dependent(:nullify) }
|
422
|
+
# end
|
423
|
+
#
|
424
|
+
# # Test::Unit
|
425
|
+
# class PersonTest < ActiveSupport::TestCase
|
426
|
+
# should have_one(:contract).dependent(:nullify)
|
427
|
+
# end
|
428
|
+
#
|
429
|
+
# ##### with_foreign_key
|
430
|
+
#
|
431
|
+
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
432
|
+
#
|
433
|
+
# class Person < ActiveRecord::Base
|
434
|
+
# has_one :job, foreign_key: 'worker_id'
|
435
|
+
# end
|
436
|
+
#
|
437
|
+
# # RSpec
|
438
|
+
# describe Person do
|
439
|
+
# it { should have_one(:job).with_foreign_key('worker_id') }
|
440
|
+
# end
|
441
|
+
#
|
442
|
+
# # Test::Unit
|
443
|
+
# class PersonTest < ActiveSupport::TestCase
|
444
|
+
# should have_one(:job).with_foreign_key('worker_id')
|
445
|
+
# end
|
446
|
+
#
|
447
|
+
# ##### through
|
448
|
+
#
|
449
|
+
# Use `through` to test usage of the `:through` option. This asserts that
|
450
|
+
# the association you are going through actually exists.
|
451
|
+
#
|
452
|
+
# class Person < ActiveRecord::Base
|
453
|
+
# has_one :life, through: :partner
|
454
|
+
# end
|
455
|
+
#
|
456
|
+
# # RSpec
|
457
|
+
# describe Person do
|
458
|
+
# it { should have_one(:life).through(:partner) }
|
459
|
+
# end
|
460
|
+
#
|
461
|
+
# # Test::Unit
|
462
|
+
# class PersonTest < ActiveSupport::TestCase
|
463
|
+
# should have_one(:life).through(:partner)
|
464
|
+
# end
|
465
|
+
#
|
466
|
+
# ##### source
|
467
|
+
#
|
468
|
+
# Use `source` to test usage of the `:source` option on a `:through`
|
469
|
+
# association.
|
470
|
+
#
|
471
|
+
# class Person < ActiveRecord::Base
|
472
|
+
# has_one :car, through: :partner, source: :vehicle
|
473
|
+
# end
|
474
|
+
#
|
475
|
+
# # RSpec
|
476
|
+
# describe Person do
|
477
|
+
# it { should have_one(:car).through(:partner).source(:vehicle) }
|
478
|
+
# end
|
479
|
+
#
|
480
|
+
# # Test::Unit
|
481
|
+
# class PersonTest < ActiveSupport::TestCase
|
482
|
+
# should have_one(:car).through(:partner).source(:vehicle)
|
483
|
+
# end
|
484
|
+
#
|
485
|
+
# ##### validate
|
486
|
+
#
|
487
|
+
# Use `validate` to assert that the the `:validate` option was specified.
|
488
|
+
#
|
489
|
+
# class Person < ActiveRecord::Base
|
490
|
+
# has_one :parking_card, validate: false
|
491
|
+
# end
|
492
|
+
#
|
493
|
+
# # RSpec
|
494
|
+
# describe Person do
|
495
|
+
# it { should have_one(:parking_card).validate(false) }
|
496
|
+
# end
|
497
|
+
#
|
498
|
+
# # Test::Unit
|
499
|
+
# class PersonTest < ActiveSupport::TestCase
|
500
|
+
# should have_one(:parking_card).validate(false)
|
501
|
+
# end
|
502
|
+
#
|
503
|
+
# @return [AssociationMatcher]
|
504
|
+
#
|
505
|
+
def have_one(name)
|
506
|
+
AssociationMatcher.new(:has_one, name)
|
507
|
+
end
|
508
|
+
|
509
|
+
# The `have_and_belong_to_many` matcher is used to test that a
|
510
|
+
# `has_and_belongs_to_many` association exists on your model and that the
|
511
|
+
# join table exists in the database.
|
512
|
+
#
|
513
|
+
# class Person < ActiveRecord::Base
|
514
|
+
# has_and_belongs_to_many :awards
|
515
|
+
# end
|
516
|
+
#
|
517
|
+
# # RSpec
|
518
|
+
# describe Person do
|
519
|
+
# it { should have_and_belong_to_many(:awards) }
|
520
|
+
# end
|
521
|
+
#
|
522
|
+
# # Test::Unit
|
523
|
+
# class PersonTest < ActiveSupport::TestCase
|
524
|
+
# should have_and_belong_to_many(:awards)
|
525
|
+
# end
|
526
|
+
#
|
527
|
+
# #### Qualifiers
|
528
|
+
#
|
529
|
+
# ##### conditions
|
530
|
+
#
|
531
|
+
# Use `conditions` if your association is defined with a scope that sets
|
532
|
+
# the `where` clause.
|
533
|
+
#
|
534
|
+
# class Person < ActiveRecord::Base
|
535
|
+
# has_and_belongs_to_many :issues, -> { where(difficulty: 'hard') }
|
536
|
+
# end
|
537
|
+
#
|
538
|
+
# # RSpec
|
539
|
+
# describe Person do
|
540
|
+
# it { should have_and_belong_to_many(:issues).conditions(difficulty: 'hard') }
|
541
|
+
# end
|
542
|
+
#
|
543
|
+
# # Test::Unit
|
544
|
+
# class PersonTest < ActiveSupport::TestCase
|
545
|
+
# should have_and_belong_to_many(:issues).conditions(difficulty: 'hard')
|
546
|
+
# end
|
547
|
+
#
|
548
|
+
# ##### order
|
549
|
+
#
|
550
|
+
# Use `order` if your association is defined with a scope that sets the
|
551
|
+
# `order` clause.
|
552
|
+
#
|
553
|
+
# class Person < ActiveRecord::Base
|
554
|
+
# has_and_belongs_to_many :projects, -> { order('time_spent') }
|
555
|
+
# end
|
556
|
+
#
|
557
|
+
# # RSpec
|
558
|
+
# describe Person do
|
559
|
+
# it { should have_and_belong_to_many(:projects).order('time_spent') }
|
560
|
+
# end
|
561
|
+
#
|
562
|
+
# # Test::Unit
|
563
|
+
# class PersonTest < ActiveSupport::TestCase
|
564
|
+
# should have_and_belong_to_many(:projects).order('time_spent')
|
565
|
+
# end
|
566
|
+
#
|
567
|
+
# ##### class_name
|
568
|
+
#
|
569
|
+
# Use `class_name` to test usage of the `:class_name` option. This
|
570
|
+
# asserts that the model you're referring to actually exists.
|
571
|
+
#
|
572
|
+
# class Person < ActiveRecord::Base
|
573
|
+
# has_and_belongs_to_many :places_visited, class_name: 'City'
|
574
|
+
# end
|
575
|
+
#
|
576
|
+
# # RSpec
|
577
|
+
# describe Person do
|
578
|
+
# it { should have_and_belong_to_many(:places_visited).class_name('City') }
|
579
|
+
# end
|
580
|
+
#
|
581
|
+
# # Test::Unit
|
582
|
+
# class PersonTest < ActiveSupport::TestCase
|
583
|
+
# should have_and_belong_to_many(:places_visited).class_name('City')
|
584
|
+
# end
|
585
|
+
#
|
586
|
+
# ##### validate
|
587
|
+
#
|
588
|
+
# Use `validate` to test that the `:validate` option was specified.
|
589
|
+
#
|
590
|
+
# class Person < ActiveRecord::Base
|
591
|
+
# has_and_belongs_to_many :interviews, validate: false
|
592
|
+
# end
|
593
|
+
#
|
594
|
+
# # RSpec
|
595
|
+
# describe Person do
|
596
|
+
# it { should have_and_belong_to_many(:interviews).validate(false) }
|
597
|
+
# end
|
598
|
+
#
|
599
|
+
# # Test::Unit
|
600
|
+
# class PersonTest < ActiveSupport::TestCase
|
601
|
+
# should have_and_belong_to_many(:interviews).validate(false)
|
602
|
+
# end
|
603
|
+
#
|
604
|
+
# @return [AssociationMatcher]
|
605
|
+
#
|
606
|
+
def have_and_belong_to_many(name)
|
607
|
+
AssociationMatcher.new(:has_and_belongs_to_many, name)
|
608
|
+
end
|
609
|
+
|
610
|
+
# @private
|
611
|
+
class AssociationMatcher
|
612
|
+
delegate :reflection, :model_class, :associated_class, :through?,
|
613
|
+
:join_table, to: :reflector
|
614
|
+
|
615
|
+
def initialize(macro, name)
|
616
|
+
@macro = macro
|
617
|
+
@name = name
|
618
|
+
@options = {}
|
619
|
+
@submatchers = []
|
620
|
+
@missing = ''
|
621
|
+
end
|
622
|
+
|
623
|
+
def through(through)
|
624
|
+
through_matcher = AssociationMatchers::ThroughMatcher.new(through, name)
|
625
|
+
add_submatcher(through_matcher)
|
626
|
+
self
|
627
|
+
end
|
628
|
+
|
629
|
+
def dependent(dependent)
|
630
|
+
dependent_matcher = AssociationMatchers::DependentMatcher.new(dependent, name)
|
631
|
+
add_submatcher(dependent_matcher)
|
632
|
+
self
|
633
|
+
end
|
634
|
+
|
635
|
+
def order(order)
|
636
|
+
order_matcher = AssociationMatchers::OrderMatcher.new(order, name)
|
637
|
+
add_submatcher(order_matcher)
|
638
|
+
self
|
639
|
+
end
|
640
|
+
|
641
|
+
def counter_cache(counter_cache = true)
|
642
|
+
counter_cache_matcher = AssociationMatchers::CounterCacheMatcher.new(counter_cache, name)
|
643
|
+
add_submatcher(counter_cache_matcher)
|
644
|
+
self
|
645
|
+
end
|
646
|
+
|
647
|
+
def source(source)
|
648
|
+
source_matcher = AssociationMatchers::SourceMatcher.new(source, name)
|
649
|
+
add_submatcher(source_matcher)
|
650
|
+
self
|
651
|
+
end
|
652
|
+
|
653
|
+
def conditions(conditions)
|
654
|
+
@options[:conditions] = conditions
|
655
|
+
self
|
656
|
+
end
|
657
|
+
|
658
|
+
def autosave(autosave)
|
659
|
+
@options[:autosave] = autosave
|
660
|
+
self
|
661
|
+
end
|
662
|
+
|
663
|
+
def class_name(class_name)
|
664
|
+
@options[:class_name] = class_name
|
665
|
+
self
|
666
|
+
end
|
667
|
+
|
668
|
+
def with_foreign_key(foreign_key)
|
669
|
+
@options[:foreign_key] = foreign_key
|
670
|
+
self
|
671
|
+
end
|
672
|
+
|
673
|
+
def validate(validate = true)
|
674
|
+
@options[:validate] = validate
|
675
|
+
self
|
676
|
+
end
|
677
|
+
|
678
|
+
def touch(touch = true)
|
679
|
+
@options[:touch] = touch
|
680
|
+
self
|
681
|
+
end
|
682
|
+
|
683
|
+
def description
|
684
|
+
description = "#{macro_description} #{name}"
|
685
|
+
description += " class_name => #{options[:class_name]}" if options.key?(:class_name)
|
686
|
+
[description, submatchers.map(&:description)].flatten.join(' ')
|
687
|
+
end
|
688
|
+
|
689
|
+
def failure_message
|
690
|
+
"Expected #{expectation} (#{missing_options})"
|
691
|
+
end
|
692
|
+
alias failure_message_for_should failure_message
|
693
|
+
|
694
|
+
def failure_message_when_negated
|
695
|
+
"Did not expect #{expectation}"
|
696
|
+
end
|
697
|
+
alias failure_message_for_should_not failure_message_when_negated
|
698
|
+
|
699
|
+
def matches?(subject)
|
700
|
+
@subject = subject
|
701
|
+
association_exists? &&
|
702
|
+
macro_correct? &&
|
703
|
+
class_exists? &&
|
704
|
+
foreign_key_exists? &&
|
705
|
+
class_name_correct? &&
|
706
|
+
autosave_correct? &&
|
707
|
+
conditions_correct? &&
|
708
|
+
join_table_exists? &&
|
709
|
+
validate_correct? &&
|
710
|
+
touch_correct? &&
|
711
|
+
submatchers_match?
|
712
|
+
end
|
713
|
+
|
714
|
+
private
|
715
|
+
|
716
|
+
attr_reader :submatchers, :missing, :subject, :macro, :name, :options
|
717
|
+
|
718
|
+
def reflector
|
719
|
+
@reflector ||= AssociationMatchers::ModelReflector.new(subject, name)
|
720
|
+
end
|
721
|
+
|
722
|
+
def option_verifier
|
723
|
+
@option_verifier ||= AssociationMatchers::OptionVerifier.new(reflector)
|
724
|
+
end
|
725
|
+
|
726
|
+
def add_submatcher(matcher)
|
727
|
+
@submatchers << matcher
|
728
|
+
end
|
729
|
+
|
730
|
+
def macro_description
|
731
|
+
case macro.to_s
|
732
|
+
when 'belongs_to'
|
733
|
+
'belong to'
|
734
|
+
when 'has_many'
|
735
|
+
'have many'
|
736
|
+
when 'has_one'
|
737
|
+
'have one'
|
738
|
+
when 'has_and_belongs_to_many'
|
739
|
+
'have and belong to many'
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
def expectation
|
744
|
+
"#{model_class.name} to have a #{macro} association called #{name}"
|
745
|
+
end
|
746
|
+
|
747
|
+
def missing_options
|
748
|
+
[missing, failing_submatchers.map(&:missing_option)].flatten.join
|
749
|
+
end
|
750
|
+
|
751
|
+
def failing_submatchers
|
752
|
+
@failing_submatchers ||= submatchers.select do |matcher|
|
753
|
+
!matcher.matches?(subject)
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
def association_exists?
|
758
|
+
if reflection.nil?
|
759
|
+
@missing = "no association called #{name}"
|
760
|
+
false
|
761
|
+
else
|
762
|
+
true
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
def macro_correct?
|
767
|
+
if reflection.macro == macro
|
768
|
+
true
|
769
|
+
else
|
770
|
+
@missing = "actual association type was #{reflection.macro}"
|
771
|
+
false
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
def foreign_key_exists?
|
776
|
+
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
|
777
|
+
end
|
778
|
+
|
779
|
+
def belongs_foreign_key_missing?
|
780
|
+
macro == :belongs_to && !class_has_foreign_key?(model_class)
|
781
|
+
end
|
782
|
+
|
783
|
+
def has_foreign_key_missing?
|
784
|
+
[:has_many, :has_one].include?(macro) &&
|
785
|
+
!through? &&
|
786
|
+
!class_has_foreign_key?(associated_class)
|
787
|
+
end
|
788
|
+
|
789
|
+
def class_name_correct?
|
790
|
+
if options.key?(:class_name)
|
791
|
+
if option_verifier.correct_for_string?(:class_name, options[:class_name])
|
792
|
+
true
|
793
|
+
else
|
794
|
+
@missing = "#{name} should resolve to #{options[:class_name]} for class_name"
|
795
|
+
false
|
796
|
+
end
|
797
|
+
else
|
798
|
+
true
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
def class_exists?
|
803
|
+
associated_class
|
804
|
+
true
|
805
|
+
rescue NameError
|
806
|
+
@missing = "#{reflection.class_name} does not exist"
|
807
|
+
false
|
808
|
+
end
|
809
|
+
|
810
|
+
def autosave_correct?
|
811
|
+
if options.key?(:autosave)
|
812
|
+
if option_verifier.correct_for_boolean?(:autosave, options[:autosave])
|
813
|
+
true
|
814
|
+
else
|
815
|
+
@missing = "#{name} should have autosave set to #{options[:autosave]}"
|
816
|
+
false
|
817
|
+
end
|
818
|
+
else
|
819
|
+
true
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
def conditions_correct?
|
824
|
+
if options.key?(:conditions)
|
825
|
+
if option_verifier.correct_for_relation_clause?(:conditions, options[:conditions])
|
826
|
+
true
|
827
|
+
else
|
828
|
+
@missing = "#{name} should have the following conditions: #{options[:conditions]}"
|
829
|
+
false
|
830
|
+
end
|
831
|
+
else
|
832
|
+
true
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
def join_table_exists?
|
837
|
+
if macro != :has_and_belongs_to_many ||
|
838
|
+
model_class.connection.tables.include?(join_table)
|
839
|
+
true
|
840
|
+
else
|
841
|
+
@missing = "join table #{join_table} doesn't exist"
|
842
|
+
false
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
def validate_correct?
|
847
|
+
if option_verifier.correct_for_boolean?(:validate, options[:validate])
|
848
|
+
true
|
849
|
+
else
|
850
|
+
@missing = "#{name} should have validate: #{options[:validate]}"
|
851
|
+
false
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
def touch_correct?
|
856
|
+
if option_verifier.correct_for_boolean?(:touch, options[:touch])
|
857
|
+
true
|
858
|
+
else
|
859
|
+
@missing = "#{name} should have touch: #{options[:touch]}"
|
860
|
+
false
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
def class_has_foreign_key?(klass)
|
865
|
+
if options.key?(:foreign_key)
|
866
|
+
option_verifier.correct_for_string?(:foreign_key, options[:foreign_key])
|
867
|
+
else
|
868
|
+
if klass.column_names.include?(foreign_key)
|
869
|
+
true
|
870
|
+
else
|
871
|
+
@missing = "#{klass} does not have a #{foreign_key} foreign key."
|
872
|
+
false
|
873
|
+
end
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
def foreign_key
|
878
|
+
if foreign_key_reflection
|
879
|
+
if foreign_key_reflection.respond_to?(:foreign_key)
|
880
|
+
foreign_key_reflection.foreign_key.to_s
|
881
|
+
else
|
882
|
+
foreign_key_reflection.primary_key_name.to_s
|
883
|
+
end
|
884
|
+
end
|
885
|
+
end
|
886
|
+
|
887
|
+
def foreign_key_reflection
|
888
|
+
if [:has_one, :has_many].include?(macro) && reflection.options.include?(:inverse_of)
|
889
|
+
associated_class.reflect_on_association(reflection.options[:inverse_of])
|
890
|
+
else
|
891
|
+
reflection
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
def submatchers_match?
|
896
|
+
failing_submatchers.empty?
|
897
|
+
end
|
898
|
+
end
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|