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.
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