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