pod4 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/pod4/model.rb CHANGED
@@ -9,7 +9,7 @@ module Pod4
9
9
 
10
10
 
11
11
  ##
12
- # The parent of all models.
12
+ # The parent of all CRUDL models.
13
13
  #
14
14
  # Models & Interfaces
15
15
  # -------------------
@@ -21,7 +21,7 @@ module Pod4
21
21
  # not. The model doesn't care about where the data comes from. Models are all subclasses of
22
22
  # Pod4::Model.
23
23
  #
24
- # An interface encapsulates the connection to whatever is providing the data.# it might be a
24
+ # An interface encapsulates the connection to whatever is providing the data. It might be a
25
25
  # wrapper for calls to the Sequel ORM, for example. Or it could be a making a series of calls to
26
26
  # a set of Nebulous verbs. It only cares about dealing with the data source, and it is only
27
27
  # called by the model.
@@ -91,7 +91,6 @@ module Pod4
91
91
  attr_accessor *cols
92
92
  end
93
93
 
94
-
95
94
  ##
96
95
  # Returns the list of columns from attr_columns
97
96
  #
@@ -99,7 +98,6 @@ module Pod4
99
98
  []
100
99
  end
101
100
 
102
-
103
101
  ##
104
102
  # Call this to return an array of record information.
105
103
  #
@@ -107,12 +105,17 @@ module Pod4
107
105
  # ID in each array element.
108
106
  #
109
107
  # For the purposes of Model we assume that we can make an instance out of each array element,
110
- # and we return an array of instances of the model.# Override this method if that is not true
108
+ # and we return an array of instances of the model. Override this method if that is not true
111
109
  # for your Interface.
112
110
  #
113
111
  # Note that list should ALWAYS return an array, and array elements should always respond to
114
112
  # :id -- otherwise we raise a Pod4Error.
115
113
  #
114
+ # Note also that while list returns an array of model objects, `read` has _not_ been run
115
+ # against each object. The data is there, but @model_status == :empty, and validation has not
116
+ # been run. This is partly for the sake of efficiency, partly to help avoid recursive loops
117
+ # in validation.
118
+ #
116
119
  def list(params=nil)
117
120
  fail_no_id_fld unless interface.id_fld
118
121
 
@@ -123,37 +126,29 @@ module Pod4
123
126
  rec.map_to_model(ot) # seperately, in case model forgot to return self
124
127
  rec
125
128
  end
126
-
127
129
  end
128
130
 
129
-
130
131
  def test_for_octo(param)
131
132
  raise( ArgumentError, 'Parameter must be a Hash or Octothorpe', caller ) \
132
133
  unless param.kind_of?(Hash) || param.kind_of?(Octothorpe)
133
134
 
134
135
  end
135
136
 
136
-
137
137
  def test_for_invalid_status(action, status)
138
138
  raise( Pod4Error, "Invalid model status for an action of #{action}", caller ) \
139
139
  if [:empty, :deleted].include? status
140
140
 
141
141
  end
142
142
 
143
-
144
-
145
143
  def fail_no_id_fld
146
144
  raise Pod4Error, "No ID field defined in interface", caller
147
145
  end
148
146
 
149
-
150
147
  def fail_no_id
151
148
  raise Pod4Error, "ID field missing from record", caller
152
149
  end
153
150
 
154
- end
155
- ##
156
-
151
+ end # of class << self
157
152
 
158
153
  ##
159
154
  # Syntactic sugar; pretty much the same as self.class.columns, which returns the `attr_columns`
@@ -161,21 +156,21 @@ module Pod4
161
156
  #
162
157
  def columns; self.class.columns.dup; end
163
158
 
164
-
165
159
  ##
166
160
  # Call this to write a new record to the data source.
167
161
  #
168
162
  # Note: create needs to set @id. But interface.create should return it, so that's okay.
169
163
  #
170
164
  def create
171
- validate
165
+ run_validation(:create)
172
166
  @model_id = interface.create(map_to_interface) unless @model_status == :error
173
167
 
174
168
  @model_status = :okay if @model_status == :empty
175
169
  self
170
+ rescue Pod4::WeakError
171
+ add_alert(:error, $!)
176
172
  end
177
173
 
178
-
179
174
  ##
180
175
  # Call this to fetch the data for this instance from the data source
181
176
  #
@@ -186,26 +181,29 @@ module Pod4
186
181
  add_alert(:error, "Record ID '#@model_id' not found on the data source")
187
182
  else
188
183
  map_to_model(r)
184
+ run_validation(:read)
189
185
  @model_status = :okay if @model_status == :empty
190
186
  end
191
187
 
192
188
  self
189
+ rescue Pod4::WeakError
190
+ add_alert(:error, $!)
193
191
  end
194
192
 
195
-
196
193
  ##
197
194
  # Call this to update the data source with the current attribute values
198
195
  #
199
196
  def update
200
197
  Model.test_for_invalid_status(:update, @model_status)
201
198
 
202
- clear_alerts; validate
199
+ clear_alerts; run_validation(:update)
203
200
  interface.update(@model_id, map_to_interface) unless @model_status == :error
204
201
 
205
202
  self
203
+ rescue Pod4::WeakError
204
+ add_alert(:error, $!)
206
205
  end
207
206
 
208
-
209
207
  ##
210
208
  # Call this to delete the record on the data source.
211
209
  #
@@ -213,13 +211,14 @@ module Pod4
213
211
  #
214
212
  def delete
215
213
  Model.test_for_invalid_status(:delete, @model_status)
216
- clear_alerts; validate
214
+ clear_alerts; run_validation(:delete)
217
215
  interface.delete(@model_id)
218
216
  @model_status = :deleted
219
217
  self
218
+ rescue Pod4::WeakError
219
+ add_alert(:error, $!)
220
220
  end
221
221
 
222
-
223
222
  ##
224
223
  # Call this to validate the model.
225
224
  #
@@ -228,14 +227,14 @@ module Pod4
228
227
  # Note that you can only validate what is actually stored on the model. If you want to check
229
228
  # the data being passed to the model in `set`, you need to override that routine.
230
229
  #
231
- # Also, you don't have any way of telling whether you are currently creating a new record or
232
- # updating an old one: override `create` and `update` respectively.
230
+ # You may optionally catch the vmode parameter, which is one of :create,
231
+ # :read, :update, :delete, to have different validation under these circumstances; or you may
232
+ # safely ignore it and override `create`, `read`, `update` or `delete` as you wish.
233
233
  #
234
- def validate
234
+ def validate(vmode=nil)
235
235
  # Holding pattern. All models should use super, in principal
236
236
  end
237
237
 
238
-
239
238
  ##
240
239
  # Set instance values on the model from a Hash or Octothorpe.
241
240
  #
@@ -252,7 +251,6 @@ module Pod4
252
251
  self
253
252
  end
254
253
 
255
-
256
254
  ##
257
255
  # Return an Octothorpe of all the attr_columns attributes.
258
256
  #
@@ -264,7 +262,6 @@ module Pod4
264
262
  Octothorpe.new(to_h)
265
263
  end
266
264
 
267
-
268
265
  ##
269
266
  # Used by the interface to set the column values on the model.
270
267
  #
@@ -278,11 +275,9 @@ module Pod4
278
275
  #
279
276
  def map_to_model(ot)
280
277
  merge(ot)
281
- validate
282
278
  self
283
279
  end
284
280
 
285
-
286
281
  ##
287
282
  # used by the model to get an OT of column values for the interface.
288
283
  #
@@ -302,10 +297,8 @@ module Pod4
302
297
  Octothorpe.new(to_h)
303
298
  end
304
299
 
305
-
306
300
  private
307
301
 
308
-
309
302
  ##
310
303
  # Output a hash of the columns
311
304
  #
@@ -315,7 +308,6 @@ module Pod4
315
308
  end
316
309
  end
317
310
 
318
-
319
311
  ##
320
312
  # Merge an OT with our columns
321
313
  #
@@ -327,8 +319,15 @@ module Pod4
327
319
  end
328
320
  end
329
321
 
330
- end
331
- ##
322
+ ##
323
+ # Call the validate method on the model. Allow the user to override the method with or without
324
+ # the vmode paramter, as they choose.
325
+ #
326
+ def run_validation(vmode)
327
+ method(:validate).arity == 0 ? validate : validate(vmode)
328
+ end
329
+
330
+ end # of Model
332
331
 
333
332
 
334
333
  end
@@ -291,7 +291,8 @@ module Pod4
291
291
  else :response
292
292
  end
293
293
 
294
- raise Pod4::CantContinue, "Nebulous returned an error verb" if @response_status == :verberror
294
+ raise Pod4::WeakError, "Nebulous returned an error verb: #{@response.description}" \
295
+ if @response_status == :verberror
295
296
 
296
297
  self
297
298
 
@@ -79,16 +79,37 @@ module Pod4
79
79
  raise(Pod4Error, 'no call to set_table in the interface definition') if self.class.table.nil?
80
80
  raise(Pod4Error, 'no call to set_id_fld in the interface definition') if self.class.id_fld.nil?
81
81
 
82
- @db = db # referemce to the db object
83
- @table = db[schema ? "#{schema}__#{table}".to_sym : table]
84
- @id_fld = self.class.id_fld
85
-
82
+ @sequel_version = Sequel.respond_to?(:qualify) ? 5 : 4
83
+ @db = db # reference to the db object
84
+ @id_fld = self.class.id_fld
85
+
86
+ @table =
87
+ if schema
88
+ if @sequel_version == 5
89
+ db[ Sequel[schema][table] ]
90
+ else
91
+ db[ "#{schema}__#{table}".to_sym ]
92
+ end
93
+ else
94
+ db[table]
95
+ end
96
+
86
97
  # Work around a problem with jdbc-postgresql where it throws an exception whenever it sees
87
98
  # the money type. This workaround actually allows us to return a BigDecimal, so it's better
88
99
  # than using postgres_pr when under jRuby!
89
100
  if @db.uri =~ /jdbc:postgresql/
90
101
  @db.conversion_procs[790] = ->(s){BigDecimal.new s[1..-1] rescue nil}
91
- Sequel::JDBC::Postgres::Dataset::PG_SPECIFIC_TYPES << Java::JavaSQL::Types::DOUBLE
102
+ c = Sequel::JDBC::Postgres::Dataset
103
+
104
+ if @sequel_version >= 5
105
+ # In Sequel 5 everything is frozen, so some hacking is required.
106
+ # See https://github.com/jeremyevans/sequel/issues/1458
107
+ vals = c::PG_SPECIFIC_TYPES + [Java::JavaSQL::Types::DOUBLE]
108
+ c.send(:remove_const, :PG_SPECIFIC_TYPES) # We can probably get away with just const_set, but.
109
+ c.send(:const_set, :PG_SPECIFIC_TYPES, vals.freeze)
110
+ else
111
+ c::PG_SPECIFIC_TYPES << Java::JavaSQL::Types::DOUBLE
112
+ end
92
113
  end
93
114
 
94
115
  rescue => e
@@ -323,8 +344,11 @@ module Pod4
323
344
  m[k] = v.kind_of?(Symbol) ? v.to_s : v
324
345
  end
325
346
 
347
+ when nil
348
+ nil
349
+
326
350
  else
327
- sel
351
+ fail Pod4::DatabaseError, "Expected a selection hash, got: #{sel.inspect}"
328
352
 
329
353
  end
330
354
 
data/lib/pod4/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pod4
2
- VERSION = '0.8.3'
2
+ VERSION = '0.9.0'
3
3
  end
data/lib/pod4.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'pod4/param'
5
5
  require_relative 'pod4/basic_model'
6
6
  require_relative 'pod4/model'
7
7
  require_relative 'pod4/alert'
8
+ require_relative 'pod4/version'
8
9
 
9
10
 
10
11
 
data/pod4.gemspec CHANGED
@@ -25,6 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.extra_rdoc_files = spec.files.grep(%r{^md/})
26
26
 
27
27
  spec.add_runtime_dependency "devnull", '~>0.1'
28
- spec.add_runtime_dependency "octothorpe", '~>0.3'
28
+ spec.add_runtime_dependency "octothorpe", '~>0.4'
29
29
 
30
30
  end
data/spec/README.md CHANGED
@@ -12,8 +12,3 @@ assumes that the schema names can be hardcoded, and since we create and wipe
12
12
  tables in these tests, you should really have a seperate database for them
13
13
  anyway.)
14
14
 
15
- Sequel
16
- ------
17
- Note that the Sequel ORM adapter does **not** require a database -- we
18
- currently use an in-memory sqlite database instead.
19
-
@@ -158,7 +158,7 @@ describe 'WeirdModel' do
158
158
  lurch = 'Dnhhhhhh'
159
159
  2.times { model.fake_an_alert(:error, :price, lurch) }
160
160
 
161
- expect( model.alerts.size ).to eq 2
161
+ expect( model.alerts.size ).to eq 1
162
162
  end
163
163
 
164
164
  end
@@ -0,0 +1,204 @@
1
+ require 'octothorpe'
2
+
3
+ require 'pod4/model'
4
+ require 'pod4/null_interface'
5
+
6
+
7
+ ##
8
+ # This is purely here to test that model works when you have a validate that accepts the new
9
+ # vmode parameter
10
+ #
11
+ describe 'Customer Model with new validate' do
12
+
13
+ let(:customer_model_class) do
14
+ Class.new Pod4::Model do
15
+ attr_columns :id, :name, :groups
16
+ attr_columns :price # specifically testing multiple calls to attr_columns
17
+ set_interface NullInterface.new(:id, :name, :price, :groups, [])
18
+
19
+ def map_to_model(ot)
20
+ super
21
+ @groups = @groups ? @groups.split(',') : []
22
+ self
23
+ end
24
+
25
+ def map_to_interface
26
+ x = super
27
+ g = (x.>>.groups || []).join(',')
28
+ x.merge(groups: g)
29
+ end
30
+
31
+ def fake_an_alert(*args)
32
+ add_alert(*args) #private method
33
+ end
34
+
35
+ def validate(vmode)
36
+ add_alert(:error, "falling over for mode #{vmode}") if name == "fall over"
37
+ end
38
+
39
+ def reset_alerts; @alerts = []; end
40
+ end
41
+ end
42
+
43
+ let(:records) do
44
+ [ {id: 10, name: 'Gomez', price: 1.23, groups: 'trains' },
45
+ {id: 20, name: 'Morticia', price: 2.34, groups: 'spanish' },
46
+ {id: 30, name: 'Wednesday', price: 3.45, groups: 'school' },
47
+ {id: 40, name: 'Pugsley', price: 4.56, groups: 'trains,school'} ]
48
+
49
+ end
50
+
51
+ let(:records_as_ot) { records.map{|r| Octothorpe.new(r) } }
52
+
53
+ # model is just a plain newly created object that you can call read on.
54
+ # model2 and model3 are in an identical state - they have been filled with a
55
+ # read(). We have two so that we can RSpec 'allow' on one and not the other.
56
+
57
+ let(:model) { customer_model_class.new(20) }
58
+
59
+ let(:model2) do
60
+ m = customer_model_class.new(30)
61
+
62
+ allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[2]) )
63
+ m.read.or_die
64
+ end
65
+
66
+ let(:model3) do
67
+ m = customer_model_class.new(40)
68
+
69
+ allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[3]) )
70
+ m.read.or_die
71
+ end
72
+
73
+ ##
74
+
75
+
76
+ describe '#create' do
77
+
78
+ let (:new_model) { customer_model_class.new }
79
+
80
+ it 'calls validate and passes the parameter' do
81
+ # validation tests arity of the validate method; rspec freaks out. So we can't
82
+ # `expect( new_model ).to receive(:validate)`
83
+
84
+ m = customer_model_class.new
85
+ m.name = "fall over"
86
+ m.create
87
+ expect( m.model_status ).to eq :error
88
+ expect( m.alerts.map(&:message) ).to include( include "create" )
89
+ end
90
+
91
+ it 'calls create on the interface if the record is good' do
92
+ expect( customer_model_class.interface ).to receive(:create)
93
+ customer_model_class.new.create
94
+
95
+ new_model.fake_an_alert(:warning, :name, 'foo')
96
+ expect( new_model.interface ).to receive(:create)
97
+ new_model.create
98
+ end
99
+
100
+ it 'doesnt call create on the interface if the record is bad' do
101
+ new_model.fake_an_alert(:error, :name, 'foo')
102
+ expect( new_model.interface ).not_to receive(:create)
103
+ new_model.create
104
+ end
105
+
106
+ end
107
+ ##
108
+
109
+
110
+ describe '#read' do
111
+
112
+ it 'calls validate and passes the parameter' do
113
+ # again, because rspec is a bit stupid, we can't just `expect(model).to receive(:validate)`
114
+
115
+ allow( model.interface ).
116
+ to receive(:read).
117
+ and_return( records_as_ot.first.merge(name: "fall over") )
118
+
119
+ model.read
120
+ expect( model.model_status ).to eq :error
121
+ expect( model.alerts.map(&:message) ).to include( include "mode read" )
122
+ end
123
+
124
+ end
125
+ ##
126
+
127
+
128
+ describe '#update' do
129
+
130
+ before do
131
+ allow( model2.interface ).
132
+ to receive(:update).
133
+ and_return( model2.interface )
134
+
135
+ end
136
+
137
+ it 'calls validate and passes the parameter' do
138
+ # again, we can't `expect(model2).to receive(:validate)` because we're testing arity there
139
+ model2.name = "fall over"
140
+ model2.update
141
+ expect( model2.model_status ).to eq :error
142
+ expect( model2.alerts.map(&:message) ).to include( include "mode update" )
143
+ end
144
+
145
+ it 'calls update on the interface if the validation passes' do
146
+ expect( model3.interface ).
147
+ to receive(:update).
148
+ and_return( model3.interface )
149
+
150
+ model3.update
151
+ end
152
+
153
+ it 'doesn\'t call update on the interface if the validation fails' do
154
+ expect( model3.interface ).not_to receive(:update)
155
+
156
+ model3.name = "fall over" # triggers validation
157
+ model3.update
158
+ end
159
+
160
+ end
161
+ ##
162
+
163
+
164
+ describe '#delete' do
165
+
166
+ before do
167
+ allow( model2.interface ).
168
+ to receive(:delete).
169
+ and_return( model2.interface )
170
+
171
+ end
172
+
173
+ it 'calls validate and passes the parameter' do
174
+ # again, because rspec can't cope with us testing arity in Pod4::Model, we can't say
175
+ # `expect(model2).to receive(:validate)`. But for delete we are only running validation as a
176
+ # courtesy -- a validation fail does not stop the delete, it just sets alerts. So the model
177
+ # status should be :deleted and not :error
178
+ model2.name = "fall over"
179
+ model2.delete
180
+ expect( model2.alerts.map(&:message) ).to include(include "mode delete")
181
+ end
182
+
183
+ it 'calls delete on the interface if the model status is good' do
184
+ expect( model3.interface ).
185
+ to receive(:delete).
186
+ and_return( model3.interface )
187
+
188
+ model3.delete
189
+ end
190
+
191
+ it 'calls delete on the interface if the model status is bad' do
192
+ expect( model3.interface ).
193
+ to receive(:delete).
194
+ and_return( model3.interface )
195
+
196
+ model3.fake_an_alert(:error, :price, 'qar')
197
+ model3.delete
198
+ end
199
+
200
+ end
201
+ ##
202
+
203
+ end
204
+
@@ -347,15 +347,6 @@ describe 'CustomerModel' do
347
347
  ##
348
348
 
349
349
 
350
- describe '#validate' do
351
- it 'takes no parameters' do
352
- expect{ customer_model_class.new.validate(12) }.to raise_exception ArgumentError
353
- expect{ customer_model_class.new.validate }.not_to raise_exception
354
- end
355
- end
356
- ##
357
-
358
-
359
350
  describe '#set' do
360
351
 
361
352
  let (:ot) { records_as_ot[3] }
@@ -482,8 +473,13 @@ describe 'CustomerModel' do
482
473
  end
483
474
 
484
475
  it 'calls validate' do
485
- expect( new_model ).to receive(:validate)
486
- new_model.create
476
+ # validation tests arity of the validate method; rspec freaks out. So we can't
477
+ # `expect( new_model ).to receive(:validate)`
478
+
479
+ m = customer_model_class.new
480
+ m.name = "fall over"
481
+ m.create
482
+ expect( m.model_status ).to eq :error
487
483
  end
488
484
 
489
485
  it 'calls create on the interface if the record is good' do
@@ -544,6 +540,16 @@ describe 'CustomerModel' do
544
540
  expect{ new_model.create }.not_to raise_error
545
541
  end
546
542
 
543
+ it "creates an alert instead when the interface raises WeakError" do
544
+ allow( new_model.interface ).to receive(:create).and_raise Pod4::WeakError, "foo"
545
+
546
+ new_model.id = 50
547
+ new_model.name = "Lurch"
548
+ expect{ new_model.create }.not_to raise_exception
549
+ expect( new_model.model_status ).to eq :error
550
+ expect( new_model.alerts.map(&:message) ).to include( include "foo" )
551
+ end
552
+
547
553
  end
548
554
  ##
549
555
 
@@ -572,12 +578,14 @@ describe 'CustomerModel' do
572
578
  end
573
579
 
574
580
  it 'calls validate' do
581
+ # again, because rspec is a bit stupid, we can't just `expect(model).to receive(:validate)`
582
+
575
583
  allow( model.interface ).
576
584
  to receive(:read).
577
- and_return( records_as_ot.first )
585
+ and_return( records_as_ot.first.merge(name: "fall over") )
578
586
 
579
- expect( model ).to receive(:validate)
580
587
  model.read
588
+ expect( model.model_status ).to eq :error
581
589
  end
582
590
 
583
591
  it 'sets the attribute columns using map_to_model' do
@@ -629,6 +637,14 @@ describe 'CustomerModel' do
629
637
 
630
638
  end
631
639
 
640
+ it "creates an alert instead when the interface raises WeakError" do
641
+ allow( model.interface ).to receive(:read).and_raise Pod4::WeakError, "foo"
642
+
643
+ expect{ model.read }.not_to raise_exception
644
+ expect( model.model_status ).to eq :error
645
+ expect( model.alerts.map(&:message) ).to include( include "foo" )
646
+ end
647
+
632
648
  end
633
649
  ##
634
650
 
@@ -664,8 +680,10 @@ describe 'CustomerModel' do
664
680
  end
665
681
 
666
682
  it 'calls validate' do
667
- expect( model2 ).to receive(:validate)
683
+ # again, we can't `expect(model2).to receive(:validate)` because we're testing arity there
684
+ model2.name = "fall over"
668
685
  model2.update
686
+ expect( model2.model_status ).to eq :error
669
687
  end
670
688
 
671
689
  it 'calls update on the interface if the validation passes' do
@@ -709,6 +727,13 @@ describe 'CustomerModel' do
709
727
 
710
728
  end
711
729
 
730
+ it "creates an alert instead when the interface raises WeakError" do
731
+ allow( model3.interface ).to receive(:update).and_raise Pod4::WeakError, "foo"
732
+
733
+ expect{ model3.update }.not_to raise_exception
734
+ expect( model3.model_status ).to eq :error
735
+ expect( model3.alerts.map(&:message) ).to include( include "foo" )
736
+ end
712
737
 
713
738
  end
714
739
  ##
@@ -745,8 +770,15 @@ describe 'CustomerModel' do
745
770
  end
746
771
 
747
772
  it 'calls validate' do
748
- expect( model2 ).to receive(:validate)
773
+ # again, because rspec can't cope with us testing arity in Pod4::Model, we can't say
774
+ # `expect(model2).to receive(:validate)`. But for delete we are only running validation as a
775
+ # courtesy -- a validation fail does not stop the delete, it just sets alerts. So the model
776
+ # status should be :deleted and not :error
777
+ model2.name = "fall over"
749
778
  model2.delete
779
+
780
+ # one of the elements of the alerts array should include the word "falling"
781
+ expect( model2.alerts.map(&:message) ).to include(include "falling")
750
782
  end
751
783
 
752
784
  it 'calls delete on the interface if the model status is good' do
@@ -787,6 +819,12 @@ describe 'CustomerModel' do
787
819
  model4.delete
788
820
  end
789
821
 
822
+ it "creates an alert instead when the interface raises WeakError" do
823
+ allow( model3.interface ).to receive(:delete).and_raise Pod4::WeakError, "foo"
824
+
825
+ expect{ model3.delete }.not_to raise_exception
826
+ expect( model3.alerts.map(&:message) ).to include( include "foo" )
827
+ end
790
828
 
791
829
  end
792
830
  ##