mongoid 8.0.7 → 8.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +65 -41
  3. data/lib/mongoid/association/accessors.rb +5 -1
  4. data/lib/mongoid/association/eager_loadable.rb +3 -0
  5. data/lib/mongoid/atomic.rb +9 -7
  6. data/lib/mongoid/config.rb +10 -0
  7. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +15 -1
  8. data/lib/mongoid/document.rb +8 -1
  9. data/lib/mongoid/fields.rb +11 -6
  10. data/lib/mongoid/interceptable.rb +10 -8
  11. data/lib/mongoid/timestamps/created.rb +8 -1
  12. data/lib/mongoid/traversable.rb +12 -0
  13. data/lib/mongoid/validatable/associated.rb +98 -17
  14. data/lib/mongoid/validatable.rb +8 -0
  15. data/lib/mongoid/version.rb +1 -1
  16. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +40 -0
  17. data/spec/integration/callbacks_models.rb +37 -0
  18. data/spec/integration/callbacks_spec.rb +27 -0
  19. data/spec/mongoid/association/eager_spec.rb +24 -2
  20. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
  21. data/spec/mongoid/association_spec.rb +60 -0
  22. data/spec/mongoid/document_spec.rb +27 -0
  23. data/spec/mongoid/interceptable_spec.rb +100 -0
  24. data/spec/mongoid/interceptable_spec_models.rb +51 -111
  25. data/spec/mongoid/serializable_spec.rb +14 -14
  26. data/spec/mongoid/timestamps/created_spec.rb +23 -0
  27. data/spec/mongoid/validatable/associated_spec.rb +27 -34
  28. data/spec/support/models/name.rb +10 -0
  29. metadata +4 -80
  30. checksums.yaml.gz.sig +0 -0
  31. data/spec/shared/LICENSE +0 -20
  32. data/spec/shared/bin/get-mongodb-download-url +0 -17
  33. data/spec/shared/bin/s3-copy +0 -45
  34. data/spec/shared/bin/s3-upload +0 -69
  35. data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
  36. data/spec/shared/lib/mrss/cluster_config.rb +0 -231
  37. data/spec/shared/lib/mrss/constraints.rb +0 -378
  38. data/spec/shared/lib/mrss/docker_runner.rb +0 -298
  39. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -51
  40. data/spec/shared/lib/mrss/event_subscriber.rb +0 -210
  41. data/spec/shared/lib/mrss/lite_constraints.rb +0 -238
  42. data/spec/shared/lib/mrss/server_version_registry.rb +0 -113
  43. data/spec/shared/lib/mrss/session_registry.rb +0 -69
  44. data/spec/shared/lib/mrss/session_registry_legacy.rb +0 -60
  45. data/spec/shared/lib/mrss/spec_organizer.rb +0 -179
  46. data/spec/shared/lib/mrss/utils.rb +0 -37
  47. data/spec/shared/share/Dockerfile.erb +0 -321
  48. data/spec/shared/share/haproxy-1.conf +0 -16
  49. data/spec/shared/share/haproxy-2.conf +0 -17
  50. data/spec/shared/shlib/config.sh +0 -27
  51. data/spec/shared/shlib/distro.sh +0 -74
  52. data/spec/shared/shlib/server.sh +0 -416
  53. data/spec/shared/shlib/set_env.sh +0 -169
  54. data.tar.gz.sig +0 -4
  55. metadata.gz.sig +0 -0
@@ -467,4 +467,31 @@ describe 'callbacks integration tests' do
467
467
  expect { book.save! }.not_to raise_error(SystemStackError)
468
468
  end
469
469
  end
470
+
471
+ context 'nested embedded documents' do
472
+ config_override :prevent_multiple_calls_of_embedded_callbacks, true
473
+
474
+ let(:logger) { Array.new }
475
+
476
+ let(:root) do
477
+ Root.new(
478
+ embedded_once: [
479
+ EmbeddedOnce.new(
480
+ embedded_twice: [EmbeddedTwice.new]
481
+ )
482
+ ]
483
+ )
484
+ end
485
+
486
+ before(:each) do
487
+ root.logger = logger
488
+ root.embedded_once.first.logger = logger
489
+ root.embedded_once.first.embedded_twice.first.logger = logger
490
+ end
491
+
492
+ it 'runs callbacks in the correct order' do
493
+ root.save!
494
+ expect(logger).to eq(%i[embedded_twice embedded_once root])
495
+ end
496
+ end
470
497
  end
@@ -14,14 +14,36 @@ describe Mongoid::Association::EagerLoadable do
14
14
  Mongoid::Contextual::Mongo.new(criteria)
15
15
  end
16
16
 
17
+ let(:association_host) { Account }
18
+
17
19
  let(:inclusions) do
18
20
  includes.map do |key|
19
- Account.reflect_on_association(key)
21
+ association_host.reflect_on_association(key)
20
22
  end
21
23
  end
22
24
 
23
25
  let(:doc) { criteria.first }
24
26
 
27
+ context 'when root is an STI subclass' do
28
+ # Driver has_one Vehicle
29
+ # Vehicle belongs_to Driver
30
+ # Truck is a Vehicle
31
+
32
+ before do
33
+ Driver.create!(vehicle: Truck.new)
34
+ end
35
+
36
+ let(:criteria) { Truck.all }
37
+ let(:includes) { %i[ driver ] }
38
+ let(:association_host) { Truck }
39
+
40
+ it 'preloads the driver' do
41
+ expect(doc.ivar(:driver)).to be false
42
+ context.preload(inclusions, [ doc ])
43
+ expect(doc.ivar(:driver)).to be == Driver.first
44
+ end
45
+ end
46
+
25
47
  context "when belongs_to" do
26
48
 
27
49
  let!(:account) do
@@ -42,7 +64,7 @@ describe Mongoid::Association::EagerLoadable do
42
64
  it "preloads the parent" do
43
65
  expect(doc.ivar(:person)).to be false
44
66
  context.preload(inclusions, [doc])
45
- expect(doc.ivar(:person)).to eq(doc.person)
67
+ expect(doc.ivar(:person)).to be == person
46
68
  end
47
69
  end
48
70
 
@@ -27,6 +27,10 @@ describe Mongoid::Association::Embedded::EmbedsMany do
27
27
  expect(legislator.attributes.keys).to eq(['_id', 'a'])
28
28
  end
29
29
 
30
+ it 'allows accessing the parent' do
31
+ expect { legislator.congress }.not_to raise_error
32
+ end
33
+
30
34
  context 'when using only with $' do
31
35
  before do
32
36
  Patient.destroy_all
@@ -100,6 +100,66 @@ describe Mongoid::Association do
100
100
  expect(name).to_not be_an_embedded_many
101
101
  end
102
102
  end
103
+
104
+ context "when validation depends on association" do
105
+ before(:all) do
106
+ class Author
107
+ include Mongoid::Document
108
+ embeds_many :books, cascade_callbacks: true
109
+ field :condition, type: Boolean
110
+ end
111
+
112
+ class Book
113
+ include Mongoid::Document
114
+ embedded_in :author
115
+ validate :parent_condition_is_not_true
116
+
117
+ def parent_condition_is_not_true
118
+ return unless author&.condition
119
+ errors.add :base, "Author condition is true."
120
+ end
121
+ end
122
+
123
+ Author.delete_all
124
+ Book.delete_all
125
+ end
126
+
127
+ let(:author) { Author.new }
128
+ let(:book) { Book.new }
129
+
130
+ context "when author is not persisted" do
131
+ it "is valid without books" do
132
+ expect(author.valid?).to be true
133
+ end
134
+
135
+ it "is valid with a book" do
136
+ author.books << book
137
+ expect(author.valid?).to be true
138
+ end
139
+
140
+ it "is not valid when condition is true with a book" do
141
+ author.condition = true
142
+ author.books << book
143
+ expect(author.valid?).to be false
144
+ end
145
+ end
146
+
147
+ context "when author is persisted" do
148
+ before do
149
+ author.books << book
150
+ author.save
151
+ end
152
+
153
+ it "remains valid initially" do
154
+ expect(author.valid?).to be true
155
+ end
156
+
157
+ it "becomes invalid when condition is set to true" do
158
+ author.update_attributes(condition: true)
159
+ expect(author.valid?).to be false
160
+ end
161
+ end
162
+ end
103
163
  end
104
164
 
105
165
  describe "#embedded_one?" do
@@ -591,6 +591,33 @@ describe Mongoid::Document do
591
591
  expect(person.as_document["addresses"].first).to have_key(:locations)
592
592
  end
593
593
 
594
+ context 'when modifying the returned object' do
595
+ let(:record) do
596
+ RootCategory.create(categories: [{ name: 'tests' }]).reload
597
+ end
598
+
599
+ shared_examples_for 'an object with protected internal state' do
600
+ it 'does not expose internal state' do
601
+ before_change = record.as_document.dup
602
+ record.categories.first.name = 'things'
603
+ after_change = record.as_document
604
+ expect(before_change['categories'].first['name']).not_to eq('things')
605
+ end
606
+ end
607
+
608
+ context 'when legacy_attributes is true' do
609
+ config_override :legacy_attributes, true
610
+
611
+ it_behaves_like 'an object with protected internal state'
612
+ end
613
+
614
+ context 'when legacy_attributes is false' do
615
+ config_override :legacy_attributes, false
616
+
617
+ it_behaves_like 'an object with protected internal state'
618
+ end
619
+ end
620
+
594
621
  context "with relation define store_as option in embeded_many" do
595
622
 
596
623
  let!(:phone) do
@@ -388,6 +388,86 @@ describe Mongoid::Interceptable do
388
388
  end
389
389
  end
390
390
  end
391
+
392
+ context 'with embedded grandchildren' do
393
+ IS = InterceptableSpec
394
+
395
+ config_override :prevent_multiple_calls_of_embedded_callbacks, true
396
+
397
+ context 'when creating' do
398
+ let(:registry) { IS::CallbackRegistry.new(only: %i[ before_save ]) }
399
+
400
+ let(:expected_calls) do
401
+ [
402
+ # the parent
403
+ [ IS::CbParent, :before_save ],
404
+
405
+ # the immediate child of the parent
406
+ [ IS::CbCascadedNode, :before_save ],
407
+
408
+ # the grandchild of the parent
409
+ [ IS::CbCascadedNode, :before_save ],
410
+ ]
411
+ end
412
+
413
+ let!(:parent) do
414
+ parent = IS::CbParent.new(registry)
415
+ child = IS::CbCascadedNode.new(registry)
416
+ grandchild = IS::CbCascadedNode.new(registry)
417
+
418
+ child.cb_cascaded_nodes = [ grandchild ]
419
+ parent.cb_cascaded_nodes = [ child ]
420
+
421
+ parent.tap(&:save)
422
+ end
423
+
424
+ it 'should cascade callbacks to grandchildren' do
425
+ expect(registry.calls).to be == expected_calls
426
+ end
427
+ end
428
+
429
+ context 'when updating' do
430
+ let(:registry) { IS::CallbackRegistry.new(only: %i[ before_update ]) }
431
+
432
+ let(:expected_calls) do
433
+ [
434
+ # the parent
435
+ [ IS::CbParent, :before_update ],
436
+
437
+ # the immediate child of the parent
438
+ [ IS::CbCascadedNode, :before_update ],
439
+
440
+ # the grandchild of the parent
441
+ [ IS::CbCascadedNode, :before_update ],
442
+ ]
443
+ end
444
+
445
+ let!(:parent) do
446
+ parent = IS::CbParent.new(nil)
447
+ child = IS::CbCascadedNode.new(nil)
448
+ grandchild = IS::CbCascadedNode.new(nil)
449
+
450
+ child.cb_cascaded_nodes = [ grandchild ]
451
+ parent.cb_cascaded_nodes = [ child ]
452
+
453
+ parent.save
454
+
455
+ parent.callback_registry = registry
456
+ child.callback_registry = registry
457
+ grandchild.callback_registry = registry
458
+
459
+ parent.name = 'updated'
460
+ child.name = 'updated'
461
+ grandchild.name = 'updated'
462
+
463
+ parent.tap(&:save)
464
+ end
465
+
466
+ it 'should cascade callbacks to grandchildren' do
467
+ expect(registry.calls).to be == expected_calls
468
+ end
469
+ end
470
+ end
391
471
  end
392
472
 
393
473
  describe ".before_destroy" do
@@ -578,6 +658,26 @@ describe Mongoid::Interceptable do
578
658
  band.notes.push(note)
579
659
  record.notes.push(note)
580
660
  end
661
+
662
+ context "when saving the root" do
663
+ context 'with prevent_multiple_calls_of_embedded_callbacks enabled' do
664
+ config_override :prevent_multiple_calls_of_embedded_callbacks, true
665
+
666
+ it "executes the callbacks only once for each document" do
667
+ expect(note).to receive(:update_saved).once
668
+ band.save!
669
+ end
670
+ end
671
+
672
+ context 'with prevent_multiple_calls_of_embedded_callbacks disabled' do
673
+ config_override :prevent_multiple_calls_of_embedded_callbacks, false
674
+
675
+ it "executes the callbacks once for each ember" do
676
+ expect(note).to receive(:update_saved).twice
677
+ band.save!
678
+ end
679
+ end
680
+ end
581
681
  end
582
682
  end
583
683
 
@@ -1,13 +1,19 @@
1
1
  module InterceptableSpec
2
2
  class CallbackRegistry
3
- def initialize
3
+ def initialize(only: [])
4
4
  @calls = []
5
+ @only = only
5
6
  end
6
7
 
7
8
  def record_call(cls, cb)
9
+ return unless @only.empty? || @only.any? { |pat| pat == cb }
8
10
  @calls << [cls, cb]
9
11
  end
10
12
 
13
+ def reset!
14
+ @calls.clear
15
+ end
16
+
11
17
  attr_reader :calls
12
18
  end
13
19
 
@@ -15,6 +21,8 @@ module InterceptableSpec
15
21
  extend ActiveSupport::Concern
16
22
 
17
23
  included do
24
+ field :name, type: String
25
+
18
26
  %i(
19
27
  validation save create update
20
28
  ).each do |what|
@@ -34,196 +42,128 @@ module InterceptableSpec
34
42
  end
35
43
  end
36
44
  end
37
- end
38
45
 
39
- class CbHasOneParent
40
- include Mongoid::Document
41
-
42
- has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
46
+ attr_accessor :callback_registry
43
47
 
44
- def initialize(callback_registry)
48
+ def initialize(callback_registry, *args, **kwargs)
45
49
  @callback_registry = callback_registry
46
- super()
50
+ super(*args, **kwargs)
47
51
  end
52
+ end
48
53
 
49
- attr_accessor :callback_registry
50
-
54
+ module RootInsertable
51
55
  def insert_as_root
52
56
  @callback_registry&.record_call(self.class, :insert_into_database)
53
57
  super
54
58
  end
59
+ end
55
60
 
61
+ class CbHasOneParent
62
+ include Mongoid::Document
56
63
  include CallbackTracking
64
+ include RootInsertable
65
+
66
+ has_one :child, autosave: true, class_name: "CbHasOneChild", inverse_of: :parent
57
67
  end
58
68
 
59
69
  class CbHasOneChild
60
70
  include Mongoid::Document
71
+ include CallbackTracking
61
72
 
62
73
  belongs_to :parent, class_name: "CbHasOneParent", inverse_of: :child
63
-
64
- def initialize(callback_registry)
65
- @callback_registry = callback_registry
66
- super()
67
- end
68
-
69
- attr_accessor :callback_registry
70
-
71
- include CallbackTracking
72
74
  end
73
75
 
74
76
  class CbHasManyParent
75
77
  include Mongoid::Document
78
+ include CallbackTracking
79
+ include RootInsertable
76
80
 
77
81
  has_many :children, autosave: true, class_name: "CbHasManyChild", inverse_of: :parent
78
-
79
- def initialize(callback_registry)
80
- @callback_registry = callback_registry
81
- super()
82
- end
83
-
84
- attr_accessor :callback_registry
85
-
86
- def insert_as_root
87
- @callback_registry&.record_call(self.class, :insert_into_database)
88
- super
89
- end
90
-
91
- include CallbackTracking
92
82
  end
93
83
 
94
84
  class CbHasManyChild
95
85
  include Mongoid::Document
86
+ include CallbackTracking
96
87
 
97
88
  belongs_to :parent, class_name: "CbHasManyParent", inverse_of: :children
98
-
99
- def initialize(callback_registry)
100
- @callback_registry = callback_registry
101
- super()
102
- end
103
-
104
- attr_accessor :callback_registry
105
-
106
- include CallbackTracking
107
89
  end
108
90
 
109
91
  class CbEmbedsOneParent
110
92
  include Mongoid::Document
93
+ include CallbackTracking
94
+ include RootInsertable
111
95
 
112
96
  field :name
113
97
 
114
98
  embeds_one :child, cascade_callbacks: true, class_name: "CbEmbedsOneChild", inverse_of: :parent
115
-
116
- def initialize(callback_registry)
117
- @callback_registry = callback_registry
118
- super()
119
- end
120
-
121
- attr_accessor :callback_registry
122
-
123
- def insert_as_root
124
- @callback_registry&.record_call(self.class, :insert_into_database)
125
- super
126
- end
127
-
128
- include CallbackTracking
129
99
  end
130
100
 
131
101
  class CbEmbedsOneChild
132
102
  include Mongoid::Document
103
+ include CallbackTracking
133
104
 
134
105
  field :age
135
106
 
136
107
  embedded_in :parent, class_name: "CbEmbedsOneParent", inverse_of: :child
137
-
138
- def initialize(callback_registry)
139
- @callback_registry = callback_registry
140
- super()
141
- end
142
-
143
- attr_accessor :callback_registry
144
-
145
- include CallbackTracking
146
108
  end
147
109
 
148
110
  class CbEmbedsManyParent
149
111
  include Mongoid::Document
112
+ include CallbackTracking
113
+ include RootInsertable
150
114
 
151
115
  embeds_many :children, cascade_callbacks: true, class_name: "CbEmbedsManyChild", inverse_of: :parent
152
-
153
- def initialize(callback_registry)
154
- @callback_registry = callback_registry
155
- super()
156
- end
157
-
158
- attr_accessor :callback_registry
159
-
160
- def insert_as_root
161
- @callback_registry&.record_call(self.class, :insert_into_database)
162
- super
163
- end
164
-
165
- include CallbackTracking
166
116
  end
167
117
 
168
118
  class CbEmbedsManyChild
169
119
  include Mongoid::Document
120
+ include CallbackTracking
170
121
 
171
122
  embedded_in :parent, class_name: "CbEmbedsManyParent", inverse_of: :children
172
-
173
- def initialize(callback_registry)
174
- @callback_registry = callback_registry
175
- super()
176
- end
177
-
178
- attr_accessor :callback_registry
179
-
180
- include CallbackTracking
181
123
  end
182
124
 
183
125
  class CbParent
184
126
  include Mongoid::Document
185
-
186
- def initialize(callback_registry)
187
- @callback_registry = callback_registry
188
- super()
189
- end
190
-
191
- attr_accessor :callback_registry
127
+ include CallbackTracking
192
128
 
193
129
  embeds_many :cb_children
194
130
  embeds_many :cb_cascaded_children, cascade_callbacks: true
195
-
196
- include CallbackTracking
131
+ embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
197
132
  end
198
133
 
199
134
  class CbChild
200
135
  include Mongoid::Document
136
+ include CallbackTracking
201
137
 
202
138
  embedded_in :cb_parent
203
-
204
- def initialize(callback_registry, options)
205
- @callback_registry = callback_registry
206
- super(options)
207
- end
208
-
209
- attr_accessor :callback_registry
210
-
211
- include CallbackTracking
212
139
  end
213
140
 
214
141
  class CbCascadedChild
215
142
  include Mongoid::Document
143
+ include CallbackTracking
216
144
 
217
145
  embedded_in :cb_parent
218
146
 
219
- def initialize(callback_registry, options)
220
- @callback_registry = callback_registry
221
- super(options)
222
- end
147
+ before_save :test_mongoid_state
223
148
 
224
- attr_accessor :callback_registry
149
+ private
150
+
151
+ # Helps test that cascading child callbacks have access to the Mongoid
152
+ # state objects; if the implementation uses fiber-local (instead of truly
153
+ # thread-local) variables, the related tests will fail because the
154
+ # cascading child callbacks use fibers to linearize the recursion.
155
+ def test_mongoid_state
156
+ Mongoid::Threaded.stack('interceptable').push(self)
157
+ end
158
+ end
225
159
 
160
+ class CbCascadedNode
161
+ include Mongoid::Document
226
162
  include CallbackTracking
163
+
164
+ embedded_in :parent, polymorphic: true
165
+
166
+ embeds_many :cb_cascaded_nodes, cascade_callbacks: true, as: :parent
227
167
  end
228
168
  end
229
169
 
@@ -528,13 +528,13 @@ describe Mongoid::Serializable do
528
528
  end
529
529
 
530
530
  it "includes the first relation" do
531
- expect(relation_hash[0]).to include
532
- { "_id" => "kudamm", "street" => "Kudamm" }
531
+ expect(relation_hash[0]).to include(
532
+ { "_id" => "kudamm", "street" => "Kudamm" })
533
533
  end
534
534
 
535
535
  it "includes the second relation" do
536
- expect(relation_hash[1]).to include
537
- { "_id" => "tauentzienstr", "street" => "Tauentzienstr" }
536
+ expect(relation_hash[1]).to include(
537
+ { "_id" => "tauentzienstr", "street" => "Tauentzienstr" })
538
538
  end
539
539
  end
540
540
 
@@ -545,13 +545,13 @@ describe Mongoid::Serializable do
545
545
  end
546
546
 
547
547
  it "includes the first relation" do
548
- expect(relation_hash[0]).to include
549
- { "_id" => "kudamm", "street" => "Kudamm" }
548
+ expect(relation_hash[0]).to include(
549
+ { "_id" => "kudamm", "street" => "Kudamm" })
550
550
  end
551
551
 
552
552
  it "includes the second relation" do
553
- expect(relation_hash[1]).to include
554
- { "_id" => "tauentzienstr", "street" => "Tauentzienstr" }
553
+ expect(relation_hash[1]).to include(
554
+ { "_id" => "tauentzienstr", "street" => "Tauentzienstr" })
555
555
  end
556
556
  end
557
557
 
@@ -670,8 +670,8 @@ describe Mongoid::Serializable do
670
670
  end
671
671
 
672
672
  it "includes the specified relation" do
673
- expect(relation_hash).to include
674
- { "_id" => "leo-marvin", "first_name" => "Leo", "last_name" => "Marvin" }
673
+ expect(relation_hash).to include(
674
+ { "_id" => "Leo-Marvin", "first_name" => "Leo", "last_name" => "Marvin" })
675
675
  end
676
676
  end
677
677
 
@@ -682,8 +682,8 @@ describe Mongoid::Serializable do
682
682
  end
683
683
 
684
684
  it "includes the specified relation" do
685
- expect(relation_hash).to include
686
- { "_id" => "leo-marvin", "first_name" => "Leo", "last_name" => "Marvin" }
685
+ expect(relation_hash).to include(
686
+ { "_id" => "Leo-Marvin", "first_name" => "Leo", "last_name" => "Marvin" })
687
687
  end
688
688
  end
689
689
 
@@ -694,8 +694,8 @@ describe Mongoid::Serializable do
694
694
  end
695
695
 
696
696
  it "includes the specified relation sans exceptions" do
697
- expect(relation_hash).to include
698
- { "first_name" => "Leo", "last_name" => "Marvin" }
697
+ expect(relation_hash).to include(
698
+ { "first_name" => "Leo", "last_name" => "Marvin" })
699
699
  end
700
700
  end
701
701
  end
@@ -43,4 +43,27 @@ describe Mongoid::Timestamps::Created do
43
43
  expect(quiz.created_at).to be_within(10).of(Time.now.utc)
44
44
  end
45
45
  end
46
+
47
+ context "when the document is destroyed" do
48
+ let(:book) do
49
+ Book.create!
50
+ end
51
+
52
+ before do
53
+ Cover.before_save do
54
+ destroy if title == "delete me"
55
+ end
56
+ end
57
+
58
+ after do
59
+ Cover.reset_callbacks(:save)
60
+ end
61
+
62
+ it "does not set the created_at timestamp" do
63
+ book.covers << Cover.new(title: "delete me")
64
+ expect {
65
+ book.save
66
+ }.not_to raise_error
67
+ end
68
+ end
46
69
  end