exception_handling 0.2.0 → 1.0.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +47 -2
- data/README.md +109 -0
- data/Rakefile +12 -4
- data/exception_handling.gemspec +23 -19
- data/lib/exception_handling/log_stub_error.rb +84 -0
- data/lib/{exception_handling_mailer.rb → exception_handling/mailer.rb} +1 -1
- data/lib/exception_handling/methods.rb +69 -0
- data/lib/exception_handling/testing.rb +65 -0
- data/lib/exception_handling/version.rb +1 -1
- data/lib/exception_handling.rb +127 -91
- data/test/test_helper.rb +55 -2
- data/test/unit/exception_handling/log_error_stub_test.rb +83 -0
- data/test/unit/exception_handling/mailer_test.rb +79 -0
- data/test/unit/exception_handling/methods_test.rb +59 -0
- data/test/unit/exception_handling_test.rb +717 -0
- data/views/exception_handling/mailer/exception_notification.html.erb +4 -1
- metadata +62 -13
- data/README +0 -1
- data/test/exception_handling_test.rb +0 -977
- data/test/mocha_patch.rb +0 -63
@@ -0,0 +1,717 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class ExceptionHandlingTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def dont_stub_log_error
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
module EventMachineStub
|
10
|
+
class << self
|
11
|
+
attr_accessor :block
|
12
|
+
|
13
|
+
def schedule(&block)
|
14
|
+
@block = block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class SmtpClientStub
|
20
|
+
class << self
|
21
|
+
attr_reader :block
|
22
|
+
attr_reader :last_method
|
23
|
+
|
24
|
+
def errback(&block)
|
25
|
+
@block = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_hash
|
29
|
+
@send_hash ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def send(hash)
|
33
|
+
@last_method = :send
|
34
|
+
send_hash.clear
|
35
|
+
send_hash.merge!(hash)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def asend(hash)
|
40
|
+
send(hash)
|
41
|
+
@last_method = :asend
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class SmtpClientErrbackStub < SmtpClientStub
|
48
|
+
end
|
49
|
+
|
50
|
+
context "configuration" do
|
51
|
+
def append_organization_info(data)
|
52
|
+
begin
|
53
|
+
data[:user_details] = {}
|
54
|
+
data[:user_details][:username] = "CaryP"
|
55
|
+
data[:user_details][:organization] = "Invoca Engineering Dept."
|
56
|
+
rescue Exception => e
|
57
|
+
# don't let these out!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def log_error_callback(data, ex)
|
62
|
+
@fail_count += 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def log_error_callback_with_failure(data, ex)
|
66
|
+
raise "this should be rescued"
|
67
|
+
end
|
68
|
+
|
69
|
+
setup do
|
70
|
+
@fail_count = 0
|
71
|
+
end
|
72
|
+
|
73
|
+
should "support a custom_data_hook" do
|
74
|
+
ExceptionHandling.custom_data_hook = method(:append_organization_info)
|
75
|
+
ExceptionHandling.ensure_safe("mooo") { raise "Some BS" }
|
76
|
+
assert_match(/Invoca Engineering Dept./, ActionMailer::Base.deliveries[-1].body.to_s)
|
77
|
+
ExceptionHandling.custom_data_hook = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
should "support a log_error hook" do
|
81
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback)
|
82
|
+
ExceptionHandling.ensure_safe("mooo") { raise "Some BS" }
|
83
|
+
assert_equal 1, @fail_count
|
84
|
+
ExceptionHandling.post_log_error_hook = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
should "support rescue exceptions from a log_error hook" do
|
88
|
+
ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
|
89
|
+
assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some BS" } }
|
90
|
+
assert_equal 0, @fail_count
|
91
|
+
ExceptionHandling.post_log_error_hook = nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "Exception Handling" do
|
96
|
+
setup do
|
97
|
+
ActionMailer::Base.deliveries.clear
|
98
|
+
ExceptionHandling.send(:clear_exception_summary)
|
99
|
+
end
|
100
|
+
|
101
|
+
context "exception filter parsing and loading" do
|
102
|
+
should "happen without an error" do
|
103
|
+
stub(File).mtime { incrementing_mtime }
|
104
|
+
exception_filters = ExceptionHandling.send( :exception_filters )
|
105
|
+
assert( exception_filters.is_a?( ExceptionHandling::ExceptionFilters ) )
|
106
|
+
assert_nothing_raised "Loading the exception filter should not raise" do
|
107
|
+
exception_filters.send :load_file
|
108
|
+
end
|
109
|
+
assert !exception_filters.filtered?( "Scott says unlikely to ever match" )
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "ExceptionHandling.ensure_safe" do
|
114
|
+
should "log an exception if an exception is raised." do
|
115
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/)
|
116
|
+
ExceptionHandling.ensure_safe { raise ArgumentError.new("blah") }
|
117
|
+
end
|
118
|
+
|
119
|
+
should "should not log an exception if an exception is not raised." do
|
120
|
+
dont_allow(ExceptionHandling.logger).fatal
|
121
|
+
ExceptionHandling.ensure_safe { ; }
|
122
|
+
end
|
123
|
+
|
124
|
+
should "return its value if used during an assignment" do
|
125
|
+
dont_allow(ExceptionHandling.logger).fatal
|
126
|
+
b = ExceptionHandling.ensure_safe { 5 }
|
127
|
+
assert_equal 5, b
|
128
|
+
end
|
129
|
+
|
130
|
+
should "return nil if an exception is raised during an assignment" do
|
131
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/)
|
132
|
+
b = ExceptionHandling.ensure_safe { raise ArgumentError.new("blah") }
|
133
|
+
assert_equal nil, b
|
134
|
+
end
|
135
|
+
|
136
|
+
should "allow a message to be appended to the error when logged." do
|
137
|
+
mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/)
|
138
|
+
b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError.new("blah") }
|
139
|
+
assert_nil b
|
140
|
+
end
|
141
|
+
|
142
|
+
should "only rescue StandardError and descendents" do
|
143
|
+
assert_raise(Exception) { ExceptionHandling.ensure_safe("mooo") { raise Exception } }
|
144
|
+
|
145
|
+
mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/)
|
146
|
+
|
147
|
+
b = ExceptionHandling.ensure_safe("mooo") { raise StandardError.new("blah") }
|
148
|
+
assert_nil b
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context "ExceptionHandling.ensure_completely_safe" do
|
153
|
+
should "log an exception if an exception is raised." do
|
154
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/)
|
155
|
+
ExceptionHandling.ensure_completely_safe { raise ArgumentError.new("blah") }
|
156
|
+
end
|
157
|
+
|
158
|
+
should "should not log an exception if an exception is not raised." do
|
159
|
+
mock(ExceptionHandling.logger).fatal.times(0)
|
160
|
+
ExceptionHandling.ensure_completely_safe { ; }
|
161
|
+
end
|
162
|
+
|
163
|
+
should "return its value if used during an assignment" do
|
164
|
+
mock(ExceptionHandling.logger).fatal.times(0)
|
165
|
+
b = ExceptionHandling.ensure_completely_safe { 5 }
|
166
|
+
assert_equal 5, b
|
167
|
+
end
|
168
|
+
|
169
|
+
should "return nil if an exception is raised during an assignment" do
|
170
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/) { nil }
|
171
|
+
b = ExceptionHandling.ensure_completely_safe { raise ArgumentError.new("blah") }
|
172
|
+
assert_equal nil, b
|
173
|
+
end
|
174
|
+
|
175
|
+
should "allow a message to be appended to the error when logged." do
|
176
|
+
mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/)
|
177
|
+
b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError.new("blah") }
|
178
|
+
assert_nil b
|
179
|
+
end
|
180
|
+
|
181
|
+
should "rescue any instance or child of Exception" do
|
182
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/)
|
183
|
+
ExceptionHandling::ensure_completely_safe { raise Exception.new("blah") }
|
184
|
+
end
|
185
|
+
|
186
|
+
should "not rescue the special exceptions that Ruby uses" do
|
187
|
+
[SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
|
188
|
+
assert_raise exception do
|
189
|
+
ExceptionHandling.ensure_completely_safe do
|
190
|
+
raise exception.new
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context "ExceptionHandling.ensure_escalation" do
|
198
|
+
should "log the exception as usual and send the proper email" do
|
199
|
+
assert_equal 0, ActionMailer::Base.deliveries.count
|
200
|
+
mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/)
|
201
|
+
ExceptionHandling.ensure_escalation( "Favorite Feature") { raise ArgumentError.new("blah") }
|
202
|
+
assert_equal 2, ActionMailer::Base.deliveries.count
|
203
|
+
email = ActionMailer::Base.deliveries.last
|
204
|
+
assert_equal "#{ExceptionHandling.email_environment} Escalation: Favorite Feature", email.subject
|
205
|
+
assert_match 'ArgumentError: blah', email.body.to_s
|
206
|
+
assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
|
207
|
+
end
|
208
|
+
|
209
|
+
should "should not escalate if an exception is not raised." do
|
210
|
+
assert_equal 0, ActionMailer::Base.deliveries.count
|
211
|
+
dont_allow(ExceptionHandling.logger).fatal
|
212
|
+
ExceptionHandling.ensure_escalation('Ignored') { ; }
|
213
|
+
assert_equal 0, ActionMailer::Base.deliveries.count
|
214
|
+
end
|
215
|
+
|
216
|
+
should "log if the escalation email can not be sent" do
|
217
|
+
any_instance_of(Mail::Message) do |message|
|
218
|
+
mock(message).deliver
|
219
|
+
mock(message).deliver { raise RuntimeError.new, "Delivery Error" }
|
220
|
+
end
|
221
|
+
mock(ExceptionHandling.logger) do |logger|
|
222
|
+
logger.fatal(/first_test_exception/)
|
223
|
+
logger.fatal(/safe_email_deliver .*Delivery Error/)
|
224
|
+
end
|
225
|
+
ExceptionHandling.ensure_escalation("Not Used") { raise ArgumentError.new("first_test_exception") }
|
226
|
+
assert_equal 0, ActionMailer::Base.deliveries.count
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "exception timestamp" do
|
231
|
+
setup do
|
232
|
+
Time.now_override = Time.parse( '1986-5-21 4:17 am UTC' )
|
233
|
+
end
|
234
|
+
|
235
|
+
should "include the timestamp when the exception is logged" do
|
236
|
+
mock(ExceptionHandling.logger).fatal(/\(Error:517033020\) ArgumentError mooo \(blah\):\n.*exception_handling_test\.rb/)
|
237
|
+
b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError.new("blah") }
|
238
|
+
assert_nil b
|
239
|
+
|
240
|
+
assert_equal 517033020, ExceptionHandling.last_exception_timestamp
|
241
|
+
|
242
|
+
assert_emails 1
|
243
|
+
assert_match(/517033020/, ActionMailer::Base.deliveries[-1].body.to_s)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
should "send just one copy of exceptions that don't repeat" do
|
248
|
+
ExceptionHandling.log_error(exception_1)
|
249
|
+
ExceptionHandling.log_error(exception_2)
|
250
|
+
assert_emails 2
|
251
|
+
assert_match(/Exception 1/, ActionMailer::Base.deliveries[-2].subject)
|
252
|
+
assert_match(/Exception 2/, ActionMailer::Base.deliveries[-1].subject)
|
253
|
+
end
|
254
|
+
|
255
|
+
should "only send 5 of a repeated error" do
|
256
|
+
assert_emails 5 do
|
257
|
+
10.times do
|
258
|
+
ExceptionHandling.log_error(exception_1)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
should "only send 5 of a repeated error but don't send summary if 6th is different" do
|
264
|
+
assert_emails 5 do
|
265
|
+
5.times do
|
266
|
+
ExceptionHandling.log_error(exception_1)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
assert_emails 1 do
|
270
|
+
ExceptionHandling.log_error(exception_2)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
should "send the summary when the error is encountered an hour after the first occurrence" do
|
275
|
+
assert_emails 5 do # 5 exceptions, 4 summarized
|
276
|
+
9.times do |t|
|
277
|
+
ExceptionHandling.log_error(exception_1)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
Time.now_override = 2.hours.from_now
|
281
|
+
assert_emails 1 do # 1 summary (4 + 1 = 5) after 2 hours
|
282
|
+
ExceptionHandling.log_error(exception_1)
|
283
|
+
end
|
284
|
+
assert_match(/\[5 SUMMARIZED\]/, ActionMailer::Base.deliveries.last.subject)
|
285
|
+
assert_match(/This exception occurred 5 times since/, ActionMailer::Base.deliveries.last.body.to_s)
|
286
|
+
|
287
|
+
assert_emails 0 do # still summarizing...
|
288
|
+
7.times do
|
289
|
+
ExceptionHandling.log_error(exception_1)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
Time.now_override = 3.hours.from_now
|
294
|
+
|
295
|
+
assert_emails 1 + 2 do # 1 summary and 2 new
|
296
|
+
2.times do
|
297
|
+
ExceptionHandling.log_error(exception_2)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
assert_match(/\[7 SUMMARIZED\]/, ActionMailer::Base.deliveries[-3].subject)
|
301
|
+
assert_match(/This exception occurred 7 times since/, ActionMailer::Base.deliveries[-3].body.to_s)
|
302
|
+
end
|
303
|
+
|
304
|
+
should "send the summary if a summary is available, but not sent when another exception comes up" do
|
305
|
+
assert_emails 5 do # 5 to start summarizing
|
306
|
+
6.times do
|
307
|
+
ExceptionHandling.log_error(exception_1)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
assert_emails 1 + 1 do # 1 summary of previous, 1 from new exception
|
312
|
+
ExceptionHandling.log_error(exception_2)
|
313
|
+
end
|
314
|
+
|
315
|
+
assert_match(/\[1 SUMMARIZED\]/, ActionMailer::Base.deliveries[-2].subject)
|
316
|
+
assert_match(/This exception occurred 1 times since/, ActionMailer::Base.deliveries[-2].body.to_s)
|
317
|
+
|
318
|
+
assert_emails 5 do # 5 to start summarizing
|
319
|
+
10.times do
|
320
|
+
ExceptionHandling.log_error(exception_1)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
assert_emails 0 do # still summarizing
|
325
|
+
11.times do
|
326
|
+
ExceptionHandling.log_error(exception_1)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
class EventResponse
|
332
|
+
def to_s
|
333
|
+
"message from to_s!"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
should "allow sections to have data with just a to_s method" do
|
338
|
+
ExceptionHandling.log_error("This is my RingSwitch example. Log, don't email!") do |data|
|
339
|
+
data.merge!(:event_response => EventResponse.new)
|
340
|
+
end
|
341
|
+
assert_emails 1
|
342
|
+
assert_match(/message from to_s!/, ActionMailer::Base.deliveries.last.body.to_s)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
should "rescue exceptions that happen in log_error" do
|
347
|
+
stub(ExceptionHandling).make_exception { raise ArgumentError.new("Bad argument") }
|
348
|
+
mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
|
349
|
+
satisfy { |context| context['ExceptionHandling.log_error rescued exception while logging Runtime message'] },
|
350
|
+
anything)
|
351
|
+
stub($stderr).puts
|
352
|
+
ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
|
353
|
+
end
|
354
|
+
|
355
|
+
should "rescue exceptions that happen when log_error yields" do
|
356
|
+
mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
|
357
|
+
satisfy { |context| context['Context message'] },
|
358
|
+
anything)
|
359
|
+
ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |data| raise 'Error!!!' }
|
360
|
+
end
|
361
|
+
|
362
|
+
context "Exception Filtering" do
|
363
|
+
setup do
|
364
|
+
filter_list = { :exception1 => { 'error' => "my error message" },
|
365
|
+
:exception2 => { 'error' => "some other message", :session => "misc data" } }
|
366
|
+
stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
367
|
+
|
368
|
+
# bump modified time up to get the above filter loaded
|
369
|
+
stub(File).mtime { incrementing_mtime }
|
370
|
+
end
|
371
|
+
|
372
|
+
should "handle case where filter list is not found" do
|
373
|
+
stub(YAML).load_file { raise Errno::ENOENT.new("File not found") }
|
374
|
+
|
375
|
+
ActionMailer::Base.deliveries.clear
|
376
|
+
ExceptionHandling.log_error( "My error message is in list" )
|
377
|
+
assert_emails 1
|
378
|
+
end
|
379
|
+
|
380
|
+
should "log exception and suppress email when exception is on filter list" do
|
381
|
+
ActionMailer::Base.deliveries.clear
|
382
|
+
ExceptionHandling.log_error( "Error message is not in list" )
|
383
|
+
assert_emails 1
|
384
|
+
|
385
|
+
ActionMailer::Base.deliveries.clear
|
386
|
+
ExceptionHandling.log_error( "My error message is in list" )
|
387
|
+
assert_emails 0
|
388
|
+
end
|
389
|
+
|
390
|
+
should "allow filtering exception on any text in exception data" do
|
391
|
+
filters = { :exception1 => { :session => "data: my extra session data" } }
|
392
|
+
stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filters) }
|
393
|
+
|
394
|
+
ActionMailer::Base.deliveries.clear
|
395
|
+
ExceptionHandling.log_error( "No match here" ) do |data|
|
396
|
+
data[:session] = {
|
397
|
+
:key => "@session_id",
|
398
|
+
:data => "my extra session data"
|
399
|
+
}
|
400
|
+
end
|
401
|
+
assert_emails 0
|
402
|
+
|
403
|
+
ActionMailer::Base.deliveries.clear
|
404
|
+
ExceptionHandling.log_error( "No match here" ) do |data|
|
405
|
+
data[:session] = {
|
406
|
+
:key => "@session_id",
|
407
|
+
:data => "my extra session <no match!> data"
|
408
|
+
}
|
409
|
+
end
|
410
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.body.*.inspect
|
411
|
+
end
|
412
|
+
|
413
|
+
should "reload filter list on the next exception if file was modified" do
|
414
|
+
ActionMailer::Base.deliveries.clear
|
415
|
+
ExceptionHandling.log_error( "Error message is not in list" )
|
416
|
+
assert_emails 1
|
417
|
+
|
418
|
+
filter_list = { :exception1 => { 'error' => "Error message is not in list" } }
|
419
|
+
stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
420
|
+
stub(File).mtime { incrementing_mtime }
|
421
|
+
|
422
|
+
ActionMailer::Base.deliveries.clear
|
423
|
+
ExceptionHandling.log_error( "Error message is not in list" )
|
424
|
+
assert_emails 0, ActionMailer::Base.deliveries.*.body.*.inspect
|
425
|
+
end
|
426
|
+
|
427
|
+
should "not consider filter if both error message and body do not match" do
|
428
|
+
# error message matches, but not full text
|
429
|
+
ActionMailer::Base.deliveries.clear
|
430
|
+
ExceptionHandling.log_error( "some other message" )
|
431
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.body.*.inspect
|
432
|
+
|
433
|
+
# now both match
|
434
|
+
ActionMailer::Base.deliveries.clear
|
435
|
+
ExceptionHandling.log_error( "some other message" ) do |data|
|
436
|
+
data[:session] = {:some_random_key => "misc data"}
|
437
|
+
end
|
438
|
+
assert_emails 0, ActionMailer::Base.deliveries.*.body.*.inspect
|
439
|
+
end
|
440
|
+
|
441
|
+
should "skip environment keys not on whitelist" do
|
442
|
+
ActionMailer::Base.deliveries.clear
|
443
|
+
ExceptionHandling.log_error( "some message" ) do |data|
|
444
|
+
data[:environment] = { :SERVER_PROTOCOL => "HTTP/1.0", :RAILS_SECRETS_YML_CONTENTS => 'password: VERY_SECRET_PASSWORD' }
|
445
|
+
end
|
446
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.body.*.inspect
|
447
|
+
mail = ActionMailer::Base.deliveries.last
|
448
|
+
assert_nil mail.body.to_s["RAILS_SECRETS_YML_CONTENTS"], mail.body.to_s # this is not on whitelist
|
449
|
+
assert mail.body.to_s["SERVER_PROTOCOL: HTTP/1.0" ], mail.body.to_s # this is
|
450
|
+
end
|
451
|
+
|
452
|
+
should "omit environment defaults" do
|
453
|
+
ActionMailer::Base.deliveries.clear
|
454
|
+
ExceptionHandling.log_error( "some message" ) do |data|
|
455
|
+
data[:environment] = {:SERVER_PORT => '80', :SERVER_PROTOCOL => "HTTP/1.0"}
|
456
|
+
end
|
457
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.body.*.inspect
|
458
|
+
mail = ActionMailer::Base.deliveries.last
|
459
|
+
assert_nil mail.body.to_s["SERVER_PORT" ], mail.body.to_s # this was default
|
460
|
+
assert mail.body.to_s["SERVER_PROTOCOL: HTTP/1.0"], mail.body.to_s # this was not
|
461
|
+
end
|
462
|
+
|
463
|
+
should "reject the filter file if any contain all empty regexes" do
|
464
|
+
filter_list = { :exception1 => { 'error' => "", :session => "" },
|
465
|
+
:exception2 => { 'error' => "is not in list", :session => "" } }
|
466
|
+
stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
467
|
+
stub(File).mtime { incrementing_mtime }
|
468
|
+
|
469
|
+
ActionMailer::Base.deliveries.clear
|
470
|
+
ExceptionHandling.log_error( "Error message is not in list" )
|
471
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.inspect
|
472
|
+
end
|
473
|
+
|
474
|
+
context "Exception Handling Mailer" do
|
475
|
+
should "create email" do
|
476
|
+
ExceptionHandling.log_error(exception_1) do |data|
|
477
|
+
data[:request] = { :params => {:id => 10993}, :url => "www.ringrevenue.com" }
|
478
|
+
data[:session] = { :key => "DECAFE" }
|
479
|
+
end
|
480
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.inspect
|
481
|
+
assert mail = ActionMailer::Base.deliveries.last
|
482
|
+
assert_equal ['exceptions@example.com'], mail.to
|
483
|
+
assert_equal ['server@example.com'].to_s, mail.from.to_s
|
484
|
+
assert_match /Exception 1/, mail.to_s
|
485
|
+
assert_match /key: DECAFE/, mail.to_s
|
486
|
+
assert_match /id: 10993/, mail.to_s
|
487
|
+
end
|
488
|
+
|
489
|
+
EXPECTED_SMTP_HASH =
|
490
|
+
{
|
491
|
+
:host => 'localhost',
|
492
|
+
:domain => 'localhost.localdomain',
|
493
|
+
:from => 'server@example.com',
|
494
|
+
:to => 'exceptions@example.com'
|
495
|
+
}
|
496
|
+
|
497
|
+
[[true, false], [true, true]].each do |em_flag, synchrony_flag|
|
498
|
+
context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
|
499
|
+
setup do
|
500
|
+
ExceptionHandling.eventmachine_safe = em_flag
|
501
|
+
ExceptionHandling.eventmachine_synchrony = synchrony_flag
|
502
|
+
EventMachineStub.block = nil
|
503
|
+
set_test_const('EventMachine', EventMachineStub)
|
504
|
+
set_test_const('EventMachine::Protocols', Module.new)
|
505
|
+
end
|
506
|
+
|
507
|
+
teardown do
|
508
|
+
ExceptionHandling.eventmachine_safe = false
|
509
|
+
ExceptionHandling.eventmachine_synchrony = false
|
510
|
+
end
|
511
|
+
|
512
|
+
should "schedule EventMachine STMP when EventMachine defined" do
|
513
|
+
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
|
514
|
+
|
515
|
+
ExceptionHandling.log_error(exception_1)
|
516
|
+
assert EventMachineStub.block
|
517
|
+
EventMachineStub.block.call
|
518
|
+
assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |k,v| v.to_s }, SmtpClientStub.send_hash.inspect
|
519
|
+
assert_equal((synchrony_flag ? :asend : :send), SmtpClientStub.last_method)
|
520
|
+
assert_match(/Exception 1/, SmtpClientStub.send_hash[:content])
|
521
|
+
assert_emails 0, ActionMailer::Base.deliveries.*.to_s
|
522
|
+
end
|
523
|
+
|
524
|
+
should "pass the content as a proper rfc 2822 message" do
|
525
|
+
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
|
526
|
+
ExceptionHandling.log_error(exception_1)
|
527
|
+
assert EventMachineStub.block
|
528
|
+
EventMachineStub.block.call
|
529
|
+
assert content = SmtpClientStub.send_hash[:content]
|
530
|
+
assert_match(/Content-Transfer-Encoding: 7bit/, content)
|
531
|
+
assert_match(/<\/html>\r\n\.\r\n\z/, content)
|
532
|
+
end
|
533
|
+
|
534
|
+
should "log fatal on EventMachine STMP errback" do
|
535
|
+
assert_emails 0, ActionMailer::Base.deliveries.*.to_s
|
536
|
+
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
|
537
|
+
mock(ExceptionHandling.logger).fatal(/Exception 1/)
|
538
|
+
mock(ExceptionHandling.logger).fatal(/Failed to email by SMTP: "credential mismatch"/)
|
539
|
+
|
540
|
+
ExceptionHandling.log_error(exception_1)
|
541
|
+
assert EventMachineStub.block
|
542
|
+
EventMachineStub.block.call
|
543
|
+
SmtpClientErrbackStub.block.call("credential mismatch")
|
544
|
+
assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |k,v| v.to_s }, SmtpClientErrbackStub.send_hash.inspect
|
545
|
+
#assert_emails 0, ActionMailer::Base.deliveries.*.to_s
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
should "truncate email subject" do
|
552
|
+
ActionMailer::Base.deliveries.clear
|
553
|
+
text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM".split('').join("123456789")
|
554
|
+
begin
|
555
|
+
raise text
|
556
|
+
rescue => ex
|
557
|
+
ExceptionHandling.log_error( ex )
|
558
|
+
end
|
559
|
+
assert_emails 1, ActionMailer::Base.deliveries.*.inspect
|
560
|
+
mail = ActionMailer::Base.deliveries.last
|
561
|
+
subject = "#{ExceptionHandling.email_environment} exception: RuntimeError: " + text
|
562
|
+
assert_equal subject[0,300], mail.subject
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
context "Exception mapping" do
|
567
|
+
setup do
|
568
|
+
@data = {
|
569
|
+
:environment=>{
|
570
|
+
'HTTP_HOST' => "localhost",
|
571
|
+
'HTTP_REFERER' => "http://localhost/action/controller/instance",
|
572
|
+
},
|
573
|
+
:session=>{
|
574
|
+
:data=>{
|
575
|
+
:affiliate_id=> defined?(Affiliate) ? Affiliate.first.id : '1',
|
576
|
+
:edit_mode=> true,
|
577
|
+
:advertiser_id=> defined?(Advertiser) ? Advertiser.first.id : '1',
|
578
|
+
:username_id=> defined?(Username) ? Username.first.id : '1',
|
579
|
+
:user_id=> defined?(User) ? User.first.id : '1',
|
580
|
+
:flash=>{},
|
581
|
+
:impersonated_organization_pk=> 'Advertiser_1'
|
582
|
+
}
|
583
|
+
},
|
584
|
+
:request=>{},
|
585
|
+
:backtrace=>["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
|
586
|
+
:api_key=>"none",
|
587
|
+
:error_class=>"StandardError",
|
588
|
+
:error=>'Some error message'
|
589
|
+
}
|
590
|
+
end
|
591
|
+
|
592
|
+
should "clean backtraces" do
|
593
|
+
begin
|
594
|
+
raise "test exception"
|
595
|
+
rescue => ex
|
596
|
+
backtrace = ex.backtrace
|
597
|
+
end
|
598
|
+
result = ExceptionHandling.send(:clean_backtrace, ex).to_s
|
599
|
+
assert_not_equal result, backtrace
|
600
|
+
end
|
601
|
+
|
602
|
+
should "return entire backtrace if cleaned is emtpy" do
|
603
|
+
backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
|
604
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
|
605
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
|
606
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
|
607
|
+
"/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
|
608
|
+
"/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
|
609
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
|
610
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
|
611
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
|
612
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
|
613
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
|
614
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
|
615
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
|
616
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
|
617
|
+
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
|
618
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
|
619
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
|
620
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
|
621
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
|
622
|
+
"(eval):12:in `run_suite'",
|
623
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
|
624
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
|
625
|
+
"/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
|
626
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
|
627
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
|
628
|
+
"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
|
629
|
+
"-e:1"]
|
630
|
+
ex = Exception.new
|
631
|
+
ex.set_backtrace(backtrace)
|
632
|
+
result = ExceptionHandling.send(:clean_backtrace, ex)
|
633
|
+
assert_equal backtrace, result
|
634
|
+
end
|
635
|
+
|
636
|
+
should "clean params" do
|
637
|
+
p = {'password' => 'apple', 'username' => 'sam' }
|
638
|
+
ExceptionHandling.send( :clean_params, p )
|
639
|
+
assert_equal "[FILTERED]", p['password']
|
640
|
+
assert_equal 'sam', p['username']
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
context "log_perodically" do
|
645
|
+
setup do
|
646
|
+
Time.now_override = Time.now # Freeze time
|
647
|
+
ExceptionHandling.logger.clear
|
648
|
+
end
|
649
|
+
|
650
|
+
teardown do
|
651
|
+
Time.now_override = nil
|
652
|
+
end
|
653
|
+
|
654
|
+
should "log immediately when we are expected to log" do
|
655
|
+
logger_stub = ExceptionHandling.logger
|
656
|
+
|
657
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
658
|
+
assert_equal 1, logger_stub.logged.size
|
659
|
+
|
660
|
+
Time.now_override = Time.now + 5.minutes
|
661
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
|
662
|
+
assert_equal 1, logger_stub.logged.size
|
663
|
+
|
664
|
+
ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
|
665
|
+
assert_equal 2, logger_stub.logged.size
|
666
|
+
|
667
|
+
Time.now_override = Time.now + 26.minutes
|
668
|
+
|
669
|
+
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
670
|
+
assert_equal 3, logger_stub.logged.size
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
context "Errplane" do
|
675
|
+
module ErrplaneStub
|
676
|
+
end
|
677
|
+
|
678
|
+
setup do
|
679
|
+
set_test_const('Errplane', ErrplaneStub)
|
680
|
+
end
|
681
|
+
|
682
|
+
should "forward exceptions" do
|
683
|
+
mock(Errplane).transmit(exception_1, anything)
|
684
|
+
ExceptionHandling.log_error(exception_1, "context")
|
685
|
+
end
|
686
|
+
|
687
|
+
should "not forward warnings" do
|
688
|
+
stub(Errplane).transmit.times(0)
|
689
|
+
ExceptionHandling.log_warning("warning message")
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
private
|
694
|
+
|
695
|
+
def incrementing_mtime
|
696
|
+
@mtime ||= Time.now
|
697
|
+
@mtime += 1.day
|
698
|
+
end
|
699
|
+
|
700
|
+
def exception_1
|
701
|
+
@ex1 ||=
|
702
|
+
begin
|
703
|
+
raise StandardError, "Exception 1"
|
704
|
+
rescue => ex
|
705
|
+
ex
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
def exception_2
|
710
|
+
@ex2 ||=
|
711
|
+
begin
|
712
|
+
raise StandardError, "Exception 2"
|
713
|
+
rescue => ex
|
714
|
+
ex
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|