active_type 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,682 @@
1
+ require 'spec_helper'
2
+
3
+ module NestedAttributesSpec
4
+
5
+ class Record < ActiveRecord::Base
6
+ attr_accessor :fail_on_save, :error
7
+
8
+ before_save :check_fail
9
+
10
+ validate :check_error
11
+
12
+ private
13
+
14
+ def check_fail
15
+ if fail_on_save == true
16
+ false
17
+ end
18
+ end
19
+
20
+ def check_error
21
+ if error.present?
22
+ errors.add(:base, error)
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ class GlobalRecord < ActiveRecord::Base
30
+ self.table_name = 'records'
31
+ end
32
+
33
+
34
+ describe "ActiveType::Object" do
35
+
36
+ context '.nests_many' do
37
+
38
+ let(:extra_options) { {} }
39
+
40
+ subject do
41
+ extra = extra_options
42
+ Class.new(ActiveType::Object) do
43
+ nests_many :records, extra.merge(:scope => NestedAttributesSpec::Record)
44
+
45
+ def bad(attributes)
46
+ attributes[:persisted_string] =~ /bad/
47
+ end
48
+
49
+ def reject_all
50
+ true
51
+ end
52
+
53
+ end.new
54
+ end
55
+
56
+ def should_assign_and_persist(assign, persist = assign)
57
+ subject.records.map(&:persisted_string).should == assign
58
+ subject.save.should be_true
59
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ persist
60
+ end
61
+
62
+
63
+ context 'with no records assigned' do
64
+
65
+ it 'can save' do
66
+ subject.save.should be_true
67
+ end
68
+
69
+ end
70
+
71
+ context 'assigning nil' do
72
+
73
+ it 'will do nothing' do
74
+ subject.records_attributes = nil
75
+ subject.records.should be_nil
76
+ end
77
+
78
+ end
79
+
80
+ context 'when assigning records without ids' do
81
+
82
+ it 'builds single nested records' do
83
+ subject.records_attributes = { 1 => {:persisted_string => "string"} }
84
+
85
+ should_assign_and_persist(["string"])
86
+ end
87
+
88
+ it 'builds multiple nested records when given a hash of attributes, ordered by key' do
89
+ subject.records_attributes = {
90
+ 3 => {:persisted_string => "string 3"},
91
+ 1 => {:persisted_string => "string 1"},
92
+ 2 => {:persisted_string => "string 2"},
93
+ }
94
+
95
+ should_assign_and_persist(["string 1", "string 2", "string 3"])
96
+ end
97
+
98
+ it 'builds multiple nested records when given an array of attributes' do
99
+ subject.records_attributes = [
100
+ {:persisted_string => "string 1"},
101
+ {:persisted_string => "string 2"},
102
+ {:persisted_string => "string 3"},
103
+ ]
104
+
105
+ should_assign_and_persist(["string 1", "string 2", "string 3"])
106
+ end
107
+
108
+ it 'does not build records that match a :reject_if proc' do
109
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
110
+ subject.records_attributes = {
111
+ 1 => {:persisted_string => "good value"},
112
+ 2 => {:persisted_string => "bad value"},
113
+ }
114
+
115
+ should_assign_and_persist(["good value"])
116
+ end
117
+
118
+ it 'does not build records that match a :reject_if method taking attributes' do
119
+ extra_options.merge!(:reject_if => :bad)
120
+ subject.records_attributes = {
121
+ 1 => {:persisted_string => "good value"},
122
+ 2 => {:persisted_string => "bad value"},
123
+ }
124
+
125
+ should_assign_and_persist(["good value"])
126
+ end
127
+
128
+ it 'does not build records that match a :reject_if method taking attributes' do
129
+ extra_options.merge!(:reject_if => :bad)
130
+ subject.records_attributes = {
131
+ 1 => {:persisted_string => "good value"},
132
+ 2 => {:persisted_string => "bad value"},
133
+ }
134
+
135
+ should_assign_and_persist(["good value"])
136
+ end
137
+
138
+ it 'does not build records that match a :reject_if method taking no attributes' do
139
+ extra_options.merge!(:reject_if => :reject_all)
140
+ subject.records_attributes = {
141
+ 1 => {:persisted_string => "good value"},
142
+ 2 => {:persisted_string => "bad value"},
143
+ }
144
+
145
+ should_assign_and_persist([])
146
+ end
147
+
148
+ it 'does not build records that match a :reject_if all_blank' do
149
+ extra_options.merge!(:reject_if => :all_blank)
150
+ subject.records_attributes = {
151
+ 1 => {:persisted_string => "good value"},
152
+ 2 => {},
153
+ }
154
+
155
+ should_assign_and_persist(["good value"])
156
+ end
157
+
158
+ it 'appends to existing records' do
159
+ subject.records = [NestedAttributesSpec::Record.create!(:persisted_string => "existing string")]
160
+ subject.records_attributes = { 1 => {:persisted_string => "new string"} }
161
+
162
+ should_assign_and_persist(["existing string", "new string"])
163
+ end
164
+
165
+ it 'leaves unassigned records alone' do
166
+ NestedAttributesSpec::Record.create!(:persisted_string => "unassigned")
167
+ subject.records_attributes = { 1 => {:persisted_string => "string"} }
168
+
169
+ should_assign_and_persist(["string"], ["unassigned", "string"])
170
+ end
171
+
172
+ it 'does not destroy records on _destroy => trueish by default' do
173
+ existing = NestedAttributesSpec::Record.create!(:persisted_string => 'do not delete this')
174
+
175
+ subject.records_attributes = [
176
+ { :id => existing.id, :_destroy => "true" },
177
+ ]
178
+ should_assign_and_persist(["do not delete this"], ["do not delete this"])
179
+ subject.records.size.should == 1
180
+ end
181
+
182
+ it 'destroys records on _destroy => trueish if allowed' do
183
+ extra_options.merge!(:allow_destroy => true)
184
+ existing = [
185
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
186
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
187
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
188
+ NestedAttributesSpec::Record.create!(:persisted_string => 'keep this'),
189
+ ]
190
+
191
+ subject.records = existing.first(2) # assign some
192
+
193
+ subject.records_attributes = [
194
+ { :id => existing[0].id, :_destroy => "true" },
195
+ { :id => existing[1].id, :_destroy => 1 },
196
+ { :id => existing[2].id, :_destroy => "1" },
197
+ { :id => existing[3].id, :_destroy => "0" },
198
+ ]
199
+ should_assign_and_persist(["delete this", "delete this", "delete this", "keep this"], ["keep this"])
200
+ subject.records.size.should == 1
201
+ end
202
+
203
+ end
204
+
205
+ context 'when assigning records with ids' do
206
+
207
+ it 'updates the record with the id if already assigned' do
208
+ subject.records = [
209
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 1"),
210
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 2"),
211
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 3"),
212
+ ]
213
+ subject.records[0].id = 100
214
+ subject.records[1].id = 101
215
+ subject.records[2].id = 102
216
+
217
+ subject.records_attributes = { 1 => {:id => 101, :persisted_string => "updated"} }
218
+
219
+ should_assign_and_persist(["existing 1", "updated", "existing 3"])
220
+ end
221
+
222
+ it 'does not update records matching a reject_if proc' do
223
+ extra_options.merge!(:reject_if => :bad)
224
+ subject.records = [
225
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 1"),
226
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 2"),
227
+ ]
228
+ subject.records[0].id = 100
229
+ subject.records[1].id = 101
230
+
231
+ subject.records_attributes = [
232
+ {:id => 100, :persisted_string => "good"},
233
+ {:id => 101, :persisted_string => "bad"}
234
+ ]
235
+
236
+ should_assign_and_persist(["good", "existing 2"])
237
+ end
238
+
239
+ it 'fetches the record with the id if not already assigned' do
240
+ record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string 1")
241
+ subject.records = [
242
+ NestedAttributesSpec::Record.new(:persisted_string => "existing string 2"),
243
+ ]
244
+ subject.records[0].id = record.id + 1
245
+
246
+ subject.records_attributes = { 1 => {:id => record.id, :persisted_string => "updated string"} }
247
+
248
+ should_assign_and_persist(["existing string 2", "updated string"])
249
+ end
250
+
251
+ it 'raises an error if the child record does not exist' do
252
+ expect do
253
+ subject.records_attributes = { 1 => {:id => 1, :persisted_string => "updated string"} }
254
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '1' for 'records'")
255
+ end
256
+
257
+ end
258
+
259
+ context 'save failure' do
260
+
261
+ it 'returns false on #save and does not save the child' do
262
+ # it should also cause a rollback, but that will not work with sqlite3
263
+ subject.records = [
264
+ NestedAttributesSpec::Record.new(:fail_on_save => true),
265
+ ]
266
+
267
+ subject.save.should be_false
268
+ NestedAttributesSpec::Record.count.should == 0
269
+
270
+ # note that other children would be saved and not be rolled back
271
+ # this is also true for regular nested attributes
272
+ end
273
+
274
+ end
275
+
276
+ context 'validations' do
277
+
278
+ describe '#valid?' do
279
+
280
+ it 'is true if there are no records assigned' do
281
+ subject.valid?.should be_true
282
+ end
283
+
284
+ it 'is true if all records are valid' do
285
+ subject.records = [
286
+ NestedAttributesSpec::Record.new,
287
+ NestedAttributesSpec::Record.new,
288
+ ]
289
+
290
+ subject.valid?.should be_true
291
+ end
292
+
293
+ it 'is false if one child has an error' do
294
+ subject.records = [
295
+ NestedAttributesSpec::Record.new,
296
+ NestedAttributesSpec::Record.new(:error => 'some error'),
297
+ ]
298
+
299
+ subject.valid?.should be_false
300
+ end
301
+
302
+ it 'is copies the error to the record' do
303
+ subject.records = [
304
+ NestedAttributesSpec::Record.new,
305
+ NestedAttributesSpec::Record.new(:error => 'some error'),
306
+ ]
307
+
308
+ subject.valid?
309
+ subject.errors["records.base"].should == ['some error']
310
+ end
311
+
312
+ end
313
+
314
+ end
315
+
316
+ end
317
+
318
+
319
+ context '.nests_one' do
320
+
321
+ let(:extra_options) { {} }
322
+
323
+ subject do
324
+ extra = extra_options
325
+ Class.new(ActiveType::Object) do
326
+ nests_one :record, extra.merge(:scope => NestedAttributesSpec::Record)
327
+
328
+ def bad(attributes)
329
+ attributes[:persisted_string] =~ /bad/
330
+ end
331
+ end.new
332
+ end
333
+
334
+ def should_assign_and_persist(assign, persist = assign)
335
+ if assign
336
+ subject.record.should be_present
337
+ subject.record.persisted_string.should == assign
338
+ else
339
+ subject.record.should be_nil
340
+ end
341
+ subject.save.should be_true
342
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should == (persist ? [persist] : [])
343
+ end
344
+
345
+
346
+ context 'with no record assigned' do
347
+
348
+ it 'can save' do
349
+ subject.save.should be_true
350
+ end
351
+
352
+ end
353
+
354
+ context 'assigning nil' do
355
+
356
+ it 'will do nothing' do
357
+ subject.record_attributes = nil
358
+ subject.record.should be_nil
359
+ end
360
+
361
+ end
362
+
363
+ context 'when assigning a records without an id' do
364
+
365
+ it 'builds a nested records' do
366
+ subject.record_attributes = { :persisted_string => "string" }
367
+
368
+ should_assign_and_persist("string")
369
+ end
370
+
371
+ it 'does not build a record that matchs a :reject_if proc' do
372
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
373
+ subject.record_attributes = { :persisted_string => "bad" }
374
+
375
+ should_assign_and_persist(nil)
376
+ end
377
+
378
+
379
+ it 'updates an assigned record' do
380
+ subject.record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string")
381
+ subject.record_attributes = { :persisted_string => "new string" }
382
+
383
+ should_assign_and_persist("new string")
384
+ end
385
+
386
+ it 'does not update a record that matchs a :reject_if proc' do
387
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
388
+ subject.record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string")
389
+ subject.record_attributes = { :persisted_string => "bad" }
390
+
391
+ should_assign_and_persist("existing string")
392
+ end
393
+
394
+
395
+ end
396
+
397
+ context 'when assigning a records with an id' do
398
+
399
+ let(:record) { record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string") }
400
+
401
+ it 'updates the record if already assigned' do
402
+ subject.record = record
403
+
404
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string"}
405
+
406
+ should_assign_and_persist("updated string")
407
+ end
408
+
409
+ it 'fetches the record with the id if not already assigned' do
410
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string" }
411
+
412
+ should_assign_and_persist("updated string")
413
+ end
414
+
415
+ it 'does not destroy records on _destroy => true by default' do
416
+ subject.record_attributes = { :id => record.id, :_destroy => true }
417
+
418
+ should_assign_and_persist("existing string", "existing string")
419
+ end
420
+
421
+ it 'destroys records on _destroy => true if allowed' do
422
+ extra_options.merge!(:allow_destroy => true)
423
+ subject.record_attributes = { :id => record.id, :_destroy => true }
424
+
425
+ should_assign_and_persist("existing string", nil)
426
+ subject.record.should == nil
427
+ end
428
+
429
+ it 'raises an error if the assigned record does not match the id' do
430
+ expect do
431
+ subject.record = NestedAttributesSpec::Record.create!
432
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string" }
433
+ end.to raise_error(ActiveType::NestedAttributes::AssignmentError, "child record 'record' did not match id '#{record.id}'")
434
+ end
435
+
436
+ it 'raises an error if a record with the id cannot be found' do
437
+ expect do
438
+ subject.record_attributes = { :id => 1, :persisted_string => "updated string" }
439
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '1' for 'record'")
440
+ end
441
+
442
+ end
443
+
444
+ context 'validations' do
445
+
446
+ describe '#valid?' do
447
+
448
+ it 'is true if there is no record assigned' do
449
+ subject.valid?.should be_true
450
+ end
451
+
452
+ it 'is true if the assigned record is valid' do
453
+ subject.record = NestedAttributesSpec::Record.new
454
+
455
+ subject.valid?.should be_true
456
+ end
457
+
458
+ it 'is false the assigned record has an error' do
459
+ subject.record = NestedAttributesSpec::Record.new(:error => 'some error')
460
+
461
+ subject.valid?.should be_false
462
+ end
463
+
464
+ it 'is copies the error to the record' do
465
+ subject.record = NestedAttributesSpec::Record.new(:error => 'some error')
466
+
467
+ subject.valid?
468
+ subject.errors["record.base"].should == ['some error']
469
+ end
470
+
471
+ end
472
+
473
+ end
474
+
475
+ end
476
+
477
+ context '.nests_one/nests_many' do
478
+
479
+ context 'inheritance' do
480
+
481
+ let(:base_class) do
482
+ Class.new(ActiveType::Object) do
483
+ nests_one :record, :scope => NestedAttributesSpec::Record
484
+ end
485
+ end
486
+
487
+ it 'works across inheritance hierarchy' do
488
+ subject = Class.new(base_class) do
489
+ nests_one :another_record, :scope => NestedAttributesSpec::Record
490
+ end.new
491
+
492
+ subject.record_attributes = { :persisted_string => "string" }
493
+ subject.another_record_attributes = {:persisted_string => "another string"}
494
+
495
+ subject.record.persisted_string.should == "string"
496
+ subject.another_record.persisted_string.should == "another string"
497
+ subject.save.should be_true
498
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ ["string", "another string"]
499
+ end
500
+
501
+ it 'allows overriding of the accessor' do
502
+ subject = Class.new(base_class) do
503
+ def record_attributes=(attributes)
504
+ reached
505
+ super
506
+ end
507
+
508
+ def reached
509
+ end
510
+ end.new
511
+
512
+ subject.should_receive(:reached)
513
+ subject.record_attributes = { :persisted_string => "string" }
514
+
515
+ subject.record.persisted_string.should == "string"
516
+ subject.save.should be_true
517
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ ["string"]
518
+ end
519
+
520
+ end
521
+
522
+ context 'when not giving a scope' do
523
+
524
+ subject do
525
+ Class.new(ActiveType::Object) do
526
+ nests_many :global_records
527
+ nests_one :global_record
528
+ end.new
529
+ end
530
+
531
+ it 'infers the scope from the association name' do
532
+ subject.global_records_attributes = { 1 => { :persisted_string => "string" } }
533
+ subject.global_record_attributes = { :persisted_string => "string" }
534
+
535
+ subject.global_records.first.should be_a(GlobalRecord)
536
+ subject.global_record.should be_a(GlobalRecord)
537
+ end
538
+
539
+ end
540
+
541
+ context 'when giving a scope via a proc' do
542
+
543
+ subject do
544
+ Class.new(ActiveType::Object) do
545
+ nests_many :records, :scope => proc { NestedAttributesSpec::Record.where("persisted_string <> 'invisible'") }
546
+ nests_one :record, :scope => proc { NestedAttributesSpec::Record }
547
+
548
+ attribute :default_value, :string
549
+ nests_many :default_records, :scope => proc { NestedAttributesSpec::Record.where(:persisted_string => default_value) }
550
+ end.new
551
+ end
552
+
553
+ it 'uses the scope' do
554
+ subject.records_attributes = { 1 => { :persisted_string => "string" } }
555
+ subject.record_attributes = { :persisted_string => "string" }
556
+
557
+ subject.records.first.should be_a(NestedAttributesSpec::Record)
558
+ subject.record.should be_a(NestedAttributesSpec::Record)
559
+ end
560
+
561
+ it 'evals the scope lazily in the instance' do
562
+ subject.default_value = "default value"
563
+ subject.default_records_attributes = [{}]
564
+
565
+ subject.default_records.map(&:persisted_string).should == ["default value"]
566
+ end
567
+
568
+ it 'caches the scope' do
569
+ subject.default_value = "default value"
570
+ subject.default_records_attributes = [{}]
571
+ subject.default_value = "another default value"
572
+ subject.default_records_attributes = [{}]
573
+
574
+ subject.default_records.map(&:persisted_string).should == ["default value", "default value"]
575
+ end
576
+
577
+ it 'caches the scope per instance' do
578
+ subject.default_value = "default value"
579
+ subject.default_records_attributes = [{}]
580
+
581
+ another_subject = subject.class.new
582
+ another_subject.default_value = "another default value"
583
+ another_subject.default_records_attributes = [{}]
584
+
585
+ another_subject.default_records.map(&:persisted_string).should == ["another default value"]
586
+ end
587
+
588
+ it 'raises an error if the child record is not found via the scope' do
589
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'invisible')
590
+
591
+ expect do
592
+ subject.records_attributes = { 1 => { :id => record.id, :persisted_string => "updated string" } }
593
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '#{record.id}' for 'records'")
594
+ end
595
+ end
596
+
597
+ context 'separate scopes for build and find' do
598
+
599
+ subject do
600
+ find_scope = proc { NestedAttributesSpec::Record.where(:persisted_string => 'findable') }
601
+ build_scope = proc { NestedAttributesSpec::Record.where(:persisted_string => 'buildable') }
602
+ Class.new(ActiveType::Object) do
603
+ nests_many :records, :build_scope => build_scope, :find_scope => find_scope
604
+ nests_one :record, :build_scope => build_scope, :find_scope => find_scope
605
+ end.new
606
+ end
607
+
608
+ it 'nests_many uses the find_scope to find records' do
609
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'findable')
610
+ hidden_record = NestedAttributesSpec::Record.create!(:persisted_string => 'hidden')
611
+
612
+ expect do
613
+ subject.records_attributes = [{ :id => record.id, :persisted_string => 'updated' }]
614
+ end.to_not raise_error
615
+
616
+ expect do
617
+ subject.records_attributes = [{ :id => hidden_record.id, :persisted_string => 'updated' }]
618
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound)
619
+ end
620
+
621
+ it 'nests_many uses the build_scope to find records' do
622
+ subject.records_attributes = [{}]
623
+ subject.records.first.persisted_string.should == 'buildable'
624
+ end
625
+
626
+ it 'nests_one uses the find_scope to find records' do
627
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'findable')
628
+ hidden_record = NestedAttributesSpec::Record.create!(:persisted_string => 'hidden')
629
+
630
+ expect do
631
+ subject.record_attributes = { :id => record.id, :persisted_string => 'updated' }
632
+ end.to_not raise_error
633
+
634
+ subject.record = nil
635
+ expect do
636
+ subject.record_attributes = { :id => hidden_record.id, :persisted_string => 'updated' }
637
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound)
638
+ end
639
+
640
+ it 'nests_one uses the build_scope to find records' do
641
+ subject.record_attributes = {}
642
+ subject.record.persisted_string.should == 'buildable'
643
+ end
644
+
645
+ end
646
+
647
+ context 'defaults' do
648
+
649
+ subject do
650
+ Class.new(ActiveType::Object) do
651
+ nests_many :records, :default => proc { [default_record] }
652
+ nests_one :record, :default => proc { default_record }
653
+
654
+ nests_many :global_records
655
+
656
+ nests_many :other_records, :scope => proc { NestedAttributesSpec::Record }
657
+ nests_one :other_record, :scope => proc { NestedAttributesSpec::Record }
658
+
659
+ nests_many :records_without_default, :default => nil
660
+
661
+ def default_record
662
+ NestedAttributesSpec::Record.new(:persisted_string => "default")
663
+ end
664
+ end.new
665
+ end
666
+
667
+ it 'accepts a :default value' do
668
+ subject.records.map(&:persisted_string).should == ["default"]
669
+ subject.record.persisted_string.should == "default"
670
+ end
671
+
672
+ it 'computes the value lazily' do
673
+ subject.stub :default_record => NestedAttributesSpec::Record.new(:persisted_string => "other default")
674
+ subject.records.map(&:persisted_string).should == ["other default"]
675
+ subject.record.persisted_string.should == "other default"
676
+ end
677
+
678
+ end
679
+
680
+ end
681
+
682
+ end
@@ -284,6 +284,10 @@ describe ActiveType::Object do
284
284
  end
285
285
  end
286
286
 
287
+ describe 'defaults' do
288
+ it_should_behave_like "a class accepting attribute defaults", ActiveType::Object
289
+ end
290
+
287
291
  describe '#belongs_to' do
288
292
  subject { ObjectSpec::ObjectWithBelongsTo.new }
289
293