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_nested_attributes_options
30
assert_equal Hash.new, ActiveRecord::Base.nested_attributes_options
33
def test_should_add_a_proc_to_nested_attributes_options
34
[:parrots, :birds].each do |name|
35
assert_instance_of Proc, Pirate.nested_attributes_options[name][:reject_if]
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 => { '_destroy' => true })
56
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
57
ship = Ship.create!(:name => 'Nights Dirty Lightning')
59
ship.mark_for_destruction
63
def test_underscore_delete_is_deprecated
64
ActiveSupport::Deprecation.expects(:warn)
65
ship = Ship.create!(:name => 'Nights Dirty Lightning')
69
def test_reject_if_method_without_arguments
70
Pirate.accepts_nested_attributes_for :ship, :reject_if => :new_record?
72
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
73
pirate.ship_attributes = { :name => 'Black Pearl' }
74
assert_no_difference('Ship.count') { pirate.save! }
77
def test_reject_if_method_with_arguments
78
Pirate.accepts_nested_attributes_for :ship, :reject_if => :reject_empty_ships_on_create
80
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
81
pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
82
assert_no_difference('Ship.count') { pirate.save! }
84
# pirate.reject_empty_ships_on_create returns false for saved records
85
pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
86
assert_difference('Ship.count') { pirate.save! }
89
def test_reject_if_with_indifferent_keys
90
Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:name].blank? }
92
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
93
pirate.ship_attributes = { :name => 'Hello Pearl' }
94
assert_difference('Ship.count') { pirate.save! }
98
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
99
include AssertRaiseWithMessage
102
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
103
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
106
def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to
107
assert_raise_with_message ArgumentError, "Cannot build association looter. Are you trying to build a polymorphic one-to-one association?" do
108
Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"})
112
def test_should_define_an_attribute_writer_method_for_the_association
113
assert_respond_to @pirate, :ship_attributes=
116
def test_should_build_a_new_record_if_there_is_no_id
118
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
120
assert @pirate.ship.new_record?
121
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
124
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
126
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
128
assert_nil @pirate.ship
131
def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
133
@pirate.reload.ship_attributes = {}
135
assert_nil @pirate.ship
138
def test_should_replace_an_existing_record_if_there_is_no_id
139
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
141
assert @pirate.ship.new_record?
142
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
143
assert_equal 'Nights Dirty Lightning', @ship.name
146
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
147
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
149
assert_equal @ship, @pirate.ship
150
assert_equal 'Nights Dirty Lightning', @pirate.ship.name
153
def test_should_modify_an_existing_record_if_there_is_a_matching_id
154
@pirate.reload.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
156
assert_equal @ship, @pirate.ship
157
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
160
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
161
@pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' }
163
assert_equal @ship, @pirate.ship
164
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
167
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
168
@ship.stubs(:id).returns('ABC1X')
169
@pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
171
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
174
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
176
[1, '1', true, 'true'].each do |truth|
177
@pirate.reload.create_ship(:name => 'Mister Pablo')
178
assert_difference('Ship.count', -1) do
179
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => truth })
184
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
185
[nil, '0', 0, 'false', false].each do |not_truth|
186
assert_no_difference('Ship.count') do
187
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => not_truth })
192
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
193
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
195
assert_no_difference('Ship.count') do
196
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => '1' })
199
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
202
def test_should_also_work_with_a_HashWithIndifferentAccess
203
@pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
205
assert !@pirate.ship.new_record?
206
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
209
def test_should_work_with_update_attributes_as_well
210
@pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id, :name => 'Mister Pablo' } })
213
assert_equal 'Arr', @pirate.catchphrase
214
assert_equal 'Mister Pablo', @pirate.ship.name
217
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
218
assert_no_difference('Ship.count') do
219
@pirate.attributes = { :ship_attributes => { :id => @ship.id, :_destroy => '1' } }
221
assert_difference('Ship.count', -1) do
226
def test_should_automatically_enable_autosave_on_the_association
227
assert Pirate.reflect_on_association(:ship).options[:autosave]
231
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
233
@ship = Ship.new(:name => 'Nights Dirty Lightning')
234
@pirate = @ship.build_pirate(:catchphrase => 'Aye')
238
def test_should_define_an_attribute_writer_method_for_the_association
239
assert_respond_to @ship, :pirate_attributes=
242
def test_should_build_a_new_record_if_there_is_no_id
244
@ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
246
assert @ship.pirate.new_record?
247
assert_equal 'Arr', @ship.pirate.catchphrase
250
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
252
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
254
assert_nil @ship.pirate
257
def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
259
@ship.reload.pirate_attributes = {}
261
assert_nil @ship.pirate
264
def test_should_replace_an_existing_record_if_there_is_no_id
265
@ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
267
assert @ship.pirate.new_record?
268
assert_equal 'Arr', @ship.pirate.catchphrase
269
assert_equal 'Aye', @pirate.catchphrase
272
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
273
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
275
assert_equal @pirate, @ship.pirate
276
assert_equal 'Aye', @ship.pirate.catchphrase
279
def test_should_modify_an_existing_record_if_there_is_a_matching_id
280
@ship.reload.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
282
assert_equal @pirate, @ship.pirate
283
assert_equal 'Arr', @ship.pirate.catchphrase
286
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
287
@ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' }
289
assert_equal @pirate, @ship.pirate
290
assert_equal 'Arr', @ship.pirate.catchphrase
293
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
294
@pirate.stubs(:id).returns('ABC1X')
295
@ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
297
assert_equal 'Arr', @ship.pirate.catchphrase
300
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
302
[1, '1', true, 'true'].each do |truth|
303
@ship.reload.create_pirate(:catchphrase => 'Arr')
304
assert_difference('Pirate.count', -1) do
305
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => truth })
310
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
311
[nil, '0', 0, 'false', false].each do |not_truth|
312
assert_no_difference('Pirate.count') do
313
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => not_truth })
318
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
319
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
321
assert_no_difference('Pirate.count') do
322
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => '1' })
325
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
328
def test_should_work_with_update_attributes_as_well
329
@ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
332
assert_equal 'Mister Pablo', @ship.name
333
assert_equal 'Arr', @ship.pirate.catchphrase
336
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
337
assert_no_difference('Pirate.count') do
338
@ship.attributes = { :pirate_attributes => { :id => @ship.pirate.id, '_destroy' => true } }
340
assert_difference('Pirate.count', -1) { @ship.save }
343
def test_should_automatically_enable_autosave_on_the_association
344
assert Ship.reflect_on_association(:pirate).options[:autosave]
348
module NestedAttributesOnACollectionAssociationTests
349
include AssertRaiseWithMessage
351
def test_should_define_an_attribute_writer_method_for_the_association
352
assert_respond_to @pirate, association_setter
355
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
356
@alternate_params[association_getter].stringify_keys!
357
@pirate.update_attributes @alternate_params
358
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
361
def test_should_take_an_array_and_assign_the_attributes_to_the_associated_models
362
@pirate.send(association_setter, @alternate_params[association_getter].values)
364
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
367
def test_should_also_work_with_a_HashWithIndifferentAccess
368
@pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
370
assert_equal 'Grace OMalley', @child_1.reload.name
373
def test_should_take_a_hash_and_assign_the_attributes_to_the_associated_models
374
@pirate.attributes = @alternate_params
375
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
376
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
379
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
380
@child_1.stubs(:id).returns('ABC1X')
381
@child_2.stubs(:id).returns('ABC2X')
383
@pirate.attributes = {
384
association_getter => [
385
{ :id => @child_1.id, :name => 'Grace OMalley' },
386
{ :id => @child_2.id, :name => 'Privateers Greed' }
390
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
393
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
394
@pirate.send(@association_name).destroy_all
395
@pirate.reload.attributes = {
396
association_getter => { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }}
399
assert @pirate.send(@association_name).first.new_record?
400
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
402
assert @pirate.send(@association_name).last.new_record?
403
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
406
def test_should_not_assign_destroy_key_to_a_record
407
assert_nothing_raised ActiveRecord::UnknownAttributeError do
408
@pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
412
def test_should_ignore_new_associated_records_with_truthy_destroy_attribute
413
@pirate.send(@association_name).destroy_all
414
@pirate.reload.attributes = {
415
association_getter => {
416
'foo' => { :name => 'Grace OMalley' },
417
'bar' => { :name => 'Privateers Greed', '_destroy' => '1' }
421
assert_equal 1, @pirate.send(@association_name).length
422
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
425
def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
426
@alternate_params[association_getter]['baz'] = {}
427
assert_no_difference("@pirate.send(@association_name).length") do
428
@pirate.attributes = @alternate_params
432
def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
433
attributes = ActiveSupport::OrderedHash.new
434
attributes['123726353'] = { :name => 'Grace OMalley' }
435
attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
436
@pirate.send(association_setter, attributes)
438
assert_equal ['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set, @pirate.send(@association_name).map(&:name).to_set
441
def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
442
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
443
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) }
445
assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do
446
@pirate.send(association_setter, "foo")
450
def test_should_work_with_update_attributes_as_well
451
@pirate.update_attributes(:catchphrase => 'Arr',
452
association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
454
assert_equal 'Grace OMalley', @child_1.reload.name
457
def test_should_update_existing_records_and_add_new_ones_that_have_no_id
458
@alternate_params[association_getter]['baz'] = { :name => 'Buccaneers Servant' }
459
assert_difference('@pirate.send(@association_name).count', +1) do
460
@pirate.update_attributes @alternate_params
462
assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
465
def test_should_be_possible_to_destroy_a_record
466
['1', 1, 'true', true].each do |true_variable|
467
record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
468
@pirate.send(association_setter,
469
@alternate_params[association_getter].merge('baz' => { :id => record.id, '_destroy' => true_variable })
472
assert_difference('@pirate.send(@association_name).count', -1) do
478
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
479
[nil, '', '0', 0, 'false', false].each do |false_variable|
480
@alternate_params[association_getter]['foo']['_destroy'] = false_variable
481
assert_no_difference('@pirate.send(@association_name).count') do
482
@pirate.update_attributes(@alternate_params)
487
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
488
assert_no_difference('@pirate.send(@association_name).count') do
489
@pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_destroy' => true }))
491
assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
494
def test_should_automatically_enable_autosave_on_the_association
495
assert Pirate.reflect_on_association(@association_name).options[:autosave]
500
def association_setter
501
@association_setter ||= "#{@association_name}_attributes=".to_sym
504
def association_getter
505
@association_getter ||= "#{@association_name}_attributes".to_sym
509
class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
511
@association_type = :has_many
512
@association_name = :birds
514
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
515
@pirate.birds.create!(:name => 'Posideons Killer')
516
@pirate.birds.create!(:name => 'Killer bandita Dionne')
518
@child_1, @child_2 = @pirate.birds
520
@alternate_params = {
521
:birds_attributes => {
522
'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
523
'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
528
include NestedAttributesOnACollectionAssociationTests
531
class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
533
@association_type = :has_and_belongs_to_many
534
@association_name = :parrots
536
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
537
@pirate.parrots.create!(:name => 'Posideons Killer')
538
@pirate.parrots.create!(:name => 'Killer bandita Dionne')
540
@child_1, @child_2 = @pirate.parrots
542
@alternate_params = {
543
:parrots_attributes => {
544
'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
545
'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
550
include NestedAttributesOnACollectionAssociationTests
553
class TestNestedAttributesLimit < ActiveRecord::TestCase
555
Pirate.accepts_nested_attributes_for :parrots, :limit => 2
557
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
561
Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
564
def test_limit_with_less_records
565
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Big Big Love' } } }
566
assert_difference('Parrot.count') { @pirate.save! }
569
def test_limit_with_number_exact_records
570
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' }, 'bar' => { :name => 'Blown Away' } } }
571
assert_difference('Parrot.count', 2) { @pirate.save! }
574
def test_limit_with_exceeding_records
575
assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do
576
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' },
577
'bar' => { :name => 'Blown Away' },
578
'car' => { :name => 'The Happening' }} }