paper_trail 6.0.2 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +20 -0
- data/.rubocop.yml +30 -2
- data/.rubocop_todo.yml +20 -0
- data/.travis.yml +3 -5
- data/Appraisals +5 -6
- data/CHANGELOG.md +33 -0
- data/README.md +43 -81
- data/Rakefile +1 -1
- data/doc/bug_report_template.rb +4 -2
- data/gemfiles/ar_4.0.gemfile +7 -0
- data/gemfiles/ar_4.2.gemfile +0 -1
- data/lib/generators/paper_trail/templates/create_version_associations.rb +1 -1
- data/lib/generators/paper_trail/templates/create_versions.rb +1 -1
- data/lib/paper_trail.rb +7 -9
- data/lib/paper_trail/config.rb +0 -15
- data/lib/paper_trail/frameworks/rspec.rb +8 -2
- data/lib/paper_trail/model_config.rb +6 -2
- data/lib/paper_trail/record_trail.rb +3 -1
- data/lib/paper_trail/reifier.rb +43 -354
- data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
- data/lib/paper_trail/reifiers/has_many.rb +110 -0
- data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
- data/lib/paper_trail/reifiers/has_one.rb +76 -0
- data/lib/paper_trail/serializers/yaml.rb +2 -25
- data/lib/paper_trail/version_concern.rb +5 -5
- data/lib/paper_trail/version_number.rb +7 -3
- data/paper_trail.gemspec +7 -34
- data/spec/controllers/articles_controller_spec.rb +1 -1
- data/spec/generators/install_generator_spec.rb +40 -34
- data/spec/models/animal_spec.rb +50 -25
- data/spec/models/boolit_spec.rb +8 -7
- data/spec/models/callback_modifier_spec.rb +13 -13
- data/spec/models/document_spec.rb +21 -0
- data/spec/models/gadget_spec.rb +35 -39
- data/spec/models/joined_version_spec.rb +4 -4
- data/spec/models/json_version_spec.rb +14 -15
- data/spec/models/not_on_update_spec.rb +1 -1
- data/spec/models/post_with_status_spec.rb +2 -2
- data/spec/models/skipper_spec.rb +4 -4
- data/spec/models/thing_spec.rb +1 -1
- data/spec/models/truck_spec.rb +1 -1
- data/spec/models/vehicle_spec.rb +1 -1
- data/spec/models/version_spec.rb +152 -168
- data/spec/models/widget_spec.rb +170 -196
- data/spec/modules/paper_trail_spec.rb +3 -3
- data/spec/modules/version_concern_spec.rb +5 -8
- data/spec/modules/version_number_spec.rb +11 -36
- data/spec/paper_trail/cleaner_spec.rb +152 -0
- data/spec/paper_trail/config_spec.rb +1 -1
- data/spec/paper_trail/serializers/custom_yaml_serializer_spec.rb +45 -0
- data/spec/paper_trail/serializers/json_spec.rb +57 -0
- data/spec/paper_trail/version_limit_spec.rb +55 -0
- data/spec/paper_trail_spec.rb +45 -32
- data/spec/requests/articles_spec.rb +4 -4
- data/test/dummy/app/models/custom_primary_key_record.rb +4 -2
- data/test/dummy/app/models/document.rb +1 -1
- data/test/dummy/app/models/not_on_update.rb +1 -1
- data/test/dummy/app/models/on/create.rb +6 -0
- data/test/dummy/app/models/on/destroy.rb +6 -0
- data/test/dummy/app/models/on/empty_array.rb +6 -0
- data/test/dummy/app/models/on/update.rb +6 -0
- data/test/dummy/app/models/person.rb +1 -0
- data/test/dummy/app/models/song.rb +19 -28
- data/test/dummy/config/application.rb +10 -43
- data/test/dummy/config/routes.rb +1 -1
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +25 -51
- data/test/dummy/db/schema.rb +29 -19
- data/test/test_helper.rb +0 -16
- data/test/unit/associations_test.rb +81 -81
- data/test/unit/model_test.rb +48 -131
- data/test/unit/serializer_test.rb +34 -45
- data/test/unit/serializers/mixin_json_test.rb +3 -1
- data/test/unit/serializers/yaml_test.rb +1 -5
- metadata +44 -19
- data/lib/paper_trail/frameworks/sinatra.rb +0 -40
- data/test/functional/modular_sinatra_test.rb +0 -46
- data/test/functional/sinatra_test.rb +0 -51
- data/test/unit/cleaner_test.rb +0 -151
- data/test/unit/inheritance_column_test.rb +0 -41
- data/test/unit/serializers/json_test.rb +0 -95
- data/test/unit/serializers/mixin_yaml_test.rb +0 -53
data/spec/models/widget_spec.rb
CHANGED
@@ -38,65 +38,63 @@ describe Widget, type: :model do
|
|
38
38
|
|
39
39
|
describe "versioning option" do
|
40
40
|
context "enabled", versioning: true do
|
41
|
-
it "
|
41
|
+
it "enables versioning" do
|
42
42
|
expect(widget.versions.size).to eq(1)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
46
|
context "disabled (default)" do
|
47
|
-
it "
|
47
|
+
it "does not enable versioning" do
|
48
48
|
expect(widget.versions.size).to eq(0)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
53
|
describe "Callbacks", versioning: true do
|
54
|
-
describe
|
55
|
-
|
56
|
-
before { widget.update_attributes!(name: "Foobar") }
|
54
|
+
describe "before_save" do
|
55
|
+
before { widget.update_attributes!(name: "Foobar") }
|
57
56
|
|
58
|
-
|
57
|
+
subject { widget.versions.last.reify }
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
59
|
+
it "resets value for timestamp attrs for update so that value gets updated properly" do
|
60
|
+
# Travel 1 second because MySQL lacks sub-second resolution
|
61
|
+
Timecop.travel(1) do
|
62
|
+
expect { subject.save! }.to change(subject, :updated_at)
|
65
63
|
end
|
66
64
|
end
|
67
65
|
end
|
68
66
|
|
69
|
-
describe
|
67
|
+
describe "after_create" do
|
70
68
|
let(:widget) { Widget.create!(name: "Foobar", created_at: Time.now - 1.week) }
|
71
69
|
|
72
|
-
it "corresponding version
|
70
|
+
it "corresponding version uses the widget's `updated_at`" do
|
73
71
|
expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
|
74
72
|
end
|
75
73
|
end
|
76
74
|
|
77
|
-
describe
|
75
|
+
describe "after_update" do
|
78
76
|
before { widget.update_attributes!(name: "Foobar", updated_at: Time.now + 1.week) }
|
79
77
|
|
80
78
|
subject { widget.versions.last.reify }
|
81
79
|
|
82
80
|
it { expect(subject.paper_trail).not_to be_live }
|
83
81
|
|
84
|
-
it "
|
82
|
+
it "clears the `versions_association_name` virtual attribute" do
|
85
83
|
subject.save!
|
86
84
|
expect(subject.paper_trail).to be_live
|
87
85
|
end
|
88
86
|
|
89
|
-
it "corresponding version
|
87
|
+
it "corresponding version uses the widget updated_at" do
|
90
88
|
expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
|
91
89
|
end
|
92
90
|
end
|
93
91
|
|
94
|
-
describe
|
95
|
-
it "
|
92
|
+
describe "after_destroy" do
|
93
|
+
it "creates a version for that event" do
|
96
94
|
expect { widget.destroy }.to change(widget.versions, :count).by(1)
|
97
95
|
end
|
98
96
|
|
99
|
-
it "
|
97
|
+
it "assigns the version into the `versions_association_name`" do
|
100
98
|
expect(widget.version).to be_nil
|
101
99
|
widget.destroy
|
102
100
|
expect(widget.version).not_to be_nil
|
@@ -104,7 +102,7 @@ describe Widget, type: :model do
|
|
104
102
|
end
|
105
103
|
end
|
106
104
|
|
107
|
-
describe
|
105
|
+
describe "after_rollback" do
|
108
106
|
let(:rolled_back_name) { "Big Moo" }
|
109
107
|
|
110
108
|
before do
|
@@ -122,19 +120,19 @@ describe Widget, type: :model do
|
|
122
120
|
|
123
121
|
it "does not create an event for changes that did not happen" do
|
124
122
|
widget.versions.map(&:changeset).each do |changeset|
|
125
|
-
expect(changeset.fetch("name", [])).
|
123
|
+
expect(changeset.fetch("name", [])).not_to include(rolled_back_name)
|
126
124
|
end
|
127
125
|
end
|
128
126
|
|
129
127
|
it "has not yet loaded the assocation" do
|
130
|
-
expect(widget.versions).
|
128
|
+
expect(widget.versions).not_to be_loaded
|
131
129
|
end
|
132
130
|
end
|
133
131
|
end
|
134
132
|
|
135
133
|
describe "Association", versioning: true do
|
136
134
|
describe "sort order" do
|
137
|
-
it "
|
135
|
+
it "sorts by the timestamp order from the `VersionConcern`" do
|
138
136
|
expect(widget.versions.to_sql).to eq(
|
139
137
|
widget.versions.reorder(PaperTrail::Version.timestamp_sort_order).to_sql
|
140
138
|
)
|
@@ -144,7 +142,7 @@ describe Widget, type: :model do
|
|
144
142
|
|
145
143
|
if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
|
146
144
|
describe "IdentityMap", versioning: true do
|
147
|
-
it "
|
145
|
+
it "does not clobber the IdentityMap when reifying" do
|
148
146
|
widget.update_attributes name: "Henry", created_at: Time.now - 1.day
|
149
147
|
widget.update_attributes name: "Harry"
|
150
148
|
expect(ActiveRecord::IdentityMap).to receive(:without).once
|
@@ -153,204 +151,180 @@ describe Widget, type: :model do
|
|
153
151
|
end
|
154
152
|
end
|
155
153
|
|
156
|
-
describe "
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
describe "#destroy" do
|
166
|
-
it "creates a version record" do
|
167
|
-
widget = Widget.create
|
168
|
-
assert_equal 1, widget.versions.length
|
169
|
-
widget.destroy
|
170
|
-
versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
|
171
|
-
assert_equal 2, versions_for_widget.length
|
172
|
-
end
|
173
|
-
|
174
|
-
it "can have multiple destruction records" do
|
175
|
-
versions = lambda { |widget|
|
176
|
-
# Workaround for AR 3. When we drop AR 3 support, we can simply use
|
177
|
-
# the `widget.versions` association, instead of `with_item_keys`.
|
178
|
-
PaperTrail::Version.with_item_keys("Widget", widget.id)
|
179
|
-
}
|
180
|
-
widget = Widget.create
|
181
|
-
assert_equal 1, widget.versions.length
|
182
|
-
widget.destroy
|
183
|
-
assert_equal 2, versions.call(widget).length
|
184
|
-
widget = widget.version.reify
|
185
|
-
widget.save
|
186
|
-
assert_equal 3, versions.call(widget).length
|
187
|
-
widget.destroy
|
188
|
-
assert_equal 4, versions.call(widget).length
|
189
|
-
assert_equal 2, versions.call(widget).where(event: "destroy").length
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
describe "#paper_trail.originator" do
|
194
|
-
describe "return value" do
|
195
|
-
let(:orig_name) { FFaker::Name.name }
|
196
|
-
let(:new_name) { FFaker::Name.name }
|
197
|
-
before { PaperTrail.whodunnit = orig_name }
|
198
|
-
|
199
|
-
context "accessed from live model instance" do
|
200
|
-
specify { expect(widget.paper_trail).to be_live }
|
201
|
-
|
202
|
-
it "should return the originator for the model at a given state" do
|
203
|
-
expect(widget.paper_trail.originator).to eq(orig_name)
|
204
|
-
widget.paper_trail.whodunnit(new_name) { |w|
|
205
|
-
w.update_attributes(name: "Elizabeth")
|
206
|
-
}
|
207
|
-
expect(widget.paper_trail.originator).to eq(new_name)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
context "accessed from a reified model instance" do
|
212
|
-
before do
|
213
|
-
widget.update_attributes(name: "Andy")
|
214
|
-
PaperTrail.whodunnit = new_name
|
215
|
-
widget.update_attributes(name: "Elizabeth")
|
216
|
-
end
|
217
|
-
|
218
|
-
context "default behavior (no `options[:dup]` option passed in)" do
|
219
|
-
let(:reified_widget) { widget.versions[1].reify }
|
220
|
-
|
221
|
-
it "should return the appropriate originator" do
|
222
|
-
expect(reified_widget.paper_trail.originator).to eq(orig_name)
|
223
|
-
end
|
154
|
+
describe "#create", versioning: true do
|
155
|
+
it "creates a version record" do
|
156
|
+
wordget = Widget.create
|
157
|
+
assert_equal 1, wordget.versions.length
|
158
|
+
end
|
159
|
+
end
|
224
160
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
161
|
+
describe "#destroy", versioning: true do
|
162
|
+
it "creates a version record" do
|
163
|
+
widget = Widget.create
|
164
|
+
assert_equal 1, widget.versions.length
|
165
|
+
widget.destroy
|
166
|
+
versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
|
167
|
+
assert_equal 2, versions_for_widget.length
|
168
|
+
end
|
229
169
|
|
230
|
-
|
231
|
-
|
170
|
+
it "can have multiple destruction records" do
|
171
|
+
versions = lambda { |widget|
|
172
|
+
# Workaround for AR 3. When we drop AR 3 support, we can simply use
|
173
|
+
# the `widget.versions` association, instead of `with_item_keys`.
|
174
|
+
PaperTrail::Version.with_item_keys("Widget", widget.id)
|
175
|
+
}
|
176
|
+
widget = Widget.create
|
177
|
+
assert_equal 1, widget.versions.length
|
178
|
+
widget.destroy
|
179
|
+
assert_equal 2, versions.call(widget).length
|
180
|
+
widget = widget.version.reify
|
181
|
+
widget.save
|
182
|
+
assert_equal 3, versions.call(widget).length
|
183
|
+
widget.destroy
|
184
|
+
assert_equal 4, versions.call(widget).length
|
185
|
+
assert_equal 2, versions.call(widget).where(event: "destroy").length
|
186
|
+
end
|
187
|
+
end
|
232
188
|
|
233
|
-
|
234
|
-
|
235
|
-
|
189
|
+
describe "#paper_trail.originator", versioning: true do
|
190
|
+
describe "return value" do
|
191
|
+
let(:orig_name) { FFaker::Name.name }
|
192
|
+
let(:new_name) { FFaker::Name.name }
|
236
193
|
|
237
|
-
|
238
|
-
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
194
|
+
before do
|
195
|
+
PaperTrail.whodunnit = orig_name
|
243
196
|
end
|
244
197
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
end
|
198
|
+
it "returns the originator for the model at a given state" do
|
199
|
+
expect(widget.paper_trail).to be_live
|
200
|
+
expect(widget.paper_trail.originator).to eq(orig_name)
|
201
|
+
widget.paper_trail.whodunnit(new_name) { |w|
|
202
|
+
w.update_attributes(name: "Elizabeth")
|
203
|
+
}
|
204
|
+
expect(widget.paper_trail.originator).to eq(new_name)
|
253
205
|
end
|
254
206
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
207
|
+
it "returns the appropriate originator" do
|
208
|
+
widget.update_attributes(name: "Andy")
|
209
|
+
PaperTrail.whodunnit = new_name
|
210
|
+
widget.update_attributes(name: "Elizabeth")
|
211
|
+
reified_widget = widget.versions[1].reify
|
212
|
+
expect(reified_widget.paper_trail.originator).to eq(orig_name)
|
213
|
+
expect(reified_widget).not_to be_new_record
|
214
|
+
end
|
263
215
|
|
264
|
-
|
265
|
-
|
266
|
-
|
216
|
+
it "can create a new instance with options[:dup]" do
|
217
|
+
widget.update_attributes(name: "Andy")
|
218
|
+
PaperTrail.whodunnit = new_name
|
219
|
+
widget.update_attributes(name: "Elizabeth")
|
220
|
+
reified_widget = widget.versions[1].reify(dup: true)
|
221
|
+
expect(reified_widget.paper_trail.originator).to eq(orig_name)
|
222
|
+
expect(reified_widget).to be_new_record
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
267
226
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
227
|
+
describe "#version_at", versioning: true do
|
228
|
+
context "Timestamp argument is AFTER object has been destroyed" do
|
229
|
+
it "returns nil" do
|
230
|
+
widget.update_attribute(:name, "foobar")
|
231
|
+
widget.destroy
|
232
|
+
expect(widget.paper_trail.version_at(Time.now)).to be_nil
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
272
236
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
237
|
+
describe "#whodunnit", versioning: true do
|
238
|
+
context "no block given" do
|
239
|
+
it "raises an error" do
|
240
|
+
expect {
|
241
|
+
widget.paper_trail.whodunnit("Ben")
|
242
|
+
}.to raise_error(ArgumentError, "expected to receive a block")
|
243
|
+
end
|
244
|
+
end
|
280
245
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
w.update_attributes(name: "Elizabeth")
|
285
|
-
}
|
286
|
-
expect(PaperTrail.whodunnit).to eq(orig_name)
|
287
|
-
end
|
288
|
-
end
|
246
|
+
context "block given" do
|
247
|
+
let(:orig_name) { FFaker::Name.name }
|
248
|
+
let(:new_name) { FFaker::Name.name }
|
289
249
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
widget.paper_trail.whodunnit(new_name) { raise }
|
294
|
-
}.to raise_error(RuntimeError)
|
295
|
-
expect(PaperTrail.whodunnit).to eq(orig_name)
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
250
|
+
before do
|
251
|
+
PaperTrail.whodunnit = orig_name
|
252
|
+
expect(widget.versions.last.whodunnit).to eq(orig_name) # persist `widget`
|
299
253
|
end
|
300
254
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
Timecop.travel(1) do
|
306
|
-
widget.paper_trail.touch_with_version
|
307
|
-
end
|
308
|
-
expect(widget.versions.size).to eq(count + 1)
|
255
|
+
it "modifies value of `PaperTrail.whodunnit` while executing the block" do
|
256
|
+
widget.paper_trail.whodunnit(new_name) do
|
257
|
+
expect(PaperTrail.whodunnit).to eq(new_name)
|
258
|
+
widget.update_attributes(name: "Elizabeth")
|
309
259
|
end
|
260
|
+
expect(widget.versions.last.whodunnit).to eq(new_name)
|
261
|
+
end
|
310
262
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
end
|
317
|
-
expect(widget.updated_at).to be > time_was
|
318
|
-
end
|
263
|
+
it "reverts value of whodunnit to previous value after executing the block" do
|
264
|
+
widget.paper_trail.whodunnit(new_name) { |w|
|
265
|
+
w.update_attributes(name: "Elizabeth")
|
266
|
+
}
|
267
|
+
expect(PaperTrail.whodunnit).to eq(orig_name)
|
319
268
|
end
|
320
269
|
|
321
|
-
|
322
|
-
|
323
|
-
widget
|
324
|
-
|
325
|
-
|
326
|
-
assert_equal 2, widget.versions.length
|
327
|
-
end
|
270
|
+
it "reverts to previous value, even if error within block" do
|
271
|
+
expect {
|
272
|
+
widget.paper_trail.whodunnit(new_name) { raise }
|
273
|
+
}.to raise_error(RuntimeError)
|
274
|
+
expect(PaperTrail.whodunnit).to eq(orig_name)
|
328
275
|
end
|
329
276
|
end
|
277
|
+
end
|
330
278
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
279
|
+
describe "#touch_with_version", versioning: true do
|
280
|
+
it "creates a version" do
|
281
|
+
count = widget.versions.size
|
282
|
+
# Travel 1 second because MySQL lacks sub-second resolution
|
283
|
+
Timecop.travel(1) do
|
284
|
+
widget.paper_trail.touch_with_version
|
336
285
|
end
|
286
|
+
expect(widget.versions.size).to eq(count + 1)
|
287
|
+
end
|
337
288
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
end
|
289
|
+
it "increments the `:updated_at` timestamp" do
|
290
|
+
time_was = widget.updated_at
|
291
|
+
# Travel 1 second because MySQL lacks sub-second resolution
|
292
|
+
Timecop.travel(1) do
|
293
|
+
widget.paper_trail.touch_with_version
|
344
294
|
end
|
295
|
+
expect(widget.updated_at).to be > time_was
|
296
|
+
end
|
297
|
+
end
|
345
298
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
299
|
+
describe "#update", versioning: true do
|
300
|
+
it "creates a version record" do
|
301
|
+
widget = Widget.create
|
302
|
+
assert_equal 1, widget.versions.length
|
303
|
+
widget.update_attributes(name: "Bugle")
|
304
|
+
assert_equal 2, widget.versions.length
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
describe ".paper_trail.enabled?" do
|
309
|
+
it "returns true" do
|
310
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe ".disable" do
|
315
|
+
it "sets the `paper_trail.enabled?` to `false`" do
|
316
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
317
|
+
Widget.paper_trail.disable
|
318
|
+
expect(Widget.paper_trail.enabled?).to eq(false)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe ".enable" do
|
323
|
+
it "sets the `paper_trail.enabled?` to `true`" do
|
324
|
+
Widget.paper_trail.disable
|
325
|
+
expect(Widget.paper_trail.enabled?).to eq(false)
|
326
|
+
Widget.paper_trail.enable
|
327
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
354
328
|
end
|
355
329
|
end
|
356
330
|
end
|