activerecord 2.2.3 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (120) hide show
  1. data/CHANGELOG +438 -396
  2. data/Rakefile +4 -2
  3. data/lib/active_record.rb +46 -43
  4. data/lib/active_record/association_preload.rb +34 -19
  5. data/lib/active_record/associations.rb +193 -251
  6. data/lib/active_record/associations/association_collection.rb +38 -21
  7. data/lib/active_record/associations/association_proxy.rb +11 -4
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
  9. data/lib/active_record/associations/has_many_association.rb +2 -2
  10. data/lib/active_record/associations/has_many_through_association.rb +8 -8
  11. data/lib/active_record/associations/has_one_association.rb +11 -2
  12. data/lib/active_record/attribute_methods.rb +1 -0
  13. data/lib/active_record/autosave_association.rb +349 -0
  14. data/lib/active_record/base.rb +292 -106
  15. data/lib/active_record/batches.rb +73 -0
  16. data/lib/active_record/calculations.rb +34 -16
  17. data/lib/active_record/callbacks.rb +37 -8
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +16 -0
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -0
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +103 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -6
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +28 -25
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -5
  24. data/lib/active_record/connection_adapters/mysql_adapter.rb +50 -21
  25. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -41
  26. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  27. data/lib/active_record/connection_adapters/sqlite_adapter.rb +41 -21
  28. data/lib/active_record/dirty.rb +1 -1
  29. data/lib/active_record/dynamic_scope_match.rb +25 -0
  30. data/lib/active_record/fixtures.rb +193 -198
  31. data/lib/active_record/locale/en.yml +1 -1
  32. data/lib/active_record/locking/optimistic.rb +33 -0
  33. data/lib/active_record/migration.rb +8 -2
  34. data/lib/active_record/named_scope.rb +13 -6
  35. data/lib/active_record/nested_attributes.rb +329 -0
  36. data/lib/active_record/query_cache.rb +25 -13
  37. data/lib/active_record/reflection.rb +6 -1
  38. data/lib/active_record/schema_dumper.rb +2 -0
  39. data/lib/active_record/serialization.rb +3 -1
  40. data/lib/active_record/serializers/json_serializer.rb +19 -0
  41. data/lib/active_record/serializers/xml_serializer.rb +28 -13
  42. data/lib/active_record/session_store.rb +318 -0
  43. data/lib/active_record/test_case.rb +15 -9
  44. data/lib/active_record/timestamp.rb +2 -2
  45. data/lib/active_record/transactions.rb +58 -8
  46. data/lib/active_record/validations.rb +29 -24
  47. data/lib/active_record/version.rb +2 -2
  48. data/test/cases/ar_schema_test.rb +0 -1
  49. data/test/cases/associations/belongs_to_associations_test.rb +35 -131
  50. data/test/cases/associations/cascaded_eager_loading_test.rb +8 -0
  51. data/test/cases/associations/eager_load_nested_include_test.rb +29 -0
  52. data/test/cases/associations/eager_test.rb +137 -7
  53. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +45 -7
  54. data/test/cases/associations/has_many_associations_test.rb +110 -149
  55. data/test/cases/associations/has_many_through_associations_test.rb +39 -7
  56. data/test/cases/associations/has_one_associations_test.rb +39 -92
  57. data/test/cases/associations/has_one_through_associations_test.rb +34 -3
  58. data/test/cases/associations/inner_join_association_test.rb +0 -5
  59. data/test/cases/associations/join_model_test.rb +5 -7
  60. data/test/cases/attribute_methods_test.rb +13 -1
  61. data/test/cases/autosave_association_test.rb +901 -0
  62. data/test/cases/base_test.rb +41 -21
  63. data/test/cases/batches_test.rb +61 -0
  64. data/test/cases/calculations_test.rb +37 -17
  65. data/test/cases/callbacks_test.rb +43 -5
  66. data/test/cases/connection_pool_test.rb +25 -0
  67. data/test/cases/copy_table_test_sqlite.rb +11 -0
  68. data/test/cases/datatype_test_postgresql.rb +1 -0
  69. data/test/cases/defaults_test.rb +37 -26
  70. data/test/cases/dirty_test.rb +26 -2
  71. data/test/cases/finder_test.rb +79 -44
  72. data/test/cases/fixtures_test.rb +15 -19
  73. data/test/cases/helper.rb +26 -19
  74. data/test/cases/inheritance_test.rb +2 -2
  75. data/test/cases/json_serialization_test.rb +1 -1
  76. data/test/cases/locking_test.rb +23 -5
  77. data/test/cases/method_scoping_test.rb +126 -3
  78. data/test/cases/migration_test.rb +253 -237
  79. data/test/cases/named_scope_test.rb +73 -3
  80. data/test/cases/nested_attributes_test.rb +509 -0
  81. data/test/cases/query_cache_test.rb +0 -4
  82. data/test/cases/reflection_test.rb +13 -3
  83. data/test/cases/reload_models_test.rb +3 -1
  84. data/test/cases/repair_helper.rb +50 -0
  85. data/test/cases/schema_dumper_test.rb +0 -1
  86. data/test/cases/transactions_test.rb +177 -12
  87. data/test/cases/validations_i18n_test.rb +288 -294
  88. data/test/cases/validations_test.rb +230 -180
  89. data/test/cases/xml_serialization_test.rb +19 -1
  90. data/test/fixtures/fixture_database.sqlite3 +0 -0
  91. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  92. data/test/fixtures/member_types.yml +6 -0
  93. data/test/fixtures/members.yml +3 -1
  94. data/test/fixtures/people.yml +10 -1
  95. data/test/fixtures/toys.yml +4 -0
  96. data/test/models/author.rb +1 -2
  97. data/test/models/bird.rb +3 -0
  98. data/test/models/category.rb +1 -0
  99. data/test/models/company.rb +3 -0
  100. data/test/models/developer.rb +12 -0
  101. data/test/models/event.rb +3 -0
  102. data/test/models/member.rb +1 -0
  103. data/test/models/member_detail.rb +1 -0
  104. data/test/models/member_type.rb +3 -0
  105. data/test/models/owner.rb +2 -1
  106. data/test/models/parrot.rb +2 -0
  107. data/test/models/person.rb +6 -0
  108. data/test/models/pet.rb +2 -1
  109. data/test/models/pirate.rb +55 -1
  110. data/test/models/post.rb +6 -0
  111. data/test/models/project.rb +1 -0
  112. data/test/models/reply.rb +6 -0
  113. data/test/models/ship.rb +8 -1
  114. data/test/models/ship_part.rb +5 -0
  115. data/test/models/topic.rb +13 -1
  116. data/test/models/toy.rb +4 -0
  117. data/test/schema/schema.rb +35 -2
  118. metadata +70 -9
  119. data/test/fixtures/fixture_database.sqlite +0 -0
  120. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -1,11 +1,19 @@
1
1
  require "cases/helper"
2
2
  require 'models/post'
3
3
  require 'models/person'
4
+ require 'models/reference'
5
+ require 'models/job'
4
6
  require 'models/reader'
5
7
  require 'models/comment'
8
+ require 'models/tag'
9
+ require 'models/tagging'
10
+ require 'models/author'
11
+ require 'models/owner'
12
+ require 'models/pet'
13
+ require 'models/toy'
6
14
 
7
15
  class HasManyThroughAssociationsTest < ActiveRecord::TestCase
8
- fixtures :posts, :readers, :people, :comments, :authors
16
+ fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys
9
17
 
10
18
  def test_associate_existing
11
19
  assert_queries(2) { posts(:thinking);people(:david) }
@@ -84,6 +92,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
84
92
  assert posts(:welcome).reload.people(true).empty?
85
93
  end
86
94
 
95
+ def test_destroy_association
96
+ assert_difference "Person.count", -1 do
97
+ posts(:welcome).people.destroy(people(:michael))
98
+ end
99
+
100
+ assert posts(:welcome).reload.people.empty?
101
+ assert posts(:welcome).people(true).empty?
102
+ end
103
+
104
+ def test_destroy_all
105
+ assert_difference "Person.count", -1 do
106
+ posts(:welcome).people.destroy_all
107
+ end
108
+
109
+ assert posts(:welcome).reload.people.empty?
110
+ assert posts(:welcome).people(true).empty?
111
+ end
112
+
87
113
  def test_replace_association
88
114
  assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
89
115
 
@@ -201,6 +227,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
201
227
  assert_equal 2, people(:michael).posts.count(:include => :readers)
202
228
  end
203
229
 
230
+ def test_inner_join_with_quoted_table_name
231
+ assert_equal 2, people(:michael).jobs.size
232
+ end
233
+
204
234
  def test_get_ids
205
235
  assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
206
236
  end
@@ -221,12 +251,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
221
251
  assert !person.posts.loaded?
222
252
  end
223
253
 
224
- uses_mocha 'mocking Tag.transaction' do
225
- def test_association_proxy_transaction_method_starts_transaction_in_association_class
226
- Tag.expects(:transaction)
227
- Post.find(:first).tags.transaction do
228
- # nothing
229
- end
254
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
255
+ Tag.expects(:transaction)
256
+ Post.find(:first).tags.transaction do
257
+ # nothing
230
258
  end
231
259
  end
232
260
 
@@ -244,4 +272,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
244
272
  author.author_favorites.create(:favorite_author_id => 3)
245
273
  assert_equal post.author.author_favorites, post.author_favorites
246
274
  end
275
+
276
+ def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
277
+ assert_equal 1, owners(:blackbeard).toys.count
278
+ end
247
279
  end
@@ -59,8 +59,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
59
59
  end
60
60
 
61
61
  def test_type_mismatch
62
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
63
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
62
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
63
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
64
64
  end
65
65
 
66
66
  def test_natural_assignment
@@ -76,7 +76,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
76
76
  companies(:first_firm).save
77
77
  assert_nil companies(:first_firm).account
78
78
  # account is dependent, therefore is destroyed when reference to owner is lost
79
- assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
79
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
80
+ end
81
+
82
+ def test_nullification_on_association_change
83
+ firm = companies(:rails_core)
84
+ old_account_id = firm.account.id
85
+ firm.account = Account.new
86
+ # account is dependent with nullify, therefore its firm_id should be nil
87
+ assert_nil Account.find(old_account_id).firm_id
88
+ end
89
+
90
+ def test_association_changecalls_delete
91
+ companies(:first_firm).deletable_account = Account.new
92
+ assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
93
+ end
94
+
95
+ def test_association_change_calls_destroy
96
+ companies(:first_firm).account = Account.new
97
+ assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
80
98
  end
81
99
 
82
100
  def test_natural_assignment_to_already_associated_record
@@ -193,28 +211,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
193
211
  assert_equal account, firm.account
194
212
  end
195
213
 
196
- def test_build_before_child_saved
197
- firm = Firm.find(1)
198
-
199
- account = firm.account.build("credit_limit" => 1000)
200
- assert_equal account, firm.account
201
- assert account.new_record?
202
- assert firm.save
203
- assert_equal account, firm.account
204
- assert !account.new_record?
205
- end
206
-
207
- def test_build_before_either_saved
208
- firm = Firm.new("name" => "GlobalMegaCorp")
209
-
210
- firm.account = account = Account.new("credit_limit" => 1000)
211
- assert_equal account, firm.account
212
- assert account.new_record?
213
- assert firm.save
214
- assert_equal account, firm.account
215
- assert !account.new_record?
216
- end
217
-
218
214
  def test_failing_build_association
219
215
  firm = Firm.new("name" => "GlobalMegaCorp")
220
216
  firm.save
@@ -253,16 +249,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
253
249
  firm.destroy
254
250
  end
255
251
 
256
- def test_assignment_before_parent_saved
257
- firm = Firm.new("name" => "GlobalMegaCorp")
258
- firm.account = a = Account.find(1)
259
- assert firm.new_record?
260
- assert_equal a, firm.account
261
- assert firm.save
262
- assert_equal a, firm.account
263
- assert_equal a, firm.account(true)
264
- end
265
-
266
252
  def test_finding_with_interpolated_condition
267
253
  firm = Firm.find(:first)
268
254
  superior = firm.clients.create(:name => 'SuperiorCo')
@@ -279,61 +265,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
279
265
  assert_equal a, firm.account
280
266
  assert_equal a, firm.account(true)
281
267
  end
282
-
283
- def test_save_fails_for_invalid_has_one
284
- firm = Firm.find(:first)
285
- assert firm.valid?
286
-
287
- firm.account = Account.new
288
-
289
- assert !firm.account.valid?
290
- assert !firm.valid?
291
- assert !firm.save
292
- assert_equal "is invalid", firm.errors.on("account")
293
- end
294
-
295
-
296
- def test_save_succeeds_for_invalid_has_one_with_validate_false
297
- firm = Firm.find(:first)
298
- assert firm.valid?
299
-
300
- firm.unvalidated_account = Account.new
301
-
302
- assert !firm.unvalidated_account.valid?
303
- assert firm.valid?
304
- assert firm.save
305
- end
306
-
307
- def test_assignment_before_either_saved
308
- firm = Firm.new("name" => "GlobalMegaCorp")
309
- firm.account = a = Account.new("credit_limit" => 1000)
310
- assert firm.new_record?
311
- assert a.new_record?
312
- assert_equal a, firm.account
313
- assert firm.save
314
- assert !firm.new_record?
315
- assert !a.new_record?
316
- assert_equal a, firm.account
317
- assert_equal a, firm.account(true)
318
- end
319
-
320
- def test_not_resaved_when_unchanged
321
- firm = Firm.find(:first, :include => :account)
322
- firm.name += '-changed'
323
- assert_queries(1) { firm.save! }
324
-
325
- firm = Firm.find(:first)
326
- firm.account = Account.find(:first)
327
- assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
328
-
329
- firm = Firm.find(:first).clone
330
- firm.account = Account.find(:first)
331
- assert_queries(2) { firm.save! }
332
-
333
- firm = Firm.find(:first).clone
334
- firm.account = Account.find(:first).clone
335
- assert_queries(2) { firm.save! }
336
- end
337
268
 
338
269
  def test_save_still_works_after_accessing_nil_has_one
339
270
  jp = Company.new :name => 'Jaded Pixel'
@@ -350,8 +281,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
350
281
  end
351
282
 
352
283
  def test_has_one_proxy_should_not_respond_to_private_methods
353
- assert_raises(NoMethodError) { accounts(:signals37).private_method }
354
- assert_raises(NoMethodError) { companies(:first_firm).account.private_method }
284
+ assert_raise(NoMethodError) { accounts(:signals37).private_method }
285
+ assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
355
286
  end
356
287
 
357
288
  def test_has_one_proxy_should_respond_to_private_methods_via_send
@@ -359,4 +290,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
359
290
  companies(:first_firm).account.send(:private_method)
360
291
  end
361
292
 
293
+ def test_save_of_record_with_loaded_has_one
294
+ @firm = companies(:first_firm)
295
+ assert_not_nil @firm.account
296
+
297
+ assert_nothing_raised do
298
+ Firm.find(@firm.id).save!
299
+ Firm.find(@firm.id, :include => :account).save!
300
+ end
301
+
302
+ @firm.account.destroy
303
+
304
+ assert_nothing_raised do
305
+ Firm.find(@firm.id).save!
306
+ Firm.find(@firm.id, :include => :account).save!
307
+ end
308
+ end
362
309
  end
@@ -1,5 +1,6 @@
1
1
  require "cases/helper"
2
2
  require 'models/club'
3
+ require 'models/member_type'
3
4
  require 'models/member'
4
5
  require 'models/membership'
5
6
  require 'models/sponsor'
@@ -7,7 +8,7 @@ require 'models/organization'
7
8
  require 'models/member_detail'
8
9
 
9
10
  class HasOneThroughAssociationsTest < ActiveRecord::TestCase
10
- fixtures :members, :clubs, :memberships, :sponsors, :organizations
11
+ fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations
11
12
 
12
13
  def setup
13
14
  @member = members(:groucho)
@@ -114,8 +115,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
114
115
  end
115
116
 
116
117
  def test_has_one_through_proxy_should_not_respond_to_private_methods
117
- assert_raises(NoMethodError) { clubs(:moustache_club).private_method }
118
- assert_raises(NoMethodError) { @member.club.private_method }
118
+ assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
119
+ assert_raise(NoMethodError) { @member.club.private_method }
119
120
  end
120
121
 
121
122
  def test_has_one_through_proxy_should_respond_to_private_methods_via_send
@@ -158,4 +159,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
158
159
  assert @new_organization.members.include?(@member)
159
160
  end
160
161
 
162
+ def test_preloading_has_one_through_on_belongs_to
163
+ assert_not_nil @member.member_type
164
+ @organization = organizations(:nsa)
165
+ @member_detail = MemberDetail.new
166
+ @member.member_detail = @member_detail
167
+ @member.organization = @organization
168
+ @member_details = assert_queries(3) do
169
+ MemberDetail.find(:all, :include => :member_type)
170
+ end
171
+ @new_detail = @member_details[0]
172
+ assert @new_detail.loaded_member_type?
173
+ assert_not_nil assert_no_queries { @new_detail.member_type }
174
+ end
175
+
176
+ def test_save_of_record_with_loaded_has_one_through
177
+ @club = @member.club
178
+ assert_not_nil @club.sponsored_member
179
+
180
+ assert_nothing_raised do
181
+ Club.find(@club.id).save!
182
+ Club.find(@club.id, :include => :sponsored_member).save!
183
+ end
184
+
185
+ @club.sponsor.destroy
186
+
187
+ assert_nothing_raised do
188
+ Club.find(@club.id).save!
189
+ Club.find(@club.id, :include => :sponsored_member).save!
190
+ end
191
+ end
161
192
  end
@@ -29,11 +29,6 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
29
29
  assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?.*TERMINATING_MARKER/, sql
30
30
  end
31
31
 
32
- def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
33
- result = Author.find(:all, :joins => [:thinking_posts, :welcome_posts])
34
- assert_equal authors(:david), result.first
35
- end
36
-
37
32
  def test_construct_finder_sql_unpacks_nested_joins
38
33
  sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
39
34
  assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
@@ -510,13 +510,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
510
510
  assert !author.comments.loaded?
511
511
  end
512
512
 
513
- uses_mocha('has_many_through_collection_size_uses_counter_cache_if_it_exists') do
514
- def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
515
- author = authors(:david)
516
- author.stubs(:read_attribute).with('comments_count').returns(100)
517
- assert_equal 100, author.comments.size
518
- assert !author.comments.loaded?
519
- end
513
+ def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
514
+ author = authors(:david)
515
+ author.stubs(:read_attribute).with('comments_count').returns(100)
516
+ assert_equal 100, author.comments.size
517
+ assert !author.comments.loaded?
520
518
  end
521
519
 
522
520
  def test_adding_junk_to_has_many_through_should_raise_type_mismatch
@@ -56,6 +56,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase
56
56
  assert_equal myobj, topic.content
57
57
  end
58
58
 
59
+ def test_typecast_attribute_from_select_to_false
60
+ topic = Topic.create(:title => 'Budget')
61
+ topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test")
62
+ assert !topic.is_test?
63
+ end
64
+
65
+ def test_typecast_attribute_from_select_to_true
66
+ topic = Topic.create(:title => 'Budget')
67
+ topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test")
68
+ assert topic.is_test?
69
+ end
70
+
59
71
  def test_kernel_methods_not_implemented_in_activerecord
60
72
  %w(test name display y).each do |method|
61
73
  assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
@@ -88,7 +100,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
88
100
  %w(save create_or_update).each do |method|
89
101
  klass = Class.new ActiveRecord::Base
90
102
  klass.class_eval "def #{method}() 'defined #{method}' end"
91
- assert_raises ActiveRecord::DangerousAttributeError do
103
+ assert_raise ActiveRecord::DangerousAttributeError do
92
104
  klass.instance_method_already_implemented?(method)
93
105
  end
94
106
  end
@@ -0,0 +1,901 @@
1
+ require 'cases/helper'
2
+ require 'models/bird'
3
+ require 'models/company'
4
+ require 'models/customer'
5
+ require 'models/developer'
6
+ require 'models/order'
7
+ require 'models/parrot'
8
+ require 'models/person'
9
+ require 'models/pirate'
10
+ require 'models/post'
11
+ require 'models/reader'
12
+ require 'models/ship'
13
+ require 'models/ship_part'
14
+ require 'models/treasure'
15
+
16
+ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
17
+ def test_autosave_should_be_a_valid_option_for_has_one
18
+ assert base.valid_keys_for_has_one_association.include?(:autosave)
19
+ end
20
+
21
+ def test_autosave_should_be_a_valid_option_for_belongs_to
22
+ assert base.valid_keys_for_belongs_to_association.include?(:autosave)
23
+ end
24
+
25
+ def test_autosave_should_be_a_valid_option_for_has_many
26
+ assert base.valid_keys_for_has_many_association.include?(:autosave)
27
+ end
28
+
29
+ def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many
30
+ assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
31
+ end
32
+
33
+ private
34
+
35
+ def base
36
+ ActiveRecord::Base
37
+ end
38
+ end
39
+
40
+ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
41
+ def test_save_fails_for_invalid_has_one
42
+ firm = Firm.find(:first)
43
+ assert firm.valid?
44
+
45
+ firm.account = Account.new
46
+
47
+ assert !firm.account.valid?
48
+ assert !firm.valid?
49
+ assert !firm.save
50
+ assert_equal "is invalid", firm.errors.on("account")
51
+ end
52
+
53
+ def test_save_succeeds_for_invalid_has_one_with_validate_false
54
+ firm = Firm.find(:first)
55
+ assert firm.valid?
56
+
57
+ firm.unvalidated_account = Account.new
58
+
59
+ assert !firm.unvalidated_account.valid?
60
+ assert firm.valid?
61
+ assert firm.save
62
+ end
63
+
64
+ def test_build_before_child_saved
65
+ firm = Firm.find(1)
66
+
67
+ account = firm.account.build("credit_limit" => 1000)
68
+ assert_equal account, firm.account
69
+ assert account.new_record?
70
+ assert firm.save
71
+ assert_equal account, firm.account
72
+ assert !account.new_record?
73
+ end
74
+
75
+ def test_build_before_either_saved
76
+ firm = Firm.new("name" => "GlobalMegaCorp")
77
+
78
+ firm.account = account = Account.new("credit_limit" => 1000)
79
+ assert_equal account, firm.account
80
+ assert account.new_record?
81
+ assert firm.save
82
+ assert_equal account, firm.account
83
+ assert !account.new_record?
84
+ end
85
+
86
+ def test_assignment_before_parent_saved
87
+ firm = Firm.new("name" => "GlobalMegaCorp")
88
+ firm.account = a = Account.find(1)
89
+ assert firm.new_record?
90
+ assert_equal a, firm.account
91
+ assert firm.save
92
+ assert_equal a, firm.account
93
+ assert_equal a, firm.account(true)
94
+ end
95
+
96
+ def test_assignment_before_either_saved
97
+ firm = Firm.new("name" => "GlobalMegaCorp")
98
+ firm.account = a = Account.new("credit_limit" => 1000)
99
+ assert firm.new_record?
100
+ assert a.new_record?
101
+ assert_equal a, firm.account
102
+ assert firm.save
103
+ assert !firm.new_record?
104
+ assert !a.new_record?
105
+ assert_equal a, firm.account
106
+ assert_equal a, firm.account(true)
107
+ end
108
+
109
+ def test_not_resaved_when_unchanged
110
+ firm = Firm.find(:first, :include => :account)
111
+ firm.name += '-changed'
112
+ assert_queries(1) { firm.save! }
113
+
114
+ firm = Firm.find(:first)
115
+ firm.account = Account.find(:first)
116
+ assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
117
+
118
+ firm = Firm.find(:first).clone
119
+ firm.account = Account.find(:first)
120
+ assert_queries(2) { firm.save! }
121
+
122
+ firm = Firm.find(:first).clone
123
+ firm.account = Account.find(:first).clone
124
+ assert_queries(2) { firm.save! }
125
+ end
126
+ end
127
+
128
+ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
129
+ def test_save_fails_for_invalid_belongs_to
130
+ assert log = AuditLog.create(:developer_id => 0, :message => "")
131
+
132
+ log.developer = Developer.new
133
+ assert !log.developer.valid?
134
+ assert !log.valid?
135
+ assert !log.save
136
+ assert_equal "is invalid", log.errors.on("developer")
137
+ end
138
+
139
+ def test_save_succeeds_for_invalid_belongs_to_with_validate_false
140
+ assert log = AuditLog.create(:developer_id => 0, :message=> "")
141
+
142
+ log.unvalidated_developer = Developer.new
143
+ assert !log.unvalidated_developer.valid?
144
+ assert log.valid?
145
+ assert log.save
146
+ end
147
+
148
+ def test_assignment_before_parent_saved
149
+ client = Client.find(:first)
150
+ apple = Firm.new("name" => "Apple")
151
+ client.firm = apple
152
+ assert_equal apple, client.firm
153
+ assert apple.new_record?
154
+ assert client.save
155
+ assert apple.save
156
+ assert !apple.new_record?
157
+ assert_equal apple, client.firm
158
+ assert_equal apple, client.firm(true)
159
+ end
160
+
161
+ def test_assignment_before_either_saved
162
+ final_cut = Client.new("name" => "Final Cut")
163
+ apple = Firm.new("name" => "Apple")
164
+ final_cut.firm = apple
165
+ assert final_cut.new_record?
166
+ assert apple.new_record?
167
+ assert final_cut.save
168
+ assert !final_cut.new_record?
169
+ assert !apple.new_record?
170
+ assert_equal apple, final_cut.firm
171
+ assert_equal apple, final_cut.firm(true)
172
+ end
173
+
174
+ def test_store_two_association_with_one_save
175
+ num_orders = Order.count
176
+ num_customers = Customer.count
177
+ order = Order.new
178
+
179
+ customer1 = order.billing = Customer.new
180
+ customer2 = order.shipping = Customer.new
181
+ assert order.save
182
+ assert_equal customer1, order.billing
183
+ assert_equal customer2, order.shipping
184
+
185
+ order.reload
186
+
187
+ assert_equal customer1, order.billing
188
+ assert_equal customer2, order.shipping
189
+
190
+ assert_equal num_orders +1, Order.count
191
+ assert_equal num_customers +2, Customer.count
192
+ end
193
+
194
+ def test_store_association_in_two_relations_with_one_save
195
+ num_orders = Order.count
196
+ num_customers = Customer.count
197
+ order = Order.new
198
+
199
+ customer = order.billing = order.shipping = Customer.new
200
+ assert order.save
201
+ assert_equal customer, order.billing
202
+ assert_equal customer, order.shipping
203
+
204
+ order.reload
205
+
206
+ assert_equal customer, order.billing
207
+ assert_equal customer, order.shipping
208
+
209
+ assert_equal num_orders +1, Order.count
210
+ assert_equal num_customers +1, Customer.count
211
+ end
212
+
213
+ def test_store_association_in_two_relations_with_one_save_in_existing_object
214
+ num_orders = Order.count
215
+ num_customers = Customer.count
216
+ order = Order.create
217
+
218
+ customer = order.billing = order.shipping = Customer.new
219
+ assert order.save
220
+ assert_equal customer, order.billing
221
+ assert_equal customer, order.shipping
222
+
223
+ order.reload
224
+
225
+ assert_equal customer, order.billing
226
+ assert_equal customer, order.shipping
227
+
228
+ assert_equal num_orders +1, Order.count
229
+ assert_equal num_customers +1, Customer.count
230
+ end
231
+
232
+ def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
233
+ num_orders = Order.count
234
+ num_customers = Customer.count
235
+ order = Order.create
236
+
237
+ customer = order.billing = order.shipping = Customer.new
238
+ assert order.save
239
+ assert_equal customer, order.billing
240
+ assert_equal customer, order.shipping
241
+
242
+ order.reload
243
+
244
+ customer = order.billing = order.shipping = Customer.new
245
+
246
+ assert order.save
247
+ order.reload
248
+
249
+ assert_equal customer, order.billing
250
+ assert_equal customer, order.shipping
251
+
252
+ assert_equal num_orders +1, Order.count
253
+ assert_equal num_customers +2, Customer.count
254
+ end
255
+ end
256
+
257
+ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
258
+ fixtures :companies, :people
259
+
260
+ def test_invalid_adding
261
+ firm = Firm.find(1)
262
+ assert !(firm.clients_of_firm << c = Client.new)
263
+ assert c.new_record?
264
+ assert !firm.valid?
265
+ assert !firm.save
266
+ assert c.new_record?
267
+ end
268
+
269
+ def test_invalid_adding_before_save
270
+ no_of_firms = Firm.count
271
+ no_of_clients = Client.count
272
+ new_firm = Firm.new("name" => "A New Firm, Inc")
273
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
274
+ assert c.new_record?
275
+ assert !c.valid?
276
+ assert !new_firm.valid?
277
+ assert !new_firm.save
278
+ assert c.new_record?
279
+ assert new_firm.new_record?
280
+ end
281
+
282
+ def test_invalid_adding_with_validate_false
283
+ firm = Firm.find(:first)
284
+ client = Client.new
285
+ firm.unvalidated_clients_of_firm << client
286
+
287
+ assert firm.valid?
288
+ assert !client.valid?
289
+ assert firm.save
290
+ assert client.new_record?
291
+ end
292
+
293
+ def test_valid_adding_with_validate_false
294
+ no_of_clients = Client.count
295
+
296
+ firm = Firm.find(:first)
297
+ client = Client.new("name" => "Apple")
298
+
299
+ assert firm.valid?
300
+ assert client.valid?
301
+ assert client.new_record?
302
+
303
+ firm.unvalidated_clients_of_firm << client
304
+
305
+ assert firm.save
306
+ assert !client.new_record?
307
+ assert_equal no_of_clients+1, Client.count
308
+ end
309
+
310
+ def test_invalid_build
311
+ new_client = companies(:first_firm).clients_of_firm.build
312
+ assert new_client.new_record?
313
+ assert !new_client.valid?
314
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
315
+ assert !companies(:first_firm).save
316
+ assert new_client.new_record?
317
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
318
+ end
319
+
320
+ def test_adding_before_save
321
+ no_of_firms = Firm.count
322
+ no_of_clients = Client.count
323
+
324
+ new_firm = Firm.new("name" => "A New Firm, Inc")
325
+ c = Client.new("name" => "Apple")
326
+
327
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
328
+ assert_equal 1, new_firm.clients_of_firm.size
329
+ new_firm.clients_of_firm << c
330
+ assert_equal 2, new_firm.clients_of_firm.size
331
+
332
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
333
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
334
+ assert new_firm.save
335
+ assert !new_firm.new_record?
336
+ assert !c.new_record?
337
+ assert_equal new_firm, c.firm
338
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
339
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
340
+
341
+ assert_equal 2, new_firm.clients_of_firm.size
342
+ assert_equal 2, new_firm.clients_of_firm(true).size
343
+ end
344
+
345
+ def test_assign_ids
346
+ firm = Firm.new("name" => "Apple")
347
+ firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
348
+ firm.save
349
+ firm.reload
350
+ assert_equal 2, firm.clients.length
351
+ assert firm.clients.include?(companies(:second_client))
352
+ end
353
+
354
+ def test_assign_ids_for_through_a_belongs_to
355
+ post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
356
+ post.person_ids = [people(:david).id, people(:michael).id]
357
+ post.save
358
+ post.reload
359
+ assert_equal 2, post.people.length
360
+ assert post.people.include?(people(:david))
361
+ end
362
+
363
+ def test_build_before_save
364
+ company = companies(:first_firm)
365
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
366
+ assert !company.clients_of_firm.loaded?
367
+
368
+ company.name += '-changed'
369
+ assert_queries(2) { assert company.save }
370
+ assert !new_client.new_record?
371
+ assert_equal 2, company.clients_of_firm(true).size
372
+ end
373
+
374
+ def test_build_many_before_save
375
+ company = companies(:first_firm)
376
+ new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
377
+
378
+ company.name += '-changed'
379
+ assert_queries(3) { assert company.save }
380
+ assert_equal 3, company.clients_of_firm(true).size
381
+ end
382
+
383
+ def test_build_via_block_before_save
384
+ company = companies(:first_firm)
385
+ new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
386
+ assert !company.clients_of_firm.loaded?
387
+
388
+ company.name += '-changed'
389
+ assert_queries(2) { assert company.save }
390
+ assert !new_client.new_record?
391
+ assert_equal 2, company.clients_of_firm(true).size
392
+ end
393
+
394
+ def test_build_many_via_block_before_save
395
+ company = companies(:first_firm)
396
+ new_clients = assert_no_queries do
397
+ company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
398
+ client.name = "changed"
399
+ end
400
+ end
401
+
402
+ company.name += '-changed'
403
+ assert_queries(3) { assert company.save }
404
+ assert_equal 3, company.clients_of_firm(true).size
405
+ end
406
+
407
+ def test_replace_on_new_object
408
+ firm = Firm.new("name" => "New Firm")
409
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
410
+ assert firm.save
411
+ firm.reload
412
+ assert_equal 2, firm.clients.length
413
+ assert firm.clients.include?(Client.find_by_name("New Client"))
414
+ end
415
+ end
416
+
417
+ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
418
+ self.use_transactional_fixtures = false
419
+
420
+ def setup
421
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
422
+ @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
423
+ end
424
+
425
+ # reload
426
+ def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
427
+ @pirate.mark_for_destruction
428
+ @pirate.ship.mark_for_destruction
429
+
430
+ assert !@pirate.reload.marked_for_destruction?
431
+ assert !@pirate.ship.marked_for_destruction?
432
+ end
433
+
434
+ # has_one
435
+ def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
436
+ assert !@pirate.ship.marked_for_destruction?
437
+
438
+ @pirate.ship.mark_for_destruction
439
+ id = @pirate.ship.id
440
+
441
+ assert @pirate.ship.marked_for_destruction?
442
+ assert Ship.find_by_id(id)
443
+
444
+ @pirate.save
445
+ assert_nil @pirate.reload.ship
446
+ assert_nil Ship.find_by_id(id)
447
+ end
448
+
449
+ def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
450
+ @pirate.ship.name = ''
451
+ assert !@pirate.valid?
452
+
453
+ @pirate.ship.mark_for_destruction
454
+ assert_difference('Ship.count', -1) { @pirate.save! }
455
+ end
456
+
457
+ def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
458
+ # Stub the save method of the @pirate.ship instance to destroy and then raise an exception
459
+ class << @pirate.ship
460
+ def save(*args)
461
+ super
462
+ destroy
463
+ raise 'Oh noes!'
464
+ end
465
+ end
466
+
467
+ assert_raise(RuntimeError) { assert !@pirate.save }
468
+ assert_not_nil @pirate.reload.ship
469
+ end
470
+
471
+ # belongs_to
472
+ def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
473
+ assert !@ship.pirate.marked_for_destruction?
474
+
475
+ @ship.pirate.mark_for_destruction
476
+ id = @ship.pirate.id
477
+
478
+ assert @ship.pirate.marked_for_destruction?
479
+ assert Pirate.find_by_id(id)
480
+
481
+ @ship.save
482
+ assert_nil @ship.reload.pirate
483
+ assert_nil Pirate.find_by_id(id)
484
+ end
485
+
486
+ def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
487
+ @ship.pirate.catchphrase = ''
488
+ assert !@ship.valid?
489
+
490
+ @ship.pirate.mark_for_destruction
491
+ assert_difference('Pirate.count', -1) { @ship.save! }
492
+ end
493
+
494
+ def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
495
+ # Stub the save method of the @ship.pirate instance to destroy and then raise an exception
496
+ class << @ship.pirate
497
+ def save(*args)
498
+ super
499
+ destroy
500
+ raise 'Oh noes!'
501
+ end
502
+ end
503
+
504
+ assert_raise(RuntimeError) { assert !@ship.save }
505
+ assert_not_nil @ship.reload.pirate
506
+ end
507
+
508
+ # has_many & has_and_belongs_to
509
+ %w{ parrots birds }.each do |association_name|
510
+ define_method("test_should_destroy_#{association_name}_as_part_of_the_save_transaction_if_they_were_marked_for_destroyal") do
511
+ 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
512
+
513
+ assert !@pirate.send(association_name).any? { |child| child.marked_for_destruction? }
514
+
515
+ @pirate.send(association_name).each { |child| child.mark_for_destruction }
516
+ klass = @pirate.send(association_name).first.class
517
+ ids = @pirate.send(association_name).map(&:id)
518
+
519
+ assert @pirate.send(association_name).all? { |child| child.marked_for_destruction? }
520
+ ids.each { |id| assert klass.find_by_id(id) }
521
+
522
+ @pirate.save
523
+ assert @pirate.reload.send(association_name).empty?
524
+ ids.each { |id| assert_nil klass.find_by_id(id) }
525
+ end
526
+
527
+ define_method("test_should_skip_validation_on_the_#{association_name}_association_if_marked_for_destruction") do
528
+ 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
529
+ children = @pirate.send(association_name)
530
+
531
+ children.each { |child| child.name = '' }
532
+ assert !@pirate.valid?
533
+
534
+ children.each { |child| child.mark_for_destruction }
535
+ assert_difference("#{association_name.classify}.count", -2) { @pirate.save! }
536
+ end
537
+
538
+ define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
539
+ 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
540
+ before = @pirate.send(association_name).map { |c| c }
541
+
542
+ # Stub the save method of the first child to destroy and the second to raise an exception
543
+ class << before.first
544
+ def save(*args)
545
+ super
546
+ destroy
547
+ end
548
+ end
549
+ class << before.last
550
+ def save(*args)
551
+ super
552
+ raise 'Oh noes!'
553
+ end
554
+ end
555
+
556
+ assert_raise(RuntimeError) { assert !@pirate.save }
557
+ assert_equal before, @pirate.reload.send(association_name)
558
+ end
559
+
560
+ # Add and remove callbacks tests for association collections.
561
+ %w{ method proc }.each do |callback_type|
562
+ define_method("test_should_run_add_callback_#{callback_type}s_for_#{association_name}") do
563
+ association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
564
+
565
+ pirate = Pirate.new(:catchphrase => "Arr")
566
+ pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
567
+
568
+ expected = [
569
+ "before_adding_#{callback_type}_#{association_name.singularize}_<new>",
570
+ "after_adding_#{callback_type}_#{association_name.singularize}_<new>"
571
+ ]
572
+
573
+ assert_equal expected, pirate.ship_log
574
+ end
575
+
576
+ define_method("test_should_run_remove_callback_#{callback_type}s_for_#{association_name}") do
577
+ association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
578
+
579
+ @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
580
+ @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
581
+ child_id = @pirate.send(association_name_with_callbacks).first.id
582
+
583
+ @pirate.ship_log.clear
584
+ @pirate.save
585
+
586
+ expected = [
587
+ "before_removing_#{callback_type}_#{association_name.singularize}_#{child_id}",
588
+ "after_removing_#{callback_type}_#{association_name.singularize}_#{child_id}"
589
+ ]
590
+
591
+ assert_equal expected, @pirate.ship_log
592
+ end
593
+ end
594
+ end
595
+ end
596
+
597
+ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
598
+ self.use_transactional_fixtures = false
599
+
600
+ def setup
601
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
602
+ @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
603
+ end
604
+
605
+ def test_should_still_work_without_an_associated_model
606
+ @ship.destroy
607
+ @pirate.reload.catchphrase = "Arr"
608
+ @pirate.save
609
+ assert 'Arr', @pirate.reload.catchphrase
610
+ end
611
+
612
+ def test_should_automatically_save_the_associated_model
613
+ @pirate.ship.name = 'The Vile Insanity'
614
+ @pirate.save
615
+ assert_equal 'The Vile Insanity', @pirate.reload.ship.name
616
+ end
617
+
618
+ def test_should_automatically_save_bang_the_associated_model
619
+ @pirate.ship.name = 'The Vile Insanity'
620
+ @pirate.save!
621
+ assert_equal 'The Vile Insanity', @pirate.reload.ship.name
622
+ end
623
+
624
+ def test_should_automatically_validate_the_associated_model
625
+ @pirate.ship.name = ''
626
+ assert !@pirate.valid?
627
+ assert !@pirate.errors.on(:ship_name).blank?
628
+ end
629
+
630
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
631
+ @pirate.ship.name = nil
632
+ @pirate.catchphrase = nil
633
+ assert !@pirate.valid?
634
+ assert !@pirate.errors.on(:ship_name).blank?
635
+ assert !@pirate.errors.on(:catchphrase).blank?
636
+ end
637
+
638
+ def test_should_still_allow_to_bypass_validations_on_the_associated_model
639
+ @pirate.catchphrase = ''
640
+ @pirate.ship.name = ''
641
+ @pirate.save(false)
642
+ assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name]
643
+ end
644
+
645
+ def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth
646
+ 2.times { |i| @pirate.ship.parts.create!(:name => "part #{i}") }
647
+
648
+ @pirate.catchphrase = ''
649
+ @pirate.ship.name = ''
650
+ @pirate.ship.parts.each { |part| part.name = '' }
651
+ @pirate.save(false)
652
+
653
+ values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
654
+ assert_equal ['', '', '', ''], values
655
+ end
656
+
657
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
658
+ @pirate.ship.name = ''
659
+ assert_raise(ActiveRecord::RecordInvalid) do
660
+ @pirate.save!
661
+ end
662
+ end
663
+
664
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
665
+ before = [@pirate.catchphrase, @pirate.ship.name]
666
+
667
+ @pirate.catchphrase = 'Arr'
668
+ @pirate.ship.name = 'The Vile Insanity'
669
+
670
+ # Stub the save method of the @pirate.ship instance to raise an exception
671
+ class << @pirate.ship
672
+ def save(*args)
673
+ super
674
+ raise 'Oh noes!'
675
+ end
676
+ end
677
+
678
+ assert_raise(RuntimeError) { assert !@pirate.save }
679
+ assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
680
+ end
681
+
682
+ def test_should_not_load_the_associated_model
683
+ assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
684
+ end
685
+ end
686
+
687
+ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
688
+ self.use_transactional_fixtures = false
689
+
690
+ def setup
691
+ @ship = Ship.create(:name => 'Nights Dirty Lightning')
692
+ @pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
693
+ end
694
+
695
+ def test_should_still_work_without_an_associated_model
696
+ @pirate.destroy
697
+ @ship.reload.name = "The Vile Insanity"
698
+ @ship.save
699
+ assert 'The Vile Insanity', @ship.reload.name
700
+ end
701
+
702
+ def test_should_automatically_save_the_associated_model
703
+ @ship.pirate.catchphrase = 'Arr'
704
+ @ship.save
705
+ assert_equal 'Arr', @ship.reload.pirate.catchphrase
706
+ end
707
+
708
+ def test_should_automatically_save_bang_the_associated_model
709
+ @ship.pirate.catchphrase = 'Arr'
710
+ @ship.save!
711
+ assert_equal 'Arr', @ship.reload.pirate.catchphrase
712
+ end
713
+
714
+ def test_should_automatically_validate_the_associated_model
715
+ @ship.pirate.catchphrase = ''
716
+ assert !@ship.valid?
717
+ assert !@ship.errors.on(:pirate_catchphrase).blank?
718
+ end
719
+
720
+ def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
721
+ @ship.name = nil
722
+ @ship.pirate.catchphrase = nil
723
+ assert !@ship.valid?
724
+ assert !@ship.errors.on(:name).blank?
725
+ assert !@ship.errors.on(:pirate_catchphrase).blank?
726
+ end
727
+
728
+ def test_should_still_allow_to_bypass_validations_on_the_associated_model
729
+ @ship.pirate.catchphrase = ''
730
+ @ship.name = ''
731
+ @ship.save(false)
732
+ assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase]
733
+ end
734
+
735
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
736
+ @ship.pirate.catchphrase = ''
737
+ assert_raise(ActiveRecord::RecordInvalid) do
738
+ @ship.save!
739
+ end
740
+ end
741
+
742
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
743
+ before = [@ship.pirate.catchphrase, @ship.name]
744
+
745
+ @ship.pirate.catchphrase = 'Arr'
746
+ @ship.name = 'The Vile Insanity'
747
+
748
+ # Stub the save method of the @ship.pirate instance to raise an exception
749
+ class << @ship.pirate
750
+ def save(*args)
751
+ super
752
+ raise 'Oh noes!'
753
+ end
754
+ end
755
+
756
+ assert_raise(RuntimeError) { assert !@ship.save }
757
+ # TODO: Why does using reload on @ship looses the associated pirate?
758
+ assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
759
+ end
760
+
761
+ def test_should_not_load_the_associated_model
762
+ assert_queries(1) { @ship.name = 'The Vile Insanity'; @ship.save! }
763
+ end
764
+ end
765
+
766
+ module AutosaveAssociationOnACollectionAssociationTests
767
+ def test_should_automatically_save_the_associated_models
768
+ new_names = ['Grace OMalley', 'Privateers Greed']
769
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
770
+
771
+ @pirate.save
772
+ assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
773
+ end
774
+
775
+ def test_should_automatically_save_bang_the_associated_models
776
+ new_names = ['Grace OMalley', 'Privateers Greed']
777
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
778
+
779
+ @pirate.save!
780
+ assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
781
+ end
782
+
783
+ def test_should_automatically_validate_the_associated_models
784
+ @pirate.send(@association_name).each { |child| child.name = '' }
785
+
786
+ assert !@pirate.valid?
787
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
788
+ assert @pirate.errors.on(@association_name).blank?
789
+ end
790
+
791
+ def test_should_not_use_default_invalid_error_on_associated_models
792
+ @pirate.send(@association_name).build(:name => '')
793
+
794
+ assert !@pirate.valid?
795
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
796
+ assert @pirate.errors.on(@association_name).blank?
797
+ end
798
+
799
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
800
+ @pirate.send(@association_name).each { |child| child.name = '' }
801
+ @pirate.catchphrase = nil
802
+
803
+ assert !@pirate.valid?
804
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
805
+ assert !@pirate.errors.on(:catchphrase).blank?
806
+ end
807
+
808
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
809
+ @pirate.catchphrase = ''
810
+ @pirate.send(@association_name).each { |child| child.name = '' }
811
+
812
+ assert @pirate.save(false)
813
+ assert_equal ['', '', ''], [
814
+ @pirate.reload.catchphrase,
815
+ @pirate.send(@association_name).first.name,
816
+ @pirate.send(@association_name).last.name
817
+ ]
818
+ end
819
+
820
+ def test_should_validation_the_associated_models_on_create
821
+ assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do
822
+ 2.times { @pirate.send(@association_name).build }
823
+ @pirate.save(true)
824
+ end
825
+ end
826
+
827
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
828
+ assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", +2) do
829
+ 2.times { @pirate.send(@association_name).build }
830
+ @pirate.save(false)
831
+ end
832
+ end
833
+
834
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
835
+ before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
836
+ new_names = ['Grace OMalley', 'Privateers Greed']
837
+
838
+ @pirate.catchphrase = 'Arr'
839
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
840
+
841
+ # Stub the save method of the first child instance to raise an exception
842
+ class << @pirate.send(@association_name).first
843
+ def save(*args)
844
+ super
845
+ raise 'Oh noes!'
846
+ end
847
+ end
848
+
849
+ assert_raise(RuntimeError) { assert !@pirate.save }
850
+ assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
851
+ end
852
+
853
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
854
+ @pirate.send(@association_name).each { |child| child.name = '' }
855
+ assert_raise(ActiveRecord::RecordInvalid) do
856
+ @pirate.save!
857
+ end
858
+ end
859
+
860
+ def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet
861
+ assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
862
+
863
+ @pirate.send(@association_name).class # hack to load the target
864
+
865
+ assert_queries(3) do
866
+ @pirate.catchphrase = 'Yarr'
867
+ new_names = ['Grace OMalley', 'Privateers Greed']
868
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
869
+ @pirate.save!
870
+ end
871
+ end
872
+ end
873
+
874
+ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
875
+ self.use_transactional_fixtures = false
876
+
877
+ def setup
878
+ @association_name = :birds
879
+
880
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
881
+ @child_1 = @pirate.birds.create(:name => 'Posideons Killer')
882
+ @child_2 = @pirate.birds.create(:name => 'Killer bandita Dionne')
883
+ end
884
+
885
+ include AutosaveAssociationOnACollectionAssociationTests
886
+ end
887
+
888
+ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
889
+ self.use_transactional_fixtures = false
890
+
891
+ def setup
892
+ @association_name = :parrots
893
+ @habtm = true
894
+
895
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
896
+ @child_1 = @pirate.parrots.create(:name => 'Posideons Killer')
897
+ @child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne')
898
+ end
899
+
900
+ include AutosaveAssociationOnACollectionAssociationTests
901
+ end