exception_handling 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ require 'action_mailer'
2
+
3
+ module ExceptionHandling
4
+ class Mailer < ActionMailer::Base
5
+ default :content_type => "text/html"
6
+
7
+ self.append_view_path "#{File.dirname(__FILE__)}/../views"
8
+
9
+ [:email_environment, :server_name, :sender_address, :exception_recipients, :escalation_recipients].each do |method|
10
+ define_method method do
11
+ ExceptionHandling.send(method) or raise "No #{method} set!"
12
+ end
13
+ end
14
+
15
+ def email_prefix
16
+ "#{email_environment} exception: "
17
+ end
18
+
19
+ def self.reloadable?() false end
20
+
21
+ def exception_notification( cleaned_data, first_seen_at = nil, occurrences = 0 )
22
+ if cleaned_data.is_a?(Hash)
23
+ cleaned_data.merge!({:occurrences => occurrences, :first_seen_at => first_seen_at}) if first_seen_at
24
+ cleaned_data.merge!({:server => server_name })
25
+ end
26
+
27
+ subject = "#{email_prefix}#{"[#{occurrences} SUMMARIZED]" if first_seen_at}#{cleaned_data[:error]}"[0,300]
28
+ recipients = exception_recipients
29
+ from = sender_address
30
+ @cleaned_data = cleaned_data
31
+
32
+ mail(:from => from,
33
+ :to => recipients,
34
+ :subject => subject)
35
+ end
36
+
37
+ def escalation_notification( summary, data)
38
+ subject = "#{email_environment} Escalation: #{summary}"
39
+ from = sender_address.gsub('xception', 'scalation')
40
+ recipients = escalation_recipients rescue exception_recipients
41
+
42
+ @summary = summary
43
+ @server = ExceptionHandling.server_name
44
+ @cleaned_data = data
45
+
46
+ mail(:from => from,
47
+ :to => recipients,
48
+ :subject => subject)
49
+ end
50
+
51
+ def log_parser_exception_notification( cleaned_data, key )
52
+ if cleaned_data.is_a?(Hash)
53
+ cleaned_data = cleaned_data.symbolize_keys
54
+ local_subject = cleaned_data[:error]
55
+ else
56
+ local_subject = "#{key}: #{cleaned_data}"
57
+ cleaned_data = { :error => cleaned_data.to_s }
58
+ end
59
+
60
+ @subject = "#{email_prefix}#{local_subject}"[0,300]
61
+ @recipients = exception_recipients
62
+ from = sender_address
63
+ @cleaned_data = cleaned_data
64
+
65
+ mail(:from => from,
66
+ :to => @recipients,
67
+ :subject => @subject)
68
+ end
69
+
70
+ def self.mailer_method_category
71
+ {
72
+ :exception_notification => :NetworkOptout,
73
+ :log_parser_exception_notification => :NetworkOptout
74
+ }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,923 @@
1
+ require './test/test_helper'
2
+ require 'exception_handling'
3
+
4
+ ExceptionHandling.email_environment = 'test'
5
+ ExceptionHandling.sender_address = 'server@example.com'
6
+ ExceptionHandling.exception_recipients = 'exceptions@example.com'
7
+ ExceptionHandling.server_name = 'server'
8
+
9
+ class ExceptionHandlingTest < ActiveSupport::TestCase
10
+ class LoggerStub
11
+ attr_accessor :logged
12
+
13
+ def initialize
14
+ clear
15
+ end
16
+
17
+ def info(message)
18
+ logged << message
19
+ end
20
+
21
+ def warn(message)
22
+ logged << message
23
+ end
24
+
25
+ def fatal(message)
26
+ logged << message
27
+ end
28
+
29
+ def clear
30
+ @logged = []
31
+ end
32
+ end
33
+
34
+
35
+ ExceptionHandling.logger = LoggerStub.new
36
+
37
+ def dont_stub_log_error
38
+ true
39
+ end
40
+
41
+ class TestController
42
+ class Request
43
+ attr_accessor :parameters, :protocol, :host, :request_uri, :env, :session_options
44
+ def initialize
45
+ @parameters = {:id => "1"}
46
+ @protocol = 'http'
47
+ @host = 'localhost'
48
+ @request_uri = "/fun/testing.html?foo=bar"
49
+ @env = {:HOST => "local"}
50
+ @session_options = { :id => '93951506217301' }
51
+ end
52
+ end
53
+
54
+ attr_accessor :request, :session
55
+ class << self
56
+ attr_accessor :around_filter_method
57
+ end
58
+
59
+ def initialize
60
+ @request = Request.new
61
+ @session_id = "ZKL95"
62
+ @session =
63
+ if defined?(Username)
64
+ {
65
+ :login_count => 22,
66
+ :username_id => Username.first.id,
67
+ :user_id => User.first.id,
68
+ }
69
+ else
70
+ { }
71
+ end
72
+ end
73
+
74
+ def simulate_around_filter( &block )
75
+ set_current_controller( &block )
76
+ end
77
+
78
+ def controller_name
79
+ "TestController"
80
+ end
81
+
82
+ def action_name
83
+ "test_action"
84
+ end
85
+
86
+ def self.around_filter( method )
87
+ TestController.around_filter_method = method
88
+ end
89
+
90
+ def complete_request_uri
91
+ "#{@request.protocol}#{@request.host}#{@request.request_uri}"
92
+ end
93
+
94
+ include ExceptionHandling::Methods
95
+ end
96
+
97
+ if defined?(Rails)
98
+ class TestAdvertiser < Advertiser
99
+ def test_log_error( ex, message=nil )
100
+ log_error(ex, message)
101
+ end
102
+
103
+ def test_ensure_escalation(summary)
104
+ ensure_escalation(summary) do
105
+ yield
106
+ end
107
+ end
108
+
109
+ def test_log_warning( message )
110
+ log_warning(message)
111
+ end
112
+
113
+ def test_ensure_safe(message="",&blk)
114
+ ensure_safe(message,&blk)
115
+ end
116
+
117
+ def self.task_exception exception
118
+ @@task_exception = exception
119
+ end
120
+ end
121
+ end
122
+
123
+ module EventMachineStub
124
+ class << self
125
+ attr_accessor :block
126
+
127
+ def schedule(&block)
128
+ @block = block
129
+ end
130
+ end
131
+ end
132
+
133
+ class SmtpClientStub
134
+ class << self
135
+ attr_reader :block
136
+ attr_reader :last_method
137
+
138
+ def errback(&block)
139
+ @block = block
140
+ end
141
+
142
+ def send_hash
143
+ @send_hash ||= {}
144
+ end
145
+
146
+ def send(hash)
147
+ @last_method = :send
148
+ send_hash.clear
149
+ send_hash.merge!(hash)
150
+ self
151
+ end
152
+
153
+ def asend(hash)
154
+ send(hash)
155
+ @last_method = :asend
156
+ self
157
+ end
158
+ end
159
+ end
160
+
161
+ class SmtpClientErrbackStub < SmtpClientStub
162
+ end
163
+
164
+ context "Exception Handling" do
165
+ setup do
166
+ ExceptionHandling.send(:clear_exception_summary)
167
+ end
168
+
169
+ context "exception filter parsing and loading" do
170
+ should "happen without an error" do
171
+ File.stubs(:mtime).returns( incrementing_mtime )
172
+ exception_filters = ExceptionHandling.send( :exception_filters )
173
+ assert( exception_filters.is_a?( ExceptionHandling::ExceptionFilters ) )
174
+ assert_nothing_raised "Loading the exception filter should not raise" do
175
+ exception_filters.send :load_file
176
+ end
177
+ assert !exception_filters.filtered?( "Scott says unlikely to ever match" )
178
+ end
179
+ end
180
+
181
+ context "ExceptionHandling::ensure_safe" do
182
+ should "log an exception if an exception is raised." do
183
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
184
+ ExceptionHandling::ensure_safe { raise ArgumentError.new("blah") }
185
+ end
186
+
187
+ should "should not log an exception if an exception is not raised." do
188
+ ExceptionHandling.logger.expects(:fatal).never
189
+ ExceptionHandling::ensure_safe { ; }
190
+ end
191
+
192
+ should "return its value if used during an assignment" do
193
+ ExceptionHandling.logger.expects(:fatal).never
194
+ b = ExceptionHandling::ensure_safe { 5 }
195
+ assert_equal 5, b
196
+ end
197
+
198
+ should "return nil if an exception is raised during an assignment" do
199
+ ExceptionHandling.logger.expects(:fatal).returns(nil)
200
+ b = ExceptionHandling::ensure_safe { raise ArgumentError.new("blah") }
201
+ assert_equal nil, b
202
+ end
203
+
204
+ should "allow a message to be appended to the error when logged." do
205
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /mooo \(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
206
+ b = ExceptionHandling::ensure_safe("mooo") { raise ArgumentError.new("blah") }
207
+ assert_nil b
208
+ end
209
+ end
210
+
211
+ context "ExceptionHandling::ensure_escalation" do
212
+ should "log the exception as usual and send the proper email" do
213
+ assert_equal 0, ActionMailer::Base.deliveries.count
214
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
215
+ ExceptionHandling::ensure_escalation( "Favorite Feature") { raise ArgumentError.new("blah") }
216
+ assert_equal 2, ActionMailer::Base.deliveries.count
217
+ email = ActionMailer::Base.deliveries.last
218
+ assert_equal 'test Escalation: Favorite Feature', email.subject
219
+ assert_match 'ArgumentError: blah', email.body.to_s
220
+ assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
221
+ end
222
+
223
+ should "should not escalate if an exception is not raised." do
224
+ assert_equal 0, ActionMailer::Base.deliveries.count
225
+ ExceptionHandling.logger.expects(:fatal).never
226
+ ExceptionHandling::ensure_escalation('Ignored') { ; }
227
+ assert_equal 0, ActionMailer::Base.deliveries.count
228
+ end
229
+
230
+ should "log if the escalation email can not be sent" do
231
+ Mail::Message.any_instance.expects(:deliver).times(2).returns(nil).then.raises(RuntimeError.new "Delivery Error")
232
+ exception_count = 0
233
+ exception_regexs = [/first_test_exception/, /safe_email_deliver .*Delivery Error/]
234
+
235
+ $stderr.stubs(:puts)
236
+ ExceptionHandling.logger.expects(:fatal).times(2).with { |ex| ex =~ exception_regexs[exception_count] or raise "Unexpected [#{exception_count}]: #{ex.inspect}"; exception_count += 1; true }
237
+ ExceptionHandling::ensure_escalation("Not Used") { raise ArgumentError.new("first_test_exception") }
238
+ #assert_equal 0, ActionMailer::Base.deliveries.count
239
+ end
240
+ end
241
+
242
+ context "exception timestamp" do
243
+ setup do
244
+ Time.now_override = Time.parse( '1986-5-21 4:17 am UTC' )
245
+ end
246
+
247
+ should "include the timestamp when the exception is logged" do
248
+ ExceptionHandling.logger.expects(:fatal).with { |ex| ex =~ /\(Error:517033020\) ArgumentError mooo \(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
249
+ b = ExceptionHandling::ensure_safe("mooo") { raise ArgumentError.new("blah") }
250
+ assert_nil b
251
+
252
+ assert_equal 517033020, ExceptionHandling.last_exception_timestamp
253
+
254
+ assert_emails 1
255
+ assert_match /517033020/, ActionMailer::Base.deliveries[-1].body.to_s
256
+ end
257
+ end
258
+
259
+ if defined?(LogErrorStub)
260
+ context "while running tests" do
261
+ setup do
262
+ stub_log_error
263
+ end
264
+
265
+ should "raise an error when log_error and log_warning are called" do
266
+ begin
267
+ ExceptionHandling.log_error("Something happened")
268
+ flunk
269
+ rescue LogErrorStub::UnexpectedExceptionLogged => ex
270
+ assert ex.to_s.starts_with?("StandardError: Something happened"), ex.to_s
271
+ end
272
+
273
+ begin
274
+ class ::RaisedError < StandardError; end
275
+ raise ::RaisedError, "This should raise"
276
+ rescue => ex
277
+ begin
278
+ ExceptionHandling.log_error(ex)
279
+ rescue LogErrorStub::UnexpectedExceptionLogged => ex_inner
280
+ assert ex_inner.to_s.starts_with?("RaisedError: This should raise"), ex_inner.to_s
281
+ end
282
+ end
283
+ end
284
+
285
+ should "allow for the regex specification of an expected exception to be ignored" do
286
+ exception_pattern = /StandardError: This is a test error/
287
+ assert_nil exception_whitelist # test that exception expectations are cleared
288
+ expects_exception(exception_pattern)
289
+ assert_equal exception_pattern, exception_whitelist[0][0]
290
+ begin
291
+ ExceptionHandling.log_error("This is a test error")
292
+ rescue => ex
293
+ flunk # Shouldn't raise an error in this case
294
+ end
295
+ end
296
+
297
+ should "allow for the string specification of an expected exception to be ignored" do
298
+ exception_pattern = "StandardError: This is a test error"
299
+ assert_nil exception_whitelist # test that exception expectations are cleared
300
+ expects_exception(exception_pattern)
301
+ assert_equal exception_pattern, exception_whitelist[0][0]
302
+ begin
303
+ ExceptionHandling.log_error("This is a test error")
304
+ rescue => ex
305
+ flunk # Shouldn't raise an error in this case
306
+ end
307
+ end
308
+
309
+ should "allow multiple errors to be ignored" do
310
+ class IgnoredError < StandardError; end
311
+ assert_nil exception_whitelist # test that exception expectations are cleared
312
+ expects_exception /StandardError: This is a test error/
313
+ expects_exception /IgnoredError: This should be ignored/
314
+ ExceptionHandling.log_error("This is a test error")
315
+ begin
316
+ raise IgnoredError, "This should be ignored"
317
+ rescue IgnoredError => ex
318
+ ExceptionHandling.log_error(ex)
319
+ end
320
+ end
321
+
322
+ should "expect exception twice if declared twice" do
323
+ expects_exception /StandardError: ERROR: I love lamp/
324
+ expects_exception /StandardError: ERROR: I love lamp/
325
+ ExceptionHandling.log_error("ERROR: I love lamp")
326
+ ExceptionHandling.log_error("ERROR: I love lamp")
327
+ end
328
+ end
329
+ end
330
+
331
+ should "send just one copy of exceptions that don't repeat" do
332
+ ExceptionHandling.log_error(exception_1)
333
+ ExceptionHandling.log_error(exception_2)
334
+ assert_emails 2
335
+ assert_match /Exception 1/, ActionMailer::Base.deliveries[-2].subject
336
+ assert_match /Exception 2/, ActionMailer::Base.deliveries[-1].subject
337
+ end
338
+
339
+ should "only send 5 of a repeated error" do
340
+ assert_emails 5 do
341
+ 10.times do
342
+ ExceptionHandling.log_error(exception_1)
343
+ end
344
+ end
345
+ end
346
+
347
+ should "only send 5 of a repeated error but don't send summary if 6th is different" do
348
+ assert_emails 5 do
349
+ 5.times do
350
+ ExceptionHandling.log_error(exception_1)
351
+ end
352
+ end
353
+ assert_emails 1 do
354
+ ExceptionHandling.log_error(exception_2)
355
+ end
356
+ end
357
+
358
+ should "send the summary when the error is encountered an hour after the first occurrence" do
359
+ assert_emails 5 do # 5 exceptions, 4 summarized
360
+ 9.times do |t|
361
+ ExceptionHandling.log_error(exception_1)
362
+ end
363
+ end
364
+ Time.now_override = 2.hours.from_now
365
+ assert_emails 1 do # 1 summary (4 + 1 = 5) after 2 hours
366
+ ExceptionHandling.log_error(exception_1)
367
+ end
368
+ assert_match /\[5 SUMMARIZED\]/, ActionMailer::Base.deliveries.last.subject
369
+ assert_match /This exception occurred 5 times since/, ActionMailer::Base.deliveries.last.body.to_s
370
+
371
+ assert_emails 0 do # still summarizing...
372
+ 7.times do
373
+ ExceptionHandling.log_error(exception_1)
374
+ end
375
+ end
376
+
377
+ Time.now_override = 3.hours.from_now
378
+
379
+ assert_emails 1 + 2 do # 1 summary and 2 new
380
+ 2.times do
381
+ ExceptionHandling.log_error(exception_2)
382
+ end
383
+ end
384
+ assert_match /\[7 SUMMARIZED\]/, ActionMailer::Base.deliveries[-3].subject
385
+ assert_match /This exception occurred 7 times since/, ActionMailer::Base.deliveries[-3].body.to_s
386
+ end
387
+
388
+ should "send the summary if a summary is available, but not sent when another exception comes up" do
389
+ assert_emails 5 do # 5 to start summarizing
390
+ 6.times do
391
+ ExceptionHandling.log_error(exception_1)
392
+ end
393
+ end
394
+
395
+ assert_emails 1 + 1 do # 1 summary of previous, 1 from new exception
396
+ ExceptionHandling.log_error(exception_2)
397
+ end
398
+
399
+ assert_match /\[1 SUMMARIZED\]/, ActionMailer::Base.deliveries[-2].subject
400
+ assert_match /This exception occurred 1 times since/, ActionMailer::Base.deliveries[-2].body.to_s
401
+
402
+ assert_emails 5 do # 5 to start summarizing
403
+ 10.times do
404
+ ExceptionHandling.log_error(exception_1)
405
+ end
406
+ end
407
+
408
+ assert_emails 0 do # still summarizing
409
+ 11.times do
410
+ ExceptionHandling.log_error(exception_1)
411
+ end
412
+ end
413
+ end
414
+
415
+ class EventResponse
416
+ def to_s
417
+ "message from to_s!"
418
+ end
419
+ end
420
+
421
+ should "allow sections to have data with just a to_s method" do
422
+ ExceptionHandling.log_error("This is my RingSwitch example. Log, don't email!") do |data|
423
+ data.merge!(:event_response => EventResponse.new)
424
+ end
425
+ assert_emails 1
426
+ assert_match /message from to_s!/, ActionMailer::Base.deliveries.last.body
427
+ end
428
+ end
429
+
430
+ should "rescue exceptions that happen in log_error" do
431
+ ExceptionHandling.stubs(:make_exception).raises(ArgumentError.new("Bad argument"))
432
+ ExceptionHandling.expects(:write_exception_to_log).with do |ex, context, timestamp|
433
+ ex.to_s['Bad argument'] or raise "Unexpected ex #{ex.class} - #{ex}"
434
+ context['ExceptionHandling.log_error rescued exception while logging Runtime message'] or raise "Unexpected context #{context}"
435
+ true
436
+ end
437
+ $stderr.stubs(:puts)
438
+ ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
439
+ end
440
+
441
+ should "rescue exceptions that happen when log_error yields" do
442
+ ExceptionHandling.expects(:write_exception_to_log).with do |ex, context, timestamp|
443
+ ex.to_s['Bad argument'] or raise "=================================================\nUnexpected ex #{ex.class} - #{ex}\n#{ex.backtrace.join("\n")}\n=============================================\n"
444
+ context['Context message'] or raise "Unexpected context #{context}"
445
+ true
446
+ end
447
+ ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |data| raise 'Error!!!' }
448
+ end
449
+
450
+ context "Exception Filtering" do
451
+ setup do
452
+ filter_list = { :exception1 => { :error => "my error message" },
453
+ :exception2 => { :error => "some other message", :session => "misc data" } }
454
+ YAML.stubs(:load_file).returns( ActiveSupport::HashWithIndifferentAccess.new( filter_list ) )
455
+
456
+ # bump modified time up to get the above filter loaded
457
+ File.stubs(:mtime).returns( incrementing_mtime )
458
+ end
459
+
460
+ should "handle case where filter list is not found" do
461
+ YAML.stubs(:load_file).raises( Errno::ENOENT.new( "File not found" ) )
462
+
463
+ ExceptionHandling.log_error( "My error message is in list" )
464
+ assert_emails 1
465
+ end
466
+
467
+ should "log exception and suppress email when exception is on filter list" do
468
+ ExceptionHandling.log_error( "Error message is not in list" )
469
+ assert_emails 1
470
+
471
+ ActionMailer::Base.deliveries.clear
472
+ ExceptionHandling.log_error( "My error message is in list" )
473
+ assert_emails 0
474
+ end
475
+
476
+ should "allow filtering exception on any text in exception data" do
477
+ filters = { :exception1 => { :session => "^data: my extra session data" } }
478
+ YAML.stubs(:load_file).returns( ActiveSupport::HashWithIndifferentAccess.new( filters ) )
479
+
480
+ ActionMailer::Base.deliveries.clear
481
+ ExceptionHandling.log_error( "No match here" ) do |data|
482
+ data[:session] = {
483
+ :key => "@session_id",
484
+ :data => "my extra session data"
485
+ }
486
+ end
487
+ assert_emails 0
488
+
489
+ ActionMailer::Base.deliveries.clear
490
+ ExceptionHandling.log_error( "No match here" ) do |data|
491
+ data[:session] = {
492
+ :key => "@session_id",
493
+ :data => "my extra session <no match!> data"
494
+ }
495
+ end
496
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
497
+ end
498
+
499
+ should "reload filter list on the next exception if file was modified" do
500
+ ExceptionHandling.log_error( "Error message is not in list" )
501
+ assert_emails 1
502
+
503
+ filter_list = { :exception1 => { :error => "Error message is not in list" } }
504
+ YAML.stubs(:load_file).returns( ActiveSupport::HashWithIndifferentAccess.new( filter_list ) )
505
+ File.stubs(:mtime).returns( incrementing_mtime )
506
+
507
+ ActionMailer::Base.deliveries.clear
508
+ ExceptionHandling.log_error( "Error message is not in list" )
509
+ assert_emails 0, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
510
+ end
511
+
512
+ should "not consider filter if both error message and body do not match" do
513
+ # error message matches, but not full text
514
+ ExceptionHandling.log_error( "some other message" )
515
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
516
+
517
+ # now both match
518
+ ActionMailer::Base.deliveries.clear
519
+ ExceptionHandling.log_error( "some other message" ) do |data|
520
+ data[:session] = {:some_random_key => "misc data"}
521
+ end
522
+ assert_emails 0, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
523
+ end
524
+
525
+ should "skip environment keys not on whitelist" do
526
+ ExceptionHandling.log_error( "some message" ) do |data|
527
+ data[:environment] = { :SERVER_PROTOCOL => "HTTP/1.0", :RAILS_SECRETS_YML_CONTENTS => 'password: VERY_SECRET_PASSWORD' }
528
+ end
529
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
530
+ mail = ActionMailer::Base.deliveries.last
531
+ assert_nil mail.body.to_s["RAILS_SECRETS_YML_CONTENTS"], mail.body.to_s # this is not on whitelist
532
+ assert mail.body.to_s["SERVER_PROTOCOL: HTTP/1.0" ], mail.body.to_s # this is
533
+ end
534
+
535
+ should "omit environment defaults" do
536
+ ExceptionHandling.log_error( "some message" ) do |data|
537
+ data[:environment] = {:SERVER_PORT => '80', :SERVER_PROTOCOL => "HTTP/1.0"}
538
+ end
539
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
540
+ mail = ActionMailer::Base.deliveries.last
541
+ assert_nil mail.body.to_s["SERVER_PORT" ], mail.body.to_s # this was default
542
+ assert mail.body.to_s["SERVER_PROTOCOL: HTTP/1.0"], mail.body.to_s # this was not
543
+ end
544
+
545
+ should "reject the filter file if any contain all empty regexes" do
546
+ filter_list = { :exception1 => { :error => "", :session => "" },
547
+ :exception2 => { :error => "is not in list", :session => "" } }
548
+ YAML.stubs(:load_file).returns( ActiveSupport::HashWithIndifferentAccess.new( filter_list ) )
549
+ File.stubs(:mtime).returns( incrementing_mtime )
550
+
551
+ ActionMailer::Base.deliveries.clear
552
+ ExceptionHandling.log_error( "Error message is not in list" )
553
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
554
+ end
555
+
556
+ context "Exception Handling Mailer" do
557
+ should "create email" do
558
+ ExceptionHandling.log_error(exception_1) do |data|
559
+ data[:request] = { :params => {:id => 10993}, :url => "www.ringrevenue.com" }
560
+ data[:session] = { :key => "DECAFE" }
561
+ end
562
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
563
+ assert mail = ActionMailer::Base.deliveries.last
564
+ assert_equal ['exceptions@example.com'], mail.to
565
+ assert_equal 'server@example.com', mail.from.to_s
566
+ assert_match /Exception 1/, mail.to_s
567
+ assert_match /key: DECAFE/, mail.to_s
568
+ assert_match /id: 10993/, mail.to_s
569
+ end
570
+
571
+ EXPECTED_SMTP_HASH =
572
+ {
573
+ :host => 'localhost',
574
+ :domain => 'localhost.localdomain',
575
+ :from => 'server@example.com',
576
+ :to => 'exceptions@example.com'
577
+ }
578
+
579
+ [true, :Synchrony].each do |synchrony_flag|
580
+ context "EVENTMACHINE_EXCEPTION_HANDLING = #{synchrony_flag}" do
581
+ setup do
582
+ set_test_const('EVENTMACHINE_EXCEPTION_HANDLING', synchrony_flag)
583
+ EventMachineStub.block = nil
584
+ set_test_const('EventMachine', EventMachineStub)
585
+ set_test_const('EventMachine::Protocols', Module.new)
586
+ end
587
+
588
+ should "schedule EventMachine STMP when EventMachine defined" do
589
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
590
+
591
+ ExceptionHandling.log_error(exception_1)
592
+ assert EventMachineStub.block
593
+ EventMachineStub.block.call
594
+ EXPECTED_SMTP_HASH.map { |key, value| assert_equal value, SmtpClientStub.send_hash[key].to_s, SmtpClientStub.send_hash.inspect }
595
+ assert_equal (synchrony_flag == :Synchrony ? :asend : :send), SmtpClientStub.last_method
596
+ assert_match /Exception 1/, SmtpClientStub.send_hash[:body]
597
+ assert_emails 0, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
598
+ end
599
+
600
+ should "log fatal on EventMachine STMP errback" do
601
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
602
+ ExceptionHandling.logger.expects(:fatal).twice.with do |message|
603
+ assert message =~ /Failed to email by SMTP: "credential mismatch"/ || message =~ /Exception 1/, message
604
+ true
605
+ end
606
+ ExceptionHandling.log_error(exception_1)
607
+ assert EventMachineStub.block
608
+ EventMachineStub.block.call
609
+ SmtpClientErrbackStub.block.call("credential mismatch")
610
+ EXPECTED_SMTP_HASH.map { |key, value| assert_equal value, SmtpClientErrbackStub.send_hash[key].to_s, SmtpClientErrbackStub.send_hash.inspect }
611
+ assert_emails 0, ActionMailer::Base.deliveries.map { |m| m.body.inspect }
612
+ end
613
+ end
614
+ end
615
+ end
616
+
617
+ should "truncate email subject" do
618
+ text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM".split('').join("123456789")
619
+ begin
620
+ raise text
621
+ rescue => ex
622
+ ExceptionHandling.log_error( ex )
623
+ end
624
+ assert_emails 1, ActionMailer::Base.deliveries.map { |m| m.inspect }
625
+ mail = ActionMailer::Base.deliveries.last
626
+ subject = "test exception: RuntimeError: " + text
627
+ assert_equal subject[0,300], mail.subject
628
+ end
629
+ end
630
+
631
+ if defined?(Rails)
632
+ context "ExceptionHandling.Methods" do
633
+ setup do
634
+ @controller = TestController.new
635
+ end
636
+
637
+ teardown do
638
+ Time.now_override = nil
639
+ end
640
+
641
+ should "set the around filter" do
642
+ assert_equal :set_current_controller, TestController.around_filter_method
643
+ assert_nil ExceptionHandling.current_controller
644
+ @controller.simulate_around_filter( ) do
645
+ assert_equal @controller, ExceptionHandling.current_controller
646
+ end
647
+ assert_nil ExceptionHandling.current_controller
648
+ end
649
+
650
+ should "use the current controller when included in a Model" do
651
+ ActiveRecord::Base.logger.expects(:fatal).with { |ex| ex =~ /blah/ }
652
+ @controller.simulate_around_filter( ) do
653
+ a = TestAdvertiser.new :name => 'Joe Ads'
654
+ a.test_log_error( ArgumentError.new("blah") )
655
+ mail = ActionMailer::Base.deliveries.last
656
+ assert_equal EXCEPTION_HANDLING_MAILER_RECIPIENTS, mail.to
657
+ assert_match( @controller.request.request_uri, mail.body.to_s )
658
+ assert_match( Username.first.username.to_s, mail.body.to_s )
659
+ end
660
+ end
661
+
662
+ should "use the current_controller when available" do
663
+ ActiveRecord::Base.logger.expects(:fatal).with( ) { |ex| ex =~ /blah/ }
664
+ @controller.simulate_around_filter do
665
+ ExceptionHandling.log_error( ArgumentError.new("blah") )
666
+ mail = ActionMailer::Base.deliveries.last
667
+ assert_equal EXCEPTION_HANDLING_MAILER_RECIPIENTS, mail.to
668
+ assert_match( @controller.request.request_uri, mail.body )
669
+ assert_match( Username.first.username.to_s, mail.body )
670
+ mail = ActionMailer::Base.deliveries.last
671
+ assert_equal EXCEPTION_HANDLING_MAILER_RECIPIENTS, mail.to
672
+ assert_match( @controller.request.request_uri, mail.body.to_s )
673
+ assert_match( Username.first.username.to_s, mail.body.to_s )
674
+ assert mail = ActionMailer::Base.deliveries.last
675
+ assert_equal EXCEPTION_HANDLING_MAILER_RECIPIENTS, mail.to
676
+ assert_match( @controller.request.request_uri, mail.body.to_s )
677
+ assert_match( Username.first.username.to_s, mail.body.to_s )
678
+ end
679
+ end
680
+
681
+ should "report long running controller action" do
682
+ # If stubbing this causes problems, retreat.
683
+ Rails.expects(:env).returns('production')
684
+ ExceptionHandling.logger.expects(:fatal).with { |ex| ex =~ /Long controller action detected in TestController::test_action/ or raise "Unexpected: #{ex.inspect}"}
685
+ @controller.simulate_around_filter( ) do
686
+ Time.now_override = 1.hour.from_now
687
+ end
688
+ end
689
+ end
690
+
691
+ context "a model object" do
692
+ setup do
693
+ @a = TestAdvertiser.new :name => 'Joe Ads'
694
+ end
695
+
696
+ context "with an argument error" do
697
+ setup do
698
+ begin
699
+ raise ArgumentError.new("blah");
700
+ rescue => ex
701
+ @argument_error = ex
702
+ end
703
+ end
704
+
705
+ context "log_error on a model" do
706
+ should "log errors" do
707
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
708
+ @a.test_log_error( @argument_error )
709
+ end
710
+
711
+ should "log errors from strings" do
712
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling\.rb/ or raise "Unexpected: #{ex.inspect}" }
713
+ @a.test_log_error( "blah" )
714
+ end
715
+
716
+ should "log errors with strings" do
717
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /mooo.* \(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
718
+ @a.test_log_error( @argument_error, "mooo" )
719
+ end
720
+ end
721
+
722
+ context "ensure_escalation on a model" do
723
+ should "work" do
724
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
725
+ @a.test_ensure_escalation 'Favorite Feature' do
726
+ raise @argument_error
727
+ end
728
+ assert_equal 2, ActionMailer::Base.deliveries.count
729
+ email = ActionMailer::Base.deliveries.last
730
+ assert_equal 'development-local Escalation: Favorite Feature', email.subject
731
+ assert_match 'ArgumentError: blah', email.body.to_s
732
+ end
733
+ end
734
+
735
+ context "ExceptionHandling::log_error" do
736
+ should "log errors" do
737
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
738
+ ExceptionHandling::log_error( @argument_error )
739
+ end
740
+
741
+ should "log errors from strings" do
742
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling\.rb/ or raise "Unexpected: #{ex.inspect}" }
743
+ ExceptionHandling::log_error( "blah" )
744
+ end
745
+
746
+ should "log errors with strings" do
747
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /mooo.*\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
748
+ ExceptionHandling::log_error( @argument_error, "mooo" )
749
+ end
750
+ end
751
+ end
752
+
753
+ should "log warnings" do
754
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /blah/ }
755
+ @a.test_log_warning("blah")
756
+ end
757
+
758
+ context "ensure_safe on the model" do
759
+ should "log an exception if an exception is raised." do
760
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
761
+ @a.test_ensure_safe { raise ArgumentError.new("blah") }
762
+ end
763
+
764
+ should "should not log an exception if an exception is not raised." do
765
+ ExceptionHandling.logger.expects(:fatal).never
766
+ @a.test_ensure_safe { ; }
767
+ end
768
+
769
+ should "return its value if used during an assignment" do
770
+ ExceptionHandling.logger.expects(:fatal).never
771
+ b = @a.test_ensure_safe { 5 }
772
+ assert_equal 5, b
773
+ end
774
+
775
+ should "return nil if an exception is raised during an assignment" do
776
+ ExceptionHandling.logger.expects(:fatal).returns(nil)
777
+ b = @a.test_ensure_safe { raise ArgumentError.new("blah") }
778
+ assert_nil b
779
+ end
780
+
781
+ should "allow a message to be appended to the error when logged." do
782
+ ExceptionHandling.logger.expects(:fatal).with( ) { |ex| ex =~ /mooo.*\(blah\):\n.*exception_handling_test\.rb/ or raise "Unexpected: #{ex.inspect}" }
783
+ b = @a.test_ensure_safe("mooo") { raise ArgumentError.new("blah") }
784
+ assert_nil b
785
+ end
786
+ end
787
+ end
788
+ end
789
+
790
+ context "Exception mapping" do
791
+ setup do
792
+ @data = {
793
+ :environment=>{
794
+ 'HTTP_HOST' => "localhost",
795
+ 'HTTP_REFERER' => "http://localhost/action/controller/instance",
796
+ },
797
+ :session=>{
798
+ :data=>{
799
+ :affiliate_id=> defined?(Affiliate) ? Affiliate.first.id : '1',
800
+ :edit_mode=> true,
801
+ :advertiser_id=> defined?(Advertiser) ? Advertiser.first.id : '1',
802
+ :username_id=> defined?(Username) ? Username.first.id : '1',
803
+ :user_id=> defined?(User) ? User.first.id : '1',
804
+ :flash=>{},
805
+ :impersonated_organization_pk=> 'Advertiser_1'
806
+ }
807
+ },
808
+ :request=>{},
809
+ :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'"],
810
+ :api_key=>"none",
811
+ :error_class=>"StandardError",
812
+ :error=>'Some error message'
813
+ }
814
+ end
815
+
816
+ should "clean backtraces" do
817
+ begin
818
+ raise "test exception"
819
+ rescue => ex
820
+ backtrace = ex.backtrace
821
+ end
822
+ result = ExceptionHandling.send(:clean_backtrace, ex).to_s
823
+ assert_not_equal result, backtrace
824
+ end
825
+
826
+ should "clean params" do
827
+ p = {'password' => 'apple', 'username' => 'sam' }
828
+ ExceptionHandling.send( :clean_params, p )
829
+ assert_equal "[FILTERED]", p['password']
830
+ assert_equal 'sam', p['username']
831
+ end
832
+ end
833
+
834
+ context "log_perodically" do
835
+ setup do
836
+ Time.now_override = Time.now # Freeze time
837
+ ExceptionHandling.logger.clear
838
+ end
839
+
840
+ teardown do
841
+ Time.now_override = nil
842
+ end
843
+
844
+ should "log immediately when we are expected to log" do
845
+ logger_stub = ExceptionHandling.logger
846
+
847
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
848
+ assert_equal 1, logger_stub.logged.size
849
+
850
+ Time.now_override = Time.now + 5.minutes
851
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
852
+ assert_equal 1, logger_stub.logged.size
853
+
854
+ ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
855
+ assert_equal 2, logger_stub.logged.size
856
+
857
+ Time.now_override = Time.now + 26.minutes
858
+
859
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
860
+ assert_equal 3, logger_stub.logged.size
861
+ end
862
+ end
863
+
864
+ context "Errplane" do
865
+ module ErrplaneStub
866
+ end
867
+
868
+ setup do
869
+ set_test_const('Errplane', ErrplaneStub)
870
+ end
871
+
872
+ should "forward exceptions" do
873
+ ex = data = nil
874
+ Errplane.expects(:transmit).with do |ex_, data_|
875
+ ex, data = ex_, data_
876
+ true
877
+ end
878
+
879
+ ExceptionHandling.log_error(exception_1, "context")
880
+
881
+ assert_equal exception_1, ex, ex.inspect
882
+ custom_data = data[:custom_data]
883
+ custom_data["error"].include?("context") or raise "Wrong custom_data #{custom_data["error"].inspect}"
884
+ end
885
+
886
+ should "not forward warnings" do
887
+ never = true
888
+ Errplane.stubs(:transmit).with do
889
+ never = false
890
+ true
891
+ end
892
+
893
+ ExceptionHandling.log_warning("warning message")
894
+
895
+ assert never, "transmit should not have been called"
896
+ end
897
+ end
898
+
899
+ private
900
+
901
+ def incrementing_mtime
902
+ @mtime ||= Time.now
903
+ @mtime += 1.day
904
+ end
905
+
906
+ def exception_1
907
+ @ex1 ||=
908
+ begin
909
+ raise StandardError, "Exception 1"
910
+ rescue => ex
911
+ ex
912
+ end
913
+ end
914
+
915
+ def exception_2
916
+ @ex2 ||=
917
+ begin
918
+ raise StandardError, "Exception 2"
919
+ rescue => ex
920
+ ex
921
+ end
922
+ end
923
+ end