2 require "models/pirate"
5 require "models/parrot"
6 require "models/treasure"
8 module AssertRaiseWithMessage
9 def assert_raise_with_message(expected_exception
, expected_message
)
13 rescue expected_exception
=> error
15 actual_message
= error
.message
18 assert_equal expected_message
, actual_message
22 class TestNestedAttributesInGeneral
< ActiveRecord
::TestCase
23 include AssertRaiseWithMessage
26 Pirate
.accepts_nested_attributes_for
:ship, :allow_destroy => true, :reject_if => proc
{ |attributes
| attributes
.empty
? }
29 def test_base_should_have_an_empty_reject_new_nested_attributes_procs
30 assert_equal Hash
.new
, ActiveRecord
::Base.reject_new_nested_attributes_procs
33 def test_should_add_a_proc_to_reject_new_nested_attributes_procs
34 [:parrots, :birds].each
do |name
|
35 assert_instance_of Proc
, Pirate
.reject_new_nested_attributes_procs
[name
]
39 def test_should_raise_an_ArgumentError_for_non_existing_associations
40 assert_raise_with_message ArgumentError
, "No association found for name `honesty'. Has it been defined yet?" do
41 Pirate
.accepts_nested_attributes_for
:honesty
45 def test_should_disable_allow_destroy_by_default
46 Pirate
.accepts_nested_attributes_for
:ship
48 pirate
= Pirate
.create
!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
49 ship
= pirate
.create_ship(:name => 'Nights Dirty Lightning')
51 assert_no_difference('Ship.count') do
52 pirate
.update_attributes(:ship_attributes => { '_delete' => true })
56 def test_a_model_should_respond_to_underscore_delete_and_return_if_it_is_marked_for_destruction
57 ship
= Ship
.create
!(:name => 'Nights Dirty Lightning')
59 ship
.mark_for_destruction
64 class TestNestedAttributesOnAHasOneAssociation
< ActiveRecord
::TestCase
66 @pirate = Pirate
.create
!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
67 @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
70 def test_should_define_an_attribute_writer_method_for_the_association
71 assert_respond_to
@pirate, :ship_attributes=
74 def test_should_build_a_new_record_if_there_is_no_id
76 @pirate.reload
.ship_attributes
= { :name => 'Davy Jones Gold Dagger' }
78 assert
@pirate.ship
.new_record
?
79 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
82 def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
84 @pirate.reload
.ship_attributes
= { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
86 assert_nil
@pirate.ship
89 def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
91 @pirate.reload
.ship_attributes
= {}
93 assert_nil
@pirate.ship
96 def test_should_replace_an_existing_record_if_there_is_no_id
97 @pirate.reload
.ship_attributes
= { :name => 'Davy Jones Gold Dagger' }
99 assert
@pirate.ship
.new_record
?
100 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
101 assert_equal
'Nights Dirty Lightning', @ship.name
104 def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
105 @pirate.reload
.ship_attributes
= { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
107 assert_equal
@ship, @pirate.ship
108 assert_equal
'Nights Dirty Lightning', @pirate.ship
.name
111 def test_should_modify_an_existing_record_if_there_is_a_matching_id
112 @pirate.reload
.ship_attributes
= { :id => @ship.id
, :name => 'Davy Jones Gold Dagger' }
114 assert_equal
@ship, @pirate.ship
115 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
118 def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
119 @pirate.reload
.ship_attributes
= { 'id' => @ship.id
, 'name' => 'Davy Jones Gold Dagger' }
121 assert_equal
@ship, @pirate.ship
122 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
125 def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
126 @ship.stubs(:id).returns('ABC1X')
127 @pirate.ship_attributes
= { :id => @ship.id
, :name => 'Davy Jones Gold Dagger' }
129 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
132 def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
134 [1, '1', true, 'true'].each
do |truth
|
135 @pirate.reload
.create_ship(:name => 'Mister Pablo')
136 assert_difference('Ship.count', -1) do
137 @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship
.id
, :_delete => truth
})
142 def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
143 [nil, '0', 0, 'false', false].each
do |not_truth
|
144 assert_no_difference('Ship.count') do
145 @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship
.id
, :_delete => not_truth
})
150 def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
151 Pirate
.accepts_nested_attributes_for
:ship, :allow_destroy => false, :reject_if => proc
{ |attributes
| attributes
.empty
? }
153 assert_no_difference('Ship.count') do
154 @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship
.id
, :_delete => '1' })
157 Pirate
.accepts_nested_attributes_for
:ship, :allow_destroy => true, :reject_if => proc
{ |attributes
| attributes
.empty
? }
160 def test_should_also_work_with_a_HashWithIndifferentAccess
161 @pirate.ship_attributes
= HashWithIndifferentAccess
.new(:id => @ship.id
, :name => 'Davy Jones Gold Dagger')
163 assert
!@pirate.ship
.new_record
?
164 assert_equal
'Davy Jones Gold Dagger', @pirate.ship
.name
167 def test_should_work_with_update_attributes_as_well
168 @pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id
, :name => 'Mister Pablo' } })
171 assert_equal
'Arr', @pirate.catchphrase
172 assert_equal
'Mister Pablo', @pirate.ship
.name
175 def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
176 assert_no_difference('Ship.count') do
177 @pirate.attributes
= { :ship_attributes => { :id => @ship.id
, :_delete => '1' } }
179 assert_difference('Ship.count', -1) do
184 def test_should_automatically_enable_autosave_on_the_association
185 assert Pirate
.reflect_on_association(:ship).options
[:autosave]
189 class TestNestedAttributesOnABelongsToAssociation
< ActiveRecord
::TestCase
191 @ship = Ship
.new(:name => 'Nights Dirty Lightning')
192 @pirate = @ship.build_pirate(:catchphrase => 'Aye')
196 def test_should_define_an_attribute_writer_method_for_the_association
197 assert_respond_to
@ship, :pirate_attributes=
200 def test_should_build_a_new_record_if_there_is_no_id
202 @ship.reload
.pirate_attributes
= { :catchphrase => 'Arr' }
204 assert
@ship.pirate
.new_record
?
205 assert_equal
'Arr', @ship.pirate
.catchphrase
208 def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
210 @ship.reload
.pirate_attributes
= { :catchphrase => 'Arr', :_delete => '1' }
212 assert_nil
@ship.pirate
215 def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
217 @ship.reload
.pirate_attributes
= {}
219 assert_nil
@ship.pirate
222 def test_should_replace_an_existing_record_if_there_is_no_id
223 @ship.reload
.pirate_attributes
= { :catchphrase => 'Arr' }
225 assert
@ship.pirate
.new_record
?
226 assert_equal
'Arr', @ship.pirate
.catchphrase
227 assert_equal
'Aye', @pirate.catchphrase
230 def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
231 @ship.reload
.pirate_attributes
= { :catchphrase => 'Arr', :_delete => '1' }
233 assert_equal
@pirate, @ship.pirate
234 assert_equal
'Aye', @ship.pirate
.catchphrase
237 def test_should_modify_an_existing_record_if_there_is_a_matching_id
238 @ship.reload
.pirate_attributes
= { :id => @pirate.id
, :catchphrase => 'Arr' }
240 assert_equal
@pirate, @ship.pirate
241 assert_equal
'Arr', @ship.pirate
.catchphrase
244 def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
245 @ship.reload
.pirate_attributes
= { 'id' => @pirate.id
, 'catchphrase' => 'Arr' }
247 assert_equal
@pirate, @ship.pirate
248 assert_equal
'Arr', @ship.pirate
.catchphrase
251 def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
252 @pirate.stubs(:id).returns('ABC1X')
253 @ship.pirate_attributes
= { :id => @pirate.id
, :catchphrase => 'Arr' }
255 assert_equal
'Arr', @ship.pirate
.catchphrase
258 def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
260 [1, '1', true, 'true'].each
do |truth
|
261 @ship.reload
.create_pirate(:catchphrase => 'Arr')
262 assert_difference('Pirate.count', -1) do
263 @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate
.id
, :_delete => truth
})
268 def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
269 [nil, '0', 0, 'false', false].each
do |not_truth
|
270 assert_no_difference('Pirate.count') do
271 @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate
.id
, :_delete => not_truth
})
276 def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
277 Ship
.accepts_nested_attributes_for
:pirate, :allow_destroy => false, :reject_if => proc
{ |attributes
| attributes
.empty
? }
279 assert_no_difference('Pirate.count') do
280 @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate
.id
, :_delete => '1' })
283 Ship
.accepts_nested_attributes_for
:pirate, :allow_destroy => true, :reject_if => proc
{ |attributes
| attributes
.empty
? }
286 def test_should_work_with_update_attributes_as_well
287 @ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
290 assert_equal
'Mister Pablo', @ship.name
291 assert_equal
'Arr', @ship.pirate
.catchphrase
294 def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
295 assert_no_difference('Pirate.count') do
296 @ship.attributes
= { :pirate_attributes => { :id => @ship.pirate
.id
, '_delete' => true } }
298 assert_difference('Pirate.count', -1) { @ship.save
}
301 def test_should_automatically_enable_autosave_on_the_association
302 assert Ship
.reflect_on_association(:pirate).options
[:autosave]
306 module NestedAttributesOnACollectionAssociationTests
307 include AssertRaiseWithMessage
309 def test_should_define_an_attribute_writer_method_for_the_association
310 assert_respond_to
@pirate, association_setter
313 def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
314 @alternate_params[association_getter
].stringify_keys
!
315 @pirate.update_attributes
@alternate_params
316 assert_equal
['Grace OMalley', 'Privateers Greed'], [@child_1.reload
.name
, @child_2.reload
.name
]
319 def test_should_take_an_array_and_assign_the_attributes_to_the_associated_models
320 @pirate.send(association_setter
, @alternate_params[association_getter
].values
)
322 assert_equal
['Grace OMalley', 'Privateers Greed'], [@child_1.reload
.name
, @child_2.reload
.name
]
325 def test_should_also_work_with_a_HashWithIndifferentAccess
326 @pirate.send(association_setter
, HashWithIndifferentAccess
.new('foo' => HashWithIndifferentAccess
.new(:id => @child_1.id
, :name => 'Grace OMalley')))
328 assert_equal
'Grace OMalley', @child_1.reload
.name
331 def test_should_take_a_hash_and_assign_the_attributes_to_the_associated_models
332 @pirate.attributes
= @alternate_params
333 assert_equal
'Grace OMalley', @pirate.send(@association_name).first
.name
334 assert_equal
'Privateers Greed', @pirate.send(@association_name).last
.name
337 def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
338 @child_1.stubs(:id).returns('ABC1X')
339 @child_2.stubs(:id).returns('ABC2X')
341 @pirate.attributes
= {
342 association_getter
=> [
343 { :id => @child_1.id
, :name => 'Grace OMalley' },
344 { :id => @child_2.id
, :name => 'Privateers Greed' }
348 assert_equal
['Grace OMalley', 'Privateers Greed'], [@child_1.name
, @child_2.name
]
351 def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
352 @pirate.send(@association_name).destroy_all
353 @pirate.reload
.attributes
= {
354 association_getter
=> { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }}
357 assert
@pirate.send(@association_name).first
.new_record
?
358 assert_equal
'Grace OMalley', @pirate.send(@association_name).first
.name
360 assert
@pirate.send(@association_name).last
.new_record
?
361 assert_equal
'Privateers Greed', @pirate.send(@association_name).last
.name
364 def test_should_not_assign_delete_key_to_a_record
365 assert_nothing_raised ActiveRecord
::UnknownAttributeError do
366 @pirate.send(association_setter
, { 'foo' => { '_delete' => '0' }})
370 def test_should_ignore_new_associated_records_with_truthy_delete_attribute
371 @pirate.send(@association_name).destroy_all
372 @pirate.reload
.attributes
= {
373 association_getter
=> {
374 'foo' => { :name => 'Grace OMalley' },
375 'bar' => { :name => 'Privateers Greed', '_delete' => '1' }
379 assert_equal
1, @pirate.send(@association_name).length
380 assert_equal
'Grace OMalley', @pirate.send(@association_name).first
.name
383 def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
384 @alternate_params[association_getter
]['baz'] = {}
385 assert_no_difference("@pirate.send(@association_name).length") do
386 @pirate.attributes
= @alternate_params
390 def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
391 attributes
= ActiveSupport
::OrderedHash.new
392 attributes
['123726353'] = { :name => 'Grace OMalley' }
393 attributes
['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
394 @pirate.send(association_setter
, attributes
)
396 assert_equal
['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set
, @pirate.send(@association_name).map(&:name).to_set
399 def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
400 assert_nothing_raised(ArgumentError
) { @pirate.send(association_setter
, {}) }
401 assert_nothing_raised(ArgumentError
) { @pirate.send(association_setter
, ActiveSupport
::OrderedHash.new
) }
403 assert_raise_with_message ArgumentError
, 'Hash or Array expected, got String ("foo")' do
404 @pirate.send(association_setter
, "foo")
408 def test_should_work_with_update_attributes_as_well
409 @pirate.update_attributes(:catchphrase => 'Arr',
410 association_getter
=> { 'foo' => { :id => @child_1.id
, :name => 'Grace OMalley' }})
412 assert_equal
'Grace OMalley', @child_1.reload
.name
415 def test_should_update_existing_records_and_add_new_ones_that_have_no_id
416 @alternate_params[association_getter
]['baz'] = { :name => 'Buccaneers Servant' }
417 assert_difference('@pirate.send(@association_name).count', +1) do
418 @pirate.update_attributes
@alternate_params
420 assert_equal
['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set
, @pirate.reload
.send(@association_name).map(&:name).to_set
423 def test_should_be_possible_to_destroy_a_record
424 ['1', 1, 'true', true].each
do |true_variable
|
425 record
= @pirate.reload
.send(@association_name).create
!(:name => 'Grace OMalley')
426 @pirate.send(association_setter
,
427 @alternate_params[association_getter
].merge('baz' => { :id => record
.id
, '_delete' => true_variable
})
430 assert_difference('@pirate.send(@association_name).count', -1) do
436 def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
437 [nil, '', '0', 0, 'false', false].each
do |false_variable
|
438 @alternate_params[association_getter
]['foo']['_delete'] = false_variable
439 assert_no_difference('@pirate.send(@association_name).count') do
440 @pirate.update_attributes(@alternate_params)
445 def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
446 assert_no_difference('@pirate.send(@association_name).count') do
447 @pirate.send(association_setter
, @alternate_params[association_getter
].merge('baz' => { :id => @child_1.id
, '_delete' => true }))
449 assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save
}
452 def test_should_automatically_enable_autosave_on_the_association
453 assert Pirate
.reflect_on_association(@association_name).options
[:autosave]
458 def association_setter
459 @association_setter ||= "#{@association_name}_attributes=".to_sym
462 def association_getter
463 @association_getter ||= "#{@association_name}_attributes".to_sym
467 class TestNestedAttributesOnAHasManyAssociation
< ActiveRecord
::TestCase
469 @association_type = :has_many
470 @association_name = :birds
472 @pirate = Pirate
.create
!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
473 @pirate.birds
.create
!(:name => 'Posideons Killer')
474 @pirate.birds
.create
!(:name => 'Killer bandita Dionne')
476 @child_1, @child_2 = @pirate.birds
478 @alternate_params = {
479 :birds_attributes => {
480 'foo' => { :id => @child_1.id
, :name => 'Grace OMalley' },
481 'bar' => { :id => @child_2.id
, :name => 'Privateers Greed' }
486 include NestedAttributesOnACollectionAssociationTests
489 class TestNestedAttributesOnAHasAndBelongsToManyAssociation
< ActiveRecord
::TestCase
491 @association_type = :has_and_belongs_to_many
492 @association_name = :parrots
494 @pirate = Pirate
.create
!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
495 @pirate.parrots
.create
!(:name => 'Posideons Killer')
496 @pirate.parrots
.create
!(:name => 'Killer bandita Dionne')
498 @child_1, @child_2 = @pirate.parrots
500 @alternate_params = {
501 :parrots_attributes => {
502 'foo' => { :id => @child_1.id
, :name => 'Grace OMalley' },
503 'bar' => { :id => @child_2.id
, :name => 'Privateers Greed' }
508 include NestedAttributesOnACollectionAssociationTests