mcmire-shoulda-matchers 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +32 -0
  4. data/.yardopts +7 -0
  5. data/Appraisals +45 -0
  6. data/CONTRIBUTING.md +41 -0
  7. data/Gemfile +31 -0
  8. data/Gemfile.lock +166 -0
  9. data/MIT-LICENSE +22 -0
  10. data/NEWS.md +299 -0
  11. data/README.md +163 -0
  12. data/Rakefile +116 -0
  13. data/doc_config/gh-pages/index.html.erb +9 -0
  14. data/doc_config/yard/setup.rb +22 -0
  15. data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
  18. data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
  19. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
  20. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
  21. data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
  22. data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
  23. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
  24. data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
  25. data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
  26. data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
  27. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
  28. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
  29. data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
  30. data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
  31. data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
  32. data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
  33. data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
  34. data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
  35. data/features/rails_integration.feature +113 -0
  36. data/features/step_definitions/rails_steps.rb +162 -0
  37. data/features/support/env.rb +5 -0
  38. data/gemfiles/3.0.gemfile +24 -0
  39. data/gemfiles/3.0.gemfile.lock +150 -0
  40. data/gemfiles/3.1.gemfile +27 -0
  41. data/gemfiles/3.1.gemfile.lock +173 -0
  42. data/gemfiles/3.2.gemfile +27 -0
  43. data/gemfiles/3.2.gemfile.lock +171 -0
  44. data/gemfiles/4.0.0.gemfile +28 -0
  45. data/gemfiles/4.0.0.gemfile.lock +172 -0
  46. data/gemfiles/4.0.1.gemfile +28 -0
  47. data/gemfiles/4.0.1.gemfile.lock +172 -0
  48. data/lib/shoulda-matchers.rb +1 -0
  49. data/lib/shoulda/matchers.rb +11 -0
  50. data/lib/shoulda/matchers/action_controller.rb +17 -0
  51. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
  52. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
  53. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
  54. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
  55. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
  56. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
  57. data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
  58. data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
  59. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
  60. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
  61. data/lib/shoulda/matchers/active_model.rb +30 -0
  62. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
  63. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
  64. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
  65. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
  66. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
  67. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
  68. data/lib/shoulda/matchers/active_model/errors.rb +10 -0
  69. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
  70. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
  71. data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
  72. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  73. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
  74. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
  75. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
  76. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
  77. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
  78. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
  79. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
  80. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
  81. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
  82. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
  83. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
  84. data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
  85. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
  86. data/lib/shoulda/matchers/active_record.rb +22 -0
  87. data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
  88. data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
  89. data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  90. data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
  91. data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
  92. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
  93. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
  94. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
  95. data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
  96. data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
  97. data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
  98. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
  99. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
  100. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
  101. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
  102. data/lib/shoulda/matchers/assertion_error.rb +19 -0
  103. data/lib/shoulda/matchers/error.rb +6 -0
  104. data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
  105. data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
  106. data/lib/shoulda/matchers/rails_shim.rb +50 -0
  107. data/lib/shoulda/matchers/version.rb +6 -0
  108. data/lib/shoulda/matchers/warn.rb +8 -0
  109. data/shoulda-matchers.gemspec +23 -0
  110. data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
  111. data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
  112. data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
  113. data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
  114. data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
  115. data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
  116. data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
  117. data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
  118. data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
  119. data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
  120. data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
  121. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
  122. data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
  123. data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
  124. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
  125. data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
  126. data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
  127. data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
  128. data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
  129. data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
  130. data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
  131. data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
  132. data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
  133. data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
  134. data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
  135. data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
  136. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
  137. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
  138. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
  139. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
  140. data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
  141. data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
  142. data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
  143. data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
  144. data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
  145. data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
  146. data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
  147. data/spec/spec_helper.rb +26 -0
  148. data/spec/support/active_model_versions.rb +13 -0
  149. data/spec/support/active_resource_builder.rb +29 -0
  150. data/spec/support/activemodel_helpers.rb +19 -0
  151. data/spec/support/capture_helpers.rb +19 -0
  152. data/spec/support/class_builder.rb +42 -0
  153. data/spec/support/controller_builder.rb +74 -0
  154. data/spec/support/fail_with_message_including_matcher.rb +33 -0
  155. data/spec/support/fail_with_message_matcher.rb +32 -0
  156. data/spec/support/i18n_faker.rb +10 -0
  157. data/spec/support/mailer_builder.rb +10 -0
  158. data/spec/support/model_builder.rb +81 -0
  159. data/spec/support/rails_versions.rb +18 -0
  160. data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
  161. data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
  162. data/spec/support/test_application.rb +120 -0
  163. data/yard.watchr +5 -0
  164. metadata +281 -0
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveModel::ValidationMessageFinder do
4
+ context '#allow_description' do
5
+ it 'describes its attribute' do
6
+ finder = build_finder(attribute: :attr)
7
+
8
+ description = finder.allow_description('allowed values')
9
+
10
+ expect(description).to eq 'allow attr to be set to allowed values'
11
+ end
12
+ end
13
+
14
+ context '#expected_message_from' do
15
+ it 'returns the message as-is' do
16
+ finder = build_finder
17
+
18
+ message = finder.expected_message_from('some message')
19
+
20
+ expect(message).to eq 'some message'
21
+ end
22
+ end
23
+
24
+ context '#has_messages?' do
25
+ it 'has messages when some validations fail' do
26
+ finder = build_finder(format: /abc/, value: 'xyz')
27
+
28
+ result = finder.has_messages?
29
+
30
+ expect(result).to eq true
31
+ end
32
+
33
+ it 'has no messages when all validations pass' do
34
+ finder = build_finder(format: /abc/, value: 'abc')
35
+
36
+ result = finder.has_messages?
37
+
38
+ expect(result).to eq false
39
+ end
40
+ end
41
+
42
+ context '#messages' do
43
+ it 'returns errors for the given attribute' do
44
+ finder = build_finder(format: /abc/, value: 'xyz')
45
+
46
+ messages = finder.messages
47
+
48
+ expect(messages).to eq ['is invalid']
49
+ end
50
+
51
+ it 'returns an empty array if there are no errors for the given attribute' do
52
+ finder = build_finder
53
+
54
+ messages = finder.messages
55
+
56
+ expect(messages).to eq([])
57
+ end
58
+ end
59
+
60
+ context '#messages_description' do
61
+ it 'describes errors for the given attribute' do
62
+ finder = build_finder(
63
+ attribute: :attr,
64
+ format: /abc/,
65
+ value: 'xyz'
66
+ )
67
+
68
+ description = finder.messages_description
69
+
70
+ expected_messages = ['attr is invalid ("xyz")']
71
+ expect(description).to eq "errors: #{expected_messages}"
72
+ end
73
+
74
+ it 'describes errors when there are none' do
75
+ finder = build_finder(format: /abc/, value: 'abc')
76
+
77
+ description = finder.messages_description
78
+
79
+ expect(description).to eq 'no errors'
80
+ end
81
+
82
+ it 'should not fetch attribute values for errors that were copied from an autosaved belongs_to association' do
83
+ instance = define_model(:example) do
84
+ validate do |record|
85
+ record.errors.add('association.association_attribute', 'is invalid')
86
+ end
87
+ end.new
88
+ finder = Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(instance, :attribute)
89
+
90
+ expected_messages = ['association.association_attribute is invalid']
91
+ expect(finder.messages_description).to eq "errors: #{expected_messages}"
92
+ end
93
+
94
+ end
95
+
96
+ context '#source_description' do
97
+ it 'describes the source of its messages' do
98
+ finder = build_finder
99
+
100
+ description = finder.source_description
101
+
102
+ expect(description).to eq 'errors'
103
+ end
104
+ end
105
+
106
+ def build_finder(arguments = {})
107
+ arguments[:attribute] ||= :attr
108
+ instance = build_instance_validating(
109
+ arguments[:attribute],
110
+ arguments[:format] || /abc/,
111
+ arguments[:value] || 'abc'
112
+ )
113
+ Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(
114
+ instance,
115
+ arguments[:attribute]
116
+ )
117
+ end
118
+
119
+ def build_instance_validating(attribute, format, value)
120
+ model_class = define_model(:example, attribute => :string) do
121
+ attr_accessible attribute
122
+ validates_format_of attribute, with: format
123
+ end
124
+
125
+ model_class.new(attribute => value)
126
+ end
127
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveRecord::AcceptNestedAttributesForMatcher do
4
+ it 'accepts an existing declaration' do
5
+ expect(accepting_children).to accept_nested_attributes_for(:children)
6
+ end
7
+
8
+ it 'rejects a missing declaration' do
9
+ matcher = children_matcher
10
+
11
+ expect(matcher.matches?(rejecting_children)).to eq false
12
+
13
+ expect(matcher.failure_message).
14
+ to eq 'Expected Parent to accept nested attributes for children (is not declared)'
15
+ end
16
+
17
+ context 'allow_destroy' do
18
+ it 'accepts a valid truthy value' do
19
+ matching = accepting_children(allow_destroy: true)
20
+
21
+ expect(matching).to children_matcher.allow_destroy(true)
22
+ end
23
+
24
+ it 'accepts a valid falsey value' do
25
+ matching = accepting_children(allow_destroy: false)
26
+
27
+ expect(matching).to children_matcher.allow_destroy(false)
28
+ end
29
+
30
+ it 'rejects an invalid truthy value' do
31
+ matcher = children_matcher
32
+ matching = accepting_children(allow_destroy: true)
33
+
34
+ expect(matcher.allow_destroy(false).matches?(matching)).to eq false
35
+ expect(matcher.failure_message).to match(/should not allow destroy/)
36
+ end
37
+
38
+ it 'rejects an invalid falsey value' do
39
+ matcher = children_matcher
40
+ matching = accepting_children(allow_destroy: false)
41
+
42
+ expect(matcher.allow_destroy(true).matches?(matching)).to eq false
43
+ expect(matcher.failure_message).to match(/should allow destroy/)
44
+ end
45
+ end
46
+
47
+ context 'limit' do
48
+ it 'accepts a correct value' do
49
+ expect(accepting_children(limit: 3)).to children_matcher.limit(3)
50
+ end
51
+
52
+ it 'rejects a false value' do
53
+ matcher = children_matcher
54
+ rejecting = accepting_children(limit: 3)
55
+
56
+ expect(matcher.limit(2).matches?(rejecting)).to eq false
57
+ expect(matcher.failure_message).to match(/limit should be 2, got 3/)
58
+ end
59
+ end
60
+
61
+ context 'update_only' do
62
+ it 'accepts a valid truthy value' do
63
+ expect(accepting_children(update_only: true)).
64
+ to children_matcher.update_only(true)
65
+ end
66
+
67
+ it 'accepts a valid falsey value' do
68
+ expect(accepting_children(update_only: false)).
69
+ to children_matcher.update_only(false)
70
+ end
71
+
72
+ it 'rejects an invalid truthy value' do
73
+ matcher = children_matcher.update_only(false)
74
+ rejecting = accepting_children(update_only: true)
75
+
76
+ expect(matcher.matches?(rejecting)).to eq false
77
+ expect(matcher.failure_message).to match(/should not be update only/)
78
+ end
79
+
80
+ it 'rejects an invalid falsey value' do
81
+ matcher = children_matcher.update_only(true)
82
+ rejecting = accepting_children(update_only: false)
83
+
84
+ expect(matcher.matches?(rejecting)).to eq false
85
+ expect(matcher.failure_message).to match(/should be update only/)
86
+ end
87
+ end
88
+
89
+ def accepting_children(options = {})
90
+ define_model :child, parent_id: :integer
91
+ define_model :parent do
92
+ has_many :children
93
+ accepts_nested_attributes_for :children, options
94
+ end.new
95
+ end
96
+
97
+ def children_matcher
98
+ accept_nested_attributes_for(:children)
99
+ end
100
+
101
+ def rejecting_children
102
+ define_model :child, parent_id: :integer
103
+ define_model :parent do
104
+ has_many :children
105
+ end.new
106
+ end
107
+ end
@@ -0,0 +1,860 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do
4
+ context 'belong_to' do
5
+ it 'accepts a good association with the default foreign key' do
6
+ expect(belonging_to_parent).to belong_to(:parent)
7
+ end
8
+
9
+ it 'rejects a nonexistent association' do
10
+ expect(define_model(:child).new).not_to belong_to(:parent)
11
+ end
12
+
13
+ it 'rejects an association of the wrong type' do
14
+ define_model :parent, child_id: :integer
15
+ expect(define_model(:child) { has_one :parent }.new).not_to belong_to(:parent)
16
+ end
17
+
18
+ it 'rejects an association that has a nonexistent foreign key' do
19
+ define_model :parent
20
+ expect(define_model(:child) { belongs_to :parent }.new).not_to belong_to(:parent)
21
+ end
22
+
23
+ it 'accepts an association with an existing custom foreign key' do
24
+ define_model :parent
25
+ define_model :child, guardian_id: :integer do
26
+ belongs_to :parent, foreign_key: 'guardian_id'
27
+ end
28
+
29
+ expect(Child.new).to belong_to(:parent)
30
+ end
31
+
32
+ it 'accepts a polymorphic association' do
33
+ define_model :parent
34
+ define_model :child, parent_type: :string, parent_id: :integer do
35
+ belongs_to :parent, polymorphic: true
36
+ end
37
+
38
+ expect(Child.new).to belong_to(:parent)
39
+ end
40
+
41
+ it 'accepts an association with a valid :dependent option' do
42
+ expect(belonging_to_parent(dependent: :destroy)).
43
+ to belong_to(:parent).dependent(:destroy)
44
+ end
45
+
46
+ it 'rejects an association with a bad :dependent option' do
47
+ expect(belonging_to_parent).not_to belong_to(:parent).dependent(:destroy)
48
+ end
49
+
50
+ it 'accepts an association with a valid :counter_cache option' do
51
+ expect(belonging_to_parent(counter_cache: :attribute_count)).
52
+ to belong_to(:parent).counter_cache(:attribute_count)
53
+ end
54
+
55
+ it 'defaults :counter_cache to true' do
56
+ expect(belonging_to_parent(counter_cache: true)).
57
+ to belong_to(:parent).counter_cache
58
+ end
59
+
60
+ it 'rejects an association with a bad :counter_cache option' do
61
+ expect(belonging_to_parent(counter_cache: :attribute_count)).
62
+ not_to belong_to(:parent).counter_cache(true)
63
+ end
64
+
65
+ it 'rejects an association that has no :counter_cache option' do
66
+ expect(belonging_to_parent).not_to belong_to(:parent).counter_cache
67
+ end
68
+
69
+ it 'accepts an association with a valid :conditions option' do
70
+ define_model :parent, adopter: :boolean
71
+ define_model(:child, parent_id: :integer).tap do |model|
72
+ define_association_with_conditions(model, :belongs_to, :parent, adopter: true)
73
+ end
74
+
75
+ expect(Child.new).to belong_to(:parent).conditions(adopter: true)
76
+ end
77
+
78
+ it 'rejects an association with a bad :conditions option' do
79
+ define_model :parent, adopter: :boolean
80
+ define_model :child, parent_id: :integer do
81
+ belongs_to :parent
82
+ end
83
+
84
+ expect(Child.new).not_to belong_to(:parent).conditions(adopter: true)
85
+ end
86
+
87
+ it 'accepts an association without a :class_name option' do
88
+ expect(belonging_to_parent).to belong_to(:parent).class_name('Parent')
89
+ end
90
+
91
+ it 'accepts an association with a valid :class_name option' do
92
+ define_model :tree_parent
93
+ define_model :child, parent_id: :integer do
94
+ belongs_to :parent, class_name: 'TreeParent'
95
+ end
96
+
97
+ expect(Child.new).to belong_to(:parent).class_name('TreeParent')
98
+ end
99
+
100
+ it 'rejects an association with a bad :class_name option' do
101
+ expect(belonging_to_parent).not_to belong_to(:parent).class_name('TreeChild')
102
+ end
103
+
104
+ it 'rejects an association with non-existent implicit class name' do
105
+ expect(belonging_to_non_existent_class(:child, :parent)).not_to belong_to(:parent)
106
+ end
107
+
108
+ it 'rejects an association with non-existent explicit class name' do
109
+ expect(belonging_to_non_existent_class(:child, :parent, class_name: 'Parent')).not_to belong_to(:parent)
110
+ end
111
+
112
+ it 'adds error message when rejecting an association with non-existent class' do
113
+ message = 'Expected Child to have a belongs_to association called parent (Parent2 does not exist)'
114
+ expect {
115
+ expect(belonging_to_non_existent_class(:child, :parent, class_name: 'Parent2')).to belong_to(:parent)
116
+ }.to fail_with_message(message)
117
+ end
118
+
119
+ it 'accepts an association with a matching :autosave option' do
120
+ define_model :parent, adopter: :boolean
121
+ define_model :child, parent_id: :integer do
122
+ belongs_to :parent, autosave: true
123
+ end
124
+ expect(Child.new).to belong_to(:parent).autosave(true)
125
+ end
126
+
127
+ it 'rejects an association with a non-matching :autosave option with the correct message' do
128
+ define_model :parent, adopter: :boolean
129
+ define_model :child, parent_id: :integer do
130
+ belongs_to :parent, autosave: false
131
+ end
132
+
133
+ message = 'Expected Child to have a belongs_to association called parent (parent should have autosave set to true)'
134
+ expect {
135
+ expect(Child.new).to belong_to(:parent).autosave(true)
136
+ }.to fail_with_message(message)
137
+ end
138
+
139
+ context 'an association with a :validate option' do
140
+ [false, true].each do |validate_value|
141
+ context "when the model has validate: #{validate_value}" do
142
+ it 'accepts a matching validate option' do
143
+ expect(belonging_to_parent(validate: validate_value)).
144
+ to belong_to(:parent).validate(validate_value)
145
+ end
146
+
147
+ it 'rejects a non-matching validate option' do
148
+ expect(belonging_to_parent(validate: validate_value)).
149
+ not_to belong_to(:parent).validate(!validate_value)
150
+ end
151
+
152
+ it 'defaults to validate(true)' do
153
+ if validate_value
154
+ expect(belonging_to_parent(validate: validate_value)).
155
+ to belong_to(:parent).validate
156
+ else
157
+ expect(belonging_to_parent(validate: validate_value)).
158
+ not_to belong_to(:parent).validate
159
+ end
160
+ end
161
+
162
+ it 'will not break matcher when validate option is unspecified' do
163
+ expect(belonging_to_parent(validate: validate_value)).to belong_to(:parent)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ context 'an association without a :validate option' do
170
+ it 'accepts validate(false)' do
171
+ expect(belonging_to_parent).to belong_to(:parent).validate(false)
172
+ end
173
+
174
+ it 'rejects validate(true)' do
175
+ expect(belonging_to_parent).not_to belong_to(:parent).validate(true)
176
+ end
177
+
178
+ it 'rejects validate()' do
179
+ expect(belonging_to_parent).not_to belong_to(:parent).validate
180
+ end
181
+ end
182
+
183
+ context 'an association with a :touch option' do
184
+ [false, true].each do |touch_value|
185
+ context "when the model has touch: #{touch_value}" do
186
+ it 'accepts a matching touch option' do
187
+ expect(belonging_to_parent(touch: touch_value)).
188
+ to belong_to(:parent).touch(touch_value)
189
+ end
190
+
191
+ it 'rejects a non-matching touch option' do
192
+ expect(belonging_to_parent(touch: touch_value)).
193
+ not_to belong_to(:parent).touch(!touch_value)
194
+ end
195
+
196
+ it 'defaults to touch(true)' do
197
+ if touch_value
198
+ expect(belonging_to_parent(touch: touch_value)).
199
+ to belong_to(:parent).touch
200
+ else
201
+ expect(belonging_to_parent(touch: touch_value)).
202
+ not_to belong_to(:parent).touch
203
+ end
204
+ end
205
+
206
+ it 'will not break matcher when touch option is unspecified' do
207
+ expect(belonging_to_parent(touch: touch_value)).to belong_to(:parent)
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ context 'an association without a :touch option' do
214
+ it 'accepts touch(false)' do
215
+ expect(belonging_to_parent).to belong_to(:parent).touch(false)
216
+ end
217
+
218
+ it 'rejects touch(true)' do
219
+ expect(belonging_to_parent).not_to belong_to(:parent).touch(true)
220
+ end
221
+
222
+ it 'rejects touch()' do
223
+ expect(belonging_to_parent).not_to belong_to(:parent).touch
224
+ end
225
+ end
226
+
227
+ def belonging_to_parent(options = {})
228
+ define_model :parent
229
+ define_model :child, parent_id: :integer do
230
+ belongs_to :parent, options
231
+ end.new
232
+ end
233
+
234
+ def belonging_to_non_existent_class(model_name, assoc_name, options = {})
235
+ define_model model_name, "#{assoc_name}_id" => :integer do
236
+ belongs_to assoc_name, options
237
+ end.new
238
+ end
239
+ end
240
+
241
+ context 'have_many' do
242
+ it 'accepts a valid association without any options' do
243
+ expect(having_many_children).to have_many(:children)
244
+ end
245
+
246
+ it 'accepts a valid association with a :through option' do
247
+ define_model :child
248
+ define_model :conception, child_id: :integer,
249
+ parent_id: :integer do
250
+ belongs_to :child
251
+ end
252
+ define_model :parent do
253
+ has_many :conceptions
254
+ has_many :children, through: :conceptions
255
+ end
256
+ expect(Parent.new).to have_many(:children)
257
+ end
258
+
259
+ it 'accepts a valid association with an :as option' do
260
+ define_model :child, guardian_type: :string, guardian_id: :integer
261
+ define_model :parent do
262
+ has_many :children, as: :guardian
263
+ end
264
+
265
+ expect(Parent.new).to have_many(:children)
266
+ end
267
+
268
+ it 'rejects an association that has a nonexistent foreign key' do
269
+ define_model :child
270
+ define_model :parent do
271
+ has_many :children
272
+ end
273
+
274
+ expect(Parent.new).not_to have_many(:children)
275
+ end
276
+
277
+ it 'rejects an association with a bad :as option' do
278
+ define_model :child, caretaker_type: :string,
279
+ caretaker_id: :integer
280
+ define_model :parent do
281
+ has_many :children, as: :guardian
282
+ end
283
+
284
+ expect(Parent.new).not_to have_many(:children)
285
+ end
286
+
287
+ it 'rejects an association that has a bad :through option' do
288
+ matcher = have_many(:children).through(:conceptions)
289
+
290
+ expect(matcher.matches?(having_many_children)).to eq false
291
+
292
+ expect(matcher.failure_message).to match(/does not have any relationship to conceptions/)
293
+ end
294
+
295
+ it 'rejects an association that has the wrong :through option' do
296
+ define_model :child
297
+
298
+ define_model :conception, child_id: :integer,
299
+ parent_id: :integer do
300
+ belongs_to :child
301
+ end
302
+
303
+ define_model :parent do
304
+ has_many :conceptions
305
+ has_many :relationships
306
+ has_many :children, through: :conceptions
307
+ end
308
+
309
+ matcher = have_many(:children).through(:relationships)
310
+ expect(matcher.matches?(Parent.new)).to eq false
311
+ expect(matcher.failure_message).to match(/through relationships, but got it through conceptions/)
312
+ end
313
+
314
+ it 'accepts an association with a valid :dependent option' do
315
+ expect(having_many_children(dependent: :destroy)).
316
+ to have_many(:children).dependent(:destroy)
317
+ end
318
+
319
+ it 'rejects an association with a bad :dependent option' do
320
+ matcher = have_many(:children).dependent(:destroy)
321
+
322
+ expect(having_many_children).not_to matcher
323
+
324
+ expect(matcher.failure_message).to match(/children should have destroy dependency/)
325
+ end
326
+
327
+ it 'accepts an association with a valid :source option' do
328
+ expect(having_many_children(source: :user)).
329
+ to have_many(:children).source(:user)
330
+ end
331
+
332
+ it 'rejects an association with a bad :source option' do
333
+ matcher = have_many(:children).source(:user)
334
+
335
+ expect(having_many_children).not_to matcher
336
+
337
+ expect(matcher.failure_message).to match(/children should have user as source option/)
338
+ end
339
+
340
+ it 'accepts an association with a valid :order option' do
341
+ expect(having_many_children(order: :id)).
342
+ to have_many(:children).order(:id)
343
+ end
344
+
345
+ it 'rejects an association with a bad :order option' do
346
+ matcher = have_many(:children).order(:id)
347
+
348
+ expect(having_many_children).not_to matcher
349
+
350
+ expect(matcher.failure_message).to match(/children should be ordered by id/)
351
+ end
352
+
353
+ it 'accepts an association with a valid :conditions option' do
354
+ define_model :child, parent_id: :integer, adopted: :boolean
355
+ define_model(:parent).tap do |model|
356
+ define_association_with_conditions(model, :has_many, :children, adopted: true)
357
+ end
358
+
359
+ expect(Parent.new).to have_many(:children).conditions(adopted: true)
360
+ end
361
+
362
+ it 'rejects an association with a bad :conditions option' do
363
+ define_model :child, parent_id: :integer, adopted: :boolean
364
+ define_model :parent do
365
+ has_many :children
366
+ end
367
+
368
+ expect(Parent.new).not_to have_many(:children).conditions(adopted: true)
369
+ end
370
+
371
+ it 'accepts an association without a :class_name option' do
372
+ expect(having_many_children).to have_many(:children).class_name('Child')
373
+ end
374
+
375
+ it 'accepts an association with a valid :class_name option' do
376
+ define_model :node, parent_id: :integer
377
+ define_model :parent do
378
+ has_many :children, class_name: 'Node'
379
+ end
380
+
381
+ expect(Parent.new).to have_many(:children).class_name('Node')
382
+ end
383
+
384
+ it 'rejects an association with a bad :class_name option' do
385
+ expect(having_many_children).not_to have_many(:children).class_name('Node')
386
+ end
387
+
388
+ it 'rejects an association with non-existent implicit class name' do
389
+ expect(having_many_non_existent_class(:parent, :children)).not_to have_many(:children)
390
+ end
391
+
392
+ it 'rejects an association with non-existent explicit class name' do
393
+ expect(having_many_non_existent_class(:parent, :children, class_name: 'Child')).not_to have_many(:children)
394
+ end
395
+
396
+ it 'adds error message when rejecting an association with non-existent class' do
397
+ message = 'Expected Parent to have a has_many association called children (Child2 does not exist)'
398
+ expect {
399
+ expect(having_many_non_existent_class(:parent, :children, class_name: 'Child2')).to have_many(:children)
400
+ }.to fail_with_message(message)
401
+ end
402
+
403
+ it 'accepts an association with a matching :autosave option' do
404
+ define_model :child, parent_id: :integer
405
+ define_model :parent do
406
+ has_many :children, autosave: true
407
+ end
408
+ expect(Parent.new).to have_many(:children).autosave(true)
409
+ end
410
+
411
+ it 'rejects an association with a non-matching :autosave option with the correct message' do
412
+ define_model :child, parent_id: :integer
413
+ define_model :parent do
414
+ has_many :children, autosave: false
415
+ end
416
+
417
+ message = 'Expected Parent to have a has_many association called children (children should have autosave set to true)'
418
+ expect {
419
+ expect(Parent.new).to have_many(:children).autosave(true)
420
+ }.to fail_with_message(message)
421
+ end
422
+
423
+ context 'validate' do
424
+ it 'accepts when the :validate option matches' do
425
+ expect(having_many_children(validate: false)).to have_many(:children).validate(false)
426
+ end
427
+
428
+ it 'rejects when the :validate option does not match' do
429
+ expect(having_many_children(validate: true)).not_to have_many(:children).validate(false)
430
+ end
431
+
432
+ it 'assumes validate() means validate(true)' do
433
+ expect(having_many_children(validate: false)).not_to have_many(:children).validate
434
+ end
435
+
436
+ it 'matches validate(false) to having no validate option specified' do
437
+ expect(having_many_children).to have_many(:children).validate(false)
438
+ end
439
+ end
440
+
441
+ it 'accepts an association with a nonstandard reverse foreign key, using :inverse_of' do
442
+ define_model :child, ancestor_id: :integer do
443
+ belongs_to :ancestor, inverse_of: :children, class_name: :Parent
444
+ end
445
+
446
+ define_model :parent do
447
+ has_many :children, inverse_of: :ancestor
448
+ end
449
+
450
+ expect(Parent.new).to have_many(:children)
451
+ end
452
+
453
+ it 'rejects an association with a nonstandard reverse foreign key, if :inverse_of is not correct' do
454
+ define_model :child, mother_id: :integer do
455
+ belongs_to :mother, inverse_of: :children, class_name: :Parent
456
+ end
457
+
458
+ define_model :parent do
459
+ has_many :children, inverse_of: :ancestor
460
+ end
461
+
462
+ expect(Parent.new).not_to have_many(:children)
463
+ end
464
+
465
+ def having_many_children(options = {})
466
+ define_model :child, parent_id: :integer
467
+ define_model(:parent).tap do |model|
468
+ if options.key?(:order)
469
+ order = options.delete(:order)
470
+ define_association_with_order(model, :has_many, :children, order, options)
471
+ else
472
+ model.has_many :children, options
473
+ end
474
+ end.new
475
+ end
476
+
477
+ def having_many_non_existent_class(model_name, assoc_name, options = {})
478
+ define_model model_name do
479
+ has_many assoc_name, options
480
+ end.new
481
+ end
482
+ end
483
+
484
+ context 'have_one' do
485
+ it 'accepts a valid association without any options' do
486
+ expect(having_one_detail).to have_one(:detail)
487
+ end
488
+
489
+ it 'accepts a valid association with an :as option' do
490
+ define_model :detail, detailable_id: :integer,
491
+ detailable_type: :string
492
+ define_model :person do
493
+ has_one :detail, as: :detailable
494
+ end
495
+
496
+ expect(Person.new).to have_one(:detail)
497
+ end
498
+
499
+ it 'rejects an association that has a nonexistent foreign key' do
500
+ define_model :detail
501
+ define_model :person do
502
+ has_one :detail
503
+ end
504
+
505
+ expect(Person.new).not_to have_one(:detail)
506
+ end
507
+
508
+ it 'accepts an association with an existing custom foreign key' do
509
+ define_model :detail, detailed_person_id: :integer
510
+ define_model :person do
511
+ has_one :detail, foreign_key: :detailed_person_id
512
+ end
513
+ expect(Person.new).to have_one(:detail).with_foreign_key(:detailed_person_id)
514
+ end
515
+
516
+ it 'rejects an association with a bad :as option' do
517
+ define_model :detail, detailable_id: :integer,
518
+ detailable_type: :string
519
+ define_model :person do
520
+ has_one :detail, as: :describable
521
+ end
522
+
523
+ expect(Person.new).not_to have_one(:detail)
524
+ end
525
+
526
+ it 'accepts an association with a valid :dependent option' do
527
+ expect(having_one_detail(dependent: :destroy)).
528
+ to have_one(:detail).dependent(:destroy)
529
+ end
530
+
531
+ it 'rejects an association with a bad :dependent option' do
532
+ matcher = have_one(:detail).dependent(:destroy)
533
+
534
+ expect(having_one_detail).not_to matcher
535
+
536
+ expect(matcher.failure_message).to match(/detail should have destroy dependency/)
537
+ end
538
+
539
+ it 'accepts an association with a valid :order option' do
540
+ expect(having_one_detail(order: :id)).to have_one(:detail).order(:id)
541
+ end
542
+
543
+ it 'rejects an association with a bad :order option' do
544
+ matcher = have_one(:detail).order(:id)
545
+
546
+ expect(having_one_detail).not_to matcher
547
+
548
+ expect(matcher.failure_message).to match(/detail should be ordered by id/)
549
+ end
550
+
551
+ it 'accepts an association with a valid :conditions option' do
552
+ define_model :detail, person_id: :integer, disabled: :boolean
553
+ define_model(:person).tap do |model|
554
+ define_association_with_conditions(model, :has_one, :detail, disabled: true)
555
+ end
556
+
557
+ expect(Person.new).to have_one(:detail).conditions(disabled: true)
558
+ end
559
+
560
+ it 'rejects an association with a bad :conditions option' do
561
+ define_model :detail, person_id: :integer, disabled: :boolean
562
+ define_model :person do
563
+ has_one :detail
564
+ end
565
+
566
+ expect(Person.new).not_to have_one(:detail).conditions(disabled: true)
567
+ end
568
+
569
+ it 'accepts an association without a :class_name option' do
570
+ expect(having_one_detail).to have_one(:detail).class_name('Detail')
571
+ end
572
+
573
+ it 'accepts an association with a matching :autosave option' do
574
+ define_model :detail, person_id: :integer, disabled: :boolean
575
+ define_model :person do
576
+ has_one :detail, autosave: true
577
+ end
578
+ expect(Person.new).to have_one(:detail).autosave(true)
579
+ end
580
+
581
+ it 'rejects an association with a non-matching :autosave option with the correct message' do
582
+ define_model :detail, person_id: :integer, disabled: :boolean
583
+ define_model :person do
584
+ has_one :detail, autosave: false
585
+ end
586
+
587
+ message = 'Expected Person to have a has_one association called detail (detail should have autosave set to true)'
588
+ expect {
589
+ expect(Person.new).to have_one(:detail).autosave(true)
590
+ }.to fail_with_message(message)
591
+ end
592
+
593
+ it 'accepts an association with a valid :class_name option' do
594
+ define_model :person_detail, person_id: :integer
595
+ define_model :person do
596
+ has_one :detail, class_name: 'PersonDetail'
597
+ end
598
+
599
+ expect(Person.new).to have_one(:detail).class_name('PersonDetail')
600
+ end
601
+
602
+ it 'rejects an association with a bad :class_name option' do
603
+ expect(having_one_detail).not_to have_one(:detail).class_name('NotSet')
604
+ end
605
+
606
+ it 'rejects an association with non-existent implicit class name' do
607
+ expect(having_one_non_existent(:pserson, :detail)).not_to have_one(:detail)
608
+ end
609
+
610
+ it 'rejects an association with non-existent explicit class name' do
611
+ expect(having_one_non_existent(:person, :detail, class_name: 'Detail')).not_to have_one(:detail)
612
+ end
613
+
614
+ it 'adds error message when rejecting an association with non-existent class' do
615
+ message = 'Expected Person to have a has_one association called detail (Detail2 does not exist)'
616
+ expect {
617
+ expect(having_one_non_existent(:person, :detail, class_name: 'Detail2')).to have_one(:detail)
618
+ }.to fail_with_message(message)
619
+ end
620
+
621
+ it 'accepts an association with a through' do
622
+ define_model :detail
623
+
624
+ define_model :account do
625
+ has_one :detail
626
+ end
627
+
628
+ define_model :person do
629
+ has_one :account
630
+ has_one :detail, through: :account
631
+ end
632
+
633
+ expect(Person.new).to have_one(:detail).through(:account)
634
+ end
635
+
636
+ it 'rejects an association with a bad through' do
637
+ expect(having_one_detail).not_to have_one(:detail).through(:account)
638
+ end
639
+
640
+ context 'validate' do
641
+ it 'accepts when the :validate option matches' do
642
+ expect(having_one_detail(validate: false)).
643
+ to have_one(:detail).validate(false)
644
+ end
645
+
646
+ it 'rejects when the :validate option does not match' do
647
+ expect(having_one_detail(validate: true)).
648
+ not_to have_one(:detail).validate(false)
649
+ end
650
+
651
+ it 'assumes validate() means validate(true)' do
652
+ expect(having_one_detail(validate: false)).
653
+ not_to have_one(:detail).validate
654
+ end
655
+
656
+ it 'matches validate(false) to having no validate option specified' do
657
+ expect(having_one_detail).to have_one(:detail).validate(false)
658
+ end
659
+ end
660
+
661
+ def having_one_detail(options = {})
662
+ define_model :detail, person_id: :integer
663
+ define_model(:person).tap do |model|
664
+ if options.key?(:order)
665
+ order = options.delete(:order)
666
+ define_association_with_order(model, :has_one, :detail, order, options)
667
+ else
668
+ model.has_one :detail, options
669
+ end
670
+ end.new
671
+ end
672
+
673
+ def having_one_non_existent(model_name, assoc_name, options = {})
674
+ define_model model_name do
675
+ has_one assoc_name, options
676
+ end.new
677
+ end
678
+ end
679
+
680
+ context 'have_and_belong_to_many' do
681
+ it 'accepts a valid association' do
682
+ expect(having_and_belonging_to_many_relatives).
683
+ to have_and_belong_to_many(:relatives)
684
+ end
685
+
686
+ it 'rejects a nonexistent association' do
687
+ define_model :relative
688
+ define_model :person
689
+ define_model :people_relative, id: false, person_id: :integer,
690
+ relative_id: :integer
691
+
692
+ expect(Person.new).not_to have_and_belong_to_many(:relatives)
693
+ end
694
+
695
+ it 'rejects an association with a nonexistent join table' do
696
+ define_model :relative
697
+ define_model :person do
698
+ has_and_belongs_to_many :relatives
699
+ end
700
+
701
+ expect(Person.new).not_to have_and_belong_to_many(:relatives)
702
+ end
703
+
704
+ it 'rejects an association of the wrong type' do
705
+ define_model :relative, person_id: :integer
706
+ define_model :person do
707
+ has_many :relatives
708
+ end
709
+
710
+ expect(Person.new).not_to have_and_belong_to_many(:relatives)
711
+ end
712
+
713
+ it 'accepts an association with a valid :conditions option' do
714
+ define_model :relative, adopted: :boolean
715
+ define_model(:person).tap do |model|
716
+ define_association_with_conditions(model, :has_and_belongs_to_many, :relatives, adopted: true)
717
+ end
718
+ define_model :people_relative, id: false, person_id: :integer,
719
+ relative_id: :integer
720
+
721
+ expect(Person.new).to have_and_belong_to_many(:relatives).conditions(adopted: true)
722
+ end
723
+
724
+ it 'rejects an association with a bad :conditions option' do
725
+ define_model :relative, adopted: :boolean
726
+ define_model :person do
727
+ has_and_belongs_to_many :relatives
728
+ end
729
+ define_model :people_relative, id: false, person_id: :integer,
730
+ relative_id: :integer
731
+
732
+ expect(Person.new).not_to have_and_belong_to_many(:relatives).conditions(adopted: true)
733
+ end
734
+
735
+ it 'accepts an association without a :class_name option' do
736
+ expect(having_and_belonging_to_many_relatives).
737
+ to have_and_belong_to_many(:relatives).class_name('Relative')
738
+ end
739
+
740
+ it 'accepts an association with a valid :class_name option' do
741
+ define_model :person_relative, adopted: :boolean
742
+ define_model :person do
743
+ has_and_belongs_to_many :relatives, class_name: 'PersonRelative'
744
+ end
745
+
746
+ define_model :people_person_relative, person_id: :integer,
747
+ person_relative_id: :integer
748
+
749
+ expect(Person.new).to have_and_belong_to_many(:relatives).class_name('PersonRelative')
750
+ end
751
+
752
+ it 'rejects an association with a bad :class_name option' do
753
+ expect(having_and_belonging_to_many_relatives).
754
+ not_to have_and_belong_to_many(:relatives).class_name('PersonRelatives')
755
+ end
756
+
757
+ it 'rejects an association with non-existent implicit class name' do
758
+ expect(having_and_belonging_to_many_non_existent_class(:person, :relatives)).
759
+ not_to have_and_belong_to_many(:relatives)
760
+ end
761
+
762
+ it 'rejects an association with non-existent explicit class name' do
763
+ expect(having_and_belonging_to_many_non_existent_class(:person, :relatives, class_name: 'Relative')).
764
+ not_to have_and_belong_to_many(:relatives)
765
+ end
766
+
767
+ it 'adds error message when rejecting an association with non-existent class' do
768
+ message = 'Expected Person to have a has_and_belongs_to_many association called relatives (Relative2 does not exist)'
769
+ expect {
770
+ expect(having_and_belonging_to_many_non_existent_class(:person, :relatives, class_name: 'Relative2')).
771
+ to have_and_belong_to_many(:relatives)
772
+ }.to fail_with_message(message)
773
+ end
774
+
775
+ it 'accepts an association with a matching :autosave option' do
776
+ define_model :relatives, adopted: :boolean
777
+ define_model :person do
778
+ has_and_belongs_to_many :relatives, autosave: true
779
+ end
780
+ define_model :people_relative, person_id: :integer,
781
+ relative_id: :integer
782
+ expect(Person.new).to have_and_belong_to_many(:relatives).autosave(true)
783
+ end
784
+
785
+ it 'rejects an association with a non-matching :autosave option with the correct message' do
786
+ define_model :relatives, adopted: :boolean
787
+ define_model :person do
788
+ has_and_belongs_to_many :relatives
789
+ end
790
+ define_model :people_relative, person_id: :integer,
791
+ relative_id: :integer
792
+
793
+ message = 'Expected Person to have a has_and_belongs_to_many association called relatives (relatives should have autosave set to true)'
794
+ expect {
795
+ expect(Person.new).to have_and_belong_to_many(:relatives).autosave(true)
796
+ }.to fail_with_message(message)
797
+ end
798
+
799
+ context 'validate' do
800
+ it 'accepts when the :validate option matches' do
801
+ expect(having_and_belonging_to_many_relatives(validate: false)).
802
+ to have_and_belong_to_many(:relatives).validate(false)
803
+ end
804
+
805
+ it 'rejects when the :validate option does not match' do
806
+ expect(having_and_belonging_to_many_relatives(validate: true)).
807
+ to have_and_belong_to_many(:relatives).validate(false)
808
+ end
809
+
810
+ it 'assumes validate() means validate(true)' do
811
+ expect(having_and_belonging_to_many_relatives(validate: false)).
812
+ not_to have_and_belong_to_many(:relatives).validate
813
+ end
814
+
815
+ it 'matches validate(false) to having no validate option specified' do
816
+ expect(having_and_belonging_to_many_relatives).
817
+ to have_and_belong_to_many(:relatives).validate(false)
818
+ end
819
+ end
820
+
821
+ def having_and_belonging_to_many_relatives(options = {})
822
+ define_model :relative
823
+ define_model :people_relative, id: false, person_id: :integer,
824
+ relative_id: :integer
825
+ define_model :person do
826
+ has_and_belongs_to_many :relatives
827
+ end.new
828
+ end
829
+
830
+ def having_and_belonging_to_many_non_existent_class(model_name, assoc_name, options = {})
831
+ define_model model_name do
832
+ has_and_belongs_to_many assoc_name, options
833
+ end.new
834
+ end
835
+ end
836
+
837
+ def define_association_with_conditions(model, macro, name, conditions, other_options={})
838
+ args = []
839
+ options = {}
840
+ if Shoulda::Matchers::RailsShim.active_record_major_version == 4
841
+ args << lambda { where(conditions) }
842
+ else
843
+ options[:conditions] = conditions
844
+ end
845
+ args << options
846
+ model.__send__(macro, name, *args)
847
+ end
848
+
849
+ def define_association_with_order(model, macro, name, order, other_options={})
850
+ args = []
851
+ options = {}
852
+ if Shoulda::Matchers::RailsShim.active_record_major_version == 4
853
+ args << lambda { order(order) }
854
+ else
855
+ options[:order] = order
856
+ end
857
+ args << options
858
+ model.__send__(macro, name, *args)
859
+ end
860
+ end