exception_handling 0.1.1

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.
@@ -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