exception_handling 2.5.0.pre.1 → 2.6.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.jenkins/Jenkinsfile +24 -8
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +22 -2
  6. data/Gemfile +4 -4
  7. data/Gemfile.lock +30 -20
  8. data/Rakefile +7 -6
  9. data/lib/exception_handling.rb +8 -2
  10. data/lib/exception_handling/exception_info.rb +4 -5
  11. data/lib/exception_handling/log_stub_error.rb +2 -1
  12. data/lib/exception_handling/version.rb +1 -1
  13. data/{test → spec}/helpers/controller_helpers.rb +0 -0
  14. data/{test → spec}/helpers/exception_helpers.rb +2 -2
  15. data/spec/rake_test_warning_false.rb +20 -0
  16. data/{test/test_helper.rb → spec/spec_helper.rb} +57 -42
  17. data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
  18. data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
  19. data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +118 -99
  20. data/{test/unit/exception_handling/honeybadger_callbacks_test.rb → spec/unit/exception_handling/honeybadger_callbacks_spec.rb} +20 -20
  21. data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
  22. data/{test/unit/exception_handling/mailer_test.rb → spec/unit/exception_handling/mailer_spec.rb} +17 -17
  23. data/spec/unit/exception_handling/methods_spec.rb +84 -0
  24. data/spec/unit/exception_handling/sensu_spec.rb +51 -0
  25. data/spec/unit/exception_handling_spec.rb +1226 -0
  26. metadata +34 -31
  27. data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
  28. data/test/unit/exception_handling/exception_description_test.rb +0 -82
  29. data/test/unit/exception_handling/methods_test.rb +0 -84
  30. data/test/unit/exception_handling/sensu_test.rb +0 -52
  31. data/test/unit/exception_handling_test.rb +0 -1137
@@ -0,0 +1,1226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../spec_helper', __dir__)
4
+ require_test_helper 'controller_helpers'
5
+ require_test_helper 'exception_helpers'
6
+
7
+ describe ExceptionHandling do
8
+ include ControllerHelpers
9
+ include ExceptionHelpers
10
+
11
+ before do
12
+ @fail_count = 0
13
+ end
14
+
15
+ def dont_stub_log_error
16
+ true
17
+ end
18
+
19
+ def append_organization_info_config(data)
20
+ data[:user_details] = {}
21
+ data[:user_details][:username] = "CaryP"
22
+ data[:user_details][:organization] = "Invoca Engineering Dept."
23
+ rescue StandardError
24
+ # don't let these out!
25
+ end
26
+
27
+ def custom_data_callback_returns_nil_message_exception(_data)
28
+ raise_exception_with_nil_message
29
+ end
30
+
31
+ def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
32
+ @fail_count += 1
33
+ end
34
+
35
+ def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
36
+ @callback_data = data
37
+ @treat_like_warning = treat_like_warning
38
+ @fail_count += 1
39
+ @honeybadger_status = honeybadger_status
40
+ end
41
+
42
+ def log_error_callback_with_failure(_data, _ex)
43
+ raise "this should be rescued"
44
+ end
45
+
46
+ def log_error_callback_returns_nil_message_exception(_data, _ex)
47
+ raise_exception_with_nil_message
48
+ end
49
+
50
+ module EventMachineStub
51
+ class << self
52
+ attr_accessor :block
53
+
54
+ def schedule(&block)
55
+ @block = block
56
+ end
57
+ end
58
+ end
59
+
60
+ class DNSResolvStub
61
+ class << self
62
+ attr_accessor :callback_block
63
+ attr_accessor :errback_block
64
+
65
+ def resolve(_hostname)
66
+ self
67
+ end
68
+
69
+ def callback(&block)
70
+ @callback_block = block
71
+ end
72
+
73
+ def errback(&block)
74
+ @errback_block = block
75
+ end
76
+ end
77
+ end
78
+
79
+ class SmtpClientStub
80
+ class << self
81
+ attr_reader :block
82
+ attr_reader :last_method
83
+
84
+ def errback(&block)
85
+ @block = block
86
+ end
87
+
88
+ def send_hash
89
+ @send_hash ||= {}
90
+ end
91
+
92
+ def send(hash)
93
+ @last_method = :send
94
+ send_hash.clear
95
+ send_hash.merge!(hash)
96
+ self
97
+ end
98
+
99
+ def asend(hash)
100
+ send(hash)
101
+ @last_method = :asend
102
+ self
103
+ end
104
+ end
105
+ end
106
+
107
+ class SmtpClientErrbackStub < SmtpClientStub
108
+ end
109
+
110
+ context "with warn and honeybadger notify stubbed" do
111
+ before do
112
+ allow(ExceptionHandling).to receive(:warn).with(anything)
113
+ allow(Honeybadger).to receive(:notify).with(anything)
114
+ end
115
+
116
+ context "with logger stashed" do
117
+ before { @original_logger = ExceptionHandling.logger }
118
+ after { ExceptionHandling.logger = @original_logger }
119
+
120
+ it "store logger as-is if it has ContextualLogger::Mixin" do
121
+ logger = Logger.new('/dev/null')
122
+ logger.extend(ContextualLogger::LoggerMixin)
123
+ ancestors = logger.singleton_class.ancestors.*.name
124
+
125
+ ExceptionHandling.logger = logger
126
+ expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to eq(ancestors)
127
+ end
128
+
129
+ it "allow logger = nil (no deprecation warning)" do
130
+ expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING/).never
131
+ ExceptionHandling.logger = nil
132
+ end
133
+
134
+ it "[deprecated] mix in ContextualLogger::Mixin if not there" do
135
+ expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING: implicit extend with ContextualLogger::LoggerMixin is deprecated and will be removed from exception_handling 3\.0/)
136
+ logger = Logger.new('/dev/null')
137
+ ancestors = logger.singleton_class.ancestors.*.name
138
+
139
+ ExceptionHandling.logger = logger
140
+ expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to_not eq(ancestors)
141
+ expect(ExceptionHandling.logger).to be_kind_of(ContextualLogger::LoggerMixin)
142
+ end
143
+ end
144
+
145
+ context "#log_error" do
146
+ it "take in additional logging context hash and pass it to the logger" do
147
+ ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
148
+ expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Error/)
149
+ expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
150
+ expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
151
+ end
152
+
153
+ it "log with Severity::FATAL" do
154
+ ExceptionHandling.log_error('This is a Warning', service_name: 'exception_handling')
155
+ expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
156
+ end
157
+ end
158
+
159
+ context "#log_warning" do
160
+ it "have empty array as a backtrace" do
161
+ expect(ExceptionHandling).to receive(:log_error).with((ExceptionHandling::Warning), anything) do |error|
162
+ expect(error.backtrace).to eq([])
163
+ end
164
+ ExceptionHandling.log_warning('Now with empty array as a backtrace!')
165
+ end
166
+
167
+ it "take in additional key word args as logging context and pass them to the logger" do
168
+ ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
169
+ expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Warning/)
170
+ expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
171
+ expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
172
+ end
173
+
174
+ it "log with Severity::WARN" do
175
+ ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
176
+ expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
177
+ end
178
+ end
179
+
180
+ context "#log_info" do
181
+ it "take in additional key word args as logging context and pass them to the logger" do
182
+ ExceptionHandling.log_info('This is an Info', service_name: 'exception_handling')
183
+ expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Info/)
184
+ expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
185
+ expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
186
+ end
187
+
188
+ it "log with Severity::INFO" do
189
+ ExceptionHandling.log_info('This is a Warning', service_name: 'exception_handling')
190
+ expect('INFO').to eq(logged_excluding_reload_filter.last[:severity])
191
+ end
192
+ end
193
+
194
+ context "#log_debug" do
195
+ it "take in additional key word args as logging context and pass them to the logger" do
196
+ ExceptionHandling.log_debug('This is a Debug', service_name: 'exception_handling')
197
+ expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Debug/)
198
+ expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
199
+ expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
200
+ end
201
+
202
+ it "log with Severity::DEBUG" do
203
+ ExceptionHandling.log_debug('This is a Warning', service_name: 'exception_handling')
204
+ expect('DEBUG').to eq(logged_excluding_reload_filter.last[:severity])
205
+ end
206
+ end
207
+
208
+ context "#write_exception_to_log" do
209
+ it "log warnings with Severity::WARN" do
210
+ warning = ExceptionHandling::Warning.new('This is a Warning')
211
+ ExceptionHandling.write_exception_to_log(warning, '', Time.now.to_i, service_name: 'exception_handling')
212
+ expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
213
+ end
214
+
215
+ it "log everything else with Severity::FATAL" do
216
+ error = RuntimeError.new('This is a runtime error')
217
+ ExceptionHandling.write_exception_to_log(error, '', Time.now.to_i, service_name: 'exception_handling')
218
+ expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
219
+ end
220
+ end
221
+
222
+ context "configuration with custom_data_hook or post_log_error_hook" do
223
+ after do
224
+ ExceptionHandling.custom_data_hook = nil
225
+ ExceptionHandling.post_log_error_hook = nil
226
+ end
227
+
228
+ it "support a custom_data_hook" do
229
+ capture_notifications
230
+
231
+ ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
232
+ ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
233
+
234
+ expect(sent_notifications.last.enhanced_data['user_details'].to_s).to match(/Invoca Engineering Dept./)
235
+ end
236
+
237
+ it "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
238
+ @honeybadger_status = nil
239
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
240
+
241
+ notify_args = []
242
+ expect(Honeybadger).to receive(:notify).with(any_args) { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
243
+ ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
244
+ expect(@fail_count).to eq(1)
245
+ expect(@treat_like_warning).to eq(false)
246
+ expect(@honeybadger_status).to eq(:success)
247
+
248
+ expect(@callback_data["notes"]).to eq("this is used by a test")
249
+ expect(notify_args.size).to eq(1), notify_args.inspect
250
+ expect(notify_args.last[:context].to_s).to match(/this is used by a test/)
251
+ end
252
+
253
+ it "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
254
+ @honeybadger_status = nil
255
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
256
+ ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
257
+ expect(@fail_count).to eq(1)
258
+ expect(@treat_like_warning).to eq(true)
259
+ expect(@honeybadger_status).to eq(:skipped)
260
+ end
261
+
262
+ it "include logging context in the exception data" do
263
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
264
+ ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
265
+
266
+ expected_log_context = {
267
+ "log_context_test" => "contextual_logging"
268
+ }
269
+ expect(@callback_data[:log_context]).to eq(expected_log_context)
270
+ end
271
+
272
+ it "support rescue exceptions from a log_error hook" do
273
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
274
+ log_info_messages = []
275
+ allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
276
+ log_info_messages << message
277
+ end
278
+ expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
279
+ expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
280
+ end
281
+
282
+ it "handle nil message exceptions resulting from the log_error hook" do
283
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
284
+ log_info_messages = []
285
+ allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
286
+ log_info_messages << message
287
+ end
288
+ expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
289
+ expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
290
+ end
291
+
292
+ it "handle nil message exceptions resulting from the custom data hook" do
293
+ ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
294
+ log_info_messages = []
295
+ allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
296
+ log_info_messages << message
297
+ end
298
+ expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.not_to raise_error
299
+ expect(log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }).to be_truthy
300
+ end
301
+ end
302
+
303
+ context "Exception Handling" do
304
+ context "default_metric_name" do
305
+ context "when metric_name is present in exception_data" do
306
+ it "include metric_name in resulting metric name" do
307
+ exception = StandardError.new('this is an exception')
308
+ metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
309
+ expect(metric).to eq('exception_handling.special_metric')
310
+ end
311
+ end
312
+
313
+ context "when metric_name is not present in exception_data" do
314
+ it "return exception_handling.warning when using log warning" do
315
+ warning = ExceptionHandling::Warning.new('this is a warning')
316
+ metric = ExceptionHandling.default_metric_name({}, warning, false)
317
+ expect(metric).to eq('exception_handling.warning')
318
+ end
319
+
320
+ it "return exception_handling.exception when using log error" do
321
+ exception = StandardError.new('this is an exception')
322
+ metric = ExceptionHandling.default_metric_name({}, exception, false)
323
+ expect(metric).to eq('exception_handling.exception')
324
+ end
325
+
326
+ context "when using log error with treat_like_warning" do
327
+ it "return exception_handling.unforwarded_exception when exception not present" do
328
+ metric = ExceptionHandling.default_metric_name({}, nil, true)
329
+ expect(metric).to eq('exception_handling.unforwarded_exception')
330
+ end
331
+
332
+ it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
333
+ module SomeModule
334
+ class SomeException < StandardError
335
+ end
336
+ end
337
+
338
+ exception = SomeModule::SomeException.new('this is an exception')
339
+ metric = ExceptionHandling.default_metric_name({}, exception, true)
340
+ expect(metric).to eq('exception_handling.unforwarded_exception_SomeException')
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ context "default_honeybadger_metric_name" do
347
+ it "return exception_handling.honeybadger.success when status is :success" do
348
+ metric = ExceptionHandling.default_honeybadger_metric_name(:success)
349
+ expect(metric).to eq('exception_handling.honeybadger.success')
350
+ end
351
+
352
+ it "return exception_handling.honeybadger.failure when status is :failure" do
353
+ metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
354
+ expect(metric).to eq('exception_handling.honeybadger.failure')
355
+ end
356
+
357
+ it "return exception_handling.honeybadger.skipped when status is :skipped" do
358
+ metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
359
+ expect(metric).to eq('exception_handling.honeybadger.skipped')
360
+ end
361
+
362
+ it "return exception_handling.honeybadger.unknown_status when status is not recognized" do
363
+ metric = ExceptionHandling.default_honeybadger_metric_name(nil)
364
+ expect(metric).to eq('exception_handling.honeybadger.unknown_status')
365
+ end
366
+ end
367
+
368
+ context "ExceptionHandling.ensure_safe" do
369
+ it "log an exception with call stack if an exception is raised." do
370
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
371
+ ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
372
+ end
373
+
374
+ if ActionView::VERSION::MAJOR >= 5
375
+ it "log an exception with call stack if an ActionView template exception is raised." do
376
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, anything)
377
+ ExceptionHandling.ensure_safe do
378
+ begin
379
+ # Rails 5 made the switch from ActionView::TemplateError taking in the original exception
380
+ # as an argument to using the $! global to extract the original exception
381
+ raise ArgumentError, "blah"
382
+ rescue
383
+ raise ActionView::TemplateError.new({})
384
+ end
385
+ end
386
+ end
387
+ else
388
+ it "log an exception with call stack if an ActionView template exception is raised." do
389
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, anything)
390
+ ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
391
+ end
392
+ end
393
+
394
+ it "should not log an exception if an exception is not raised." do
395
+ expect(ExceptionHandling.logger).to_not receive(:fatal)
396
+ ExceptionHandling.ensure_safe { ; }
397
+ end
398
+
399
+ it "return its value if used during an assignment" do
400
+ expect(ExceptionHandling.logger).to_not receive(:fatal)
401
+ b = ExceptionHandling.ensure_safe { 5 }
402
+ expect(b).to eq(5)
403
+ end
404
+
405
+ it "return nil if an exception is raised during an assignment" do
406
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
407
+ b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
408
+ expect(b).to be_nil
409
+ end
410
+
411
+ it "allow a message to be appended to the error when logged." do
412
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
413
+ b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
414
+ expect(b).to be_nil
415
+ end
416
+
417
+ it "only rescue StandardError and descendents" do
418
+ expect { ExceptionHandling.ensure_safe("mooo") { raise Exception } }.to raise_exception(Exception)
419
+
420
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nStandardError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
421
+
422
+ b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
423
+ expect(b).to be_nil
424
+ end
425
+ end
426
+
427
+ context "ExceptionHandling.ensure_completely_safe" do
428
+ it "log an exception if an exception is raised." do
429
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
430
+ ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
431
+ end
432
+
433
+ it "should not log an exception if an exception is not raised." do
434
+ expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
435
+ ExceptionHandling.ensure_completely_safe { ; }
436
+ end
437
+
438
+ it "return its value if used during an assignment" do
439
+ expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
440
+ b = ExceptionHandling.ensure_completely_safe { 5 }
441
+ expect(b).to eq(5)
442
+ end
443
+
444
+ it "return nil if an exception is raised during an assignment" do
445
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything) { nil }
446
+ b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
447
+ expect(b).to be_nil
448
+ end
449
+
450
+ it "allow a message to be appended to the error when logged." do
451
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
452
+ b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
453
+ expect(b).to be_nil
454
+ end
455
+
456
+ it "rescue any instance or child of Exception" do
457
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
458
+ ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
459
+ end
460
+
461
+ it "not rescue the special exceptions that Ruby uses" do
462
+ [SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
463
+ expect do
464
+ ExceptionHandling.ensure_completely_safe do
465
+ raise exception
466
+ end
467
+ end.to raise_exception(exception)
468
+ end
469
+ end
470
+ end
471
+
472
+ context "ExceptionHandling.ensure_escalation" do
473
+ before do
474
+ capture_notifications
475
+ ActionMailer::Base.deliveries.clear
476
+ end
477
+
478
+ it "log the exception as usual and send the proper email" do
479
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
480
+ ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
481
+ expect(ActionMailer::Base.deliveries.count).to eq(1)
482
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
483
+
484
+ email = ActionMailer::Base.deliveries.last
485
+ expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
486
+ expect(email.body.to_s).to match('ArgumentError: blah')
487
+ expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
488
+ end
489
+
490
+ it "should not escalate if an exception is not raised." do
491
+ expect(ExceptionHandling.logger).to_not receive(:fatal)
492
+ ExceptionHandling.ensure_escalation('Ignored') { ; }
493
+ expect(ActionMailer::Base.deliveries.count).to eq(0)
494
+ end
495
+
496
+ it "log if the escalation email cannot be sent" do
497
+ expect_any_instance_of(Mail::Message).to receive(:deliver).and_raise(RuntimeError.new, "Delivery Error")
498
+ log_fatals = []
499
+ expect(ExceptionHandling.logger).to receive(:fatal).with(any_args).at_least(:once) do |*args|
500
+ log_fatals << args
501
+ end
502
+
503
+ ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
504
+ expect(log_fatals[0].first).to match(/ArgumentError.*first_test_exception/)
505
+ expect(log_fatals[1].first).to match(/safe_email_deliver.*Delivery Error/m)
506
+
507
+ expect(log_fatals.size).to eq(2), log_fatals.inspect
508
+
509
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect # still sent to honeybadger
510
+ end
511
+
512
+ it "allow the caller to specify custom recipients" do
513
+ custom_recipients = ['something@invoca.com']
514
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
515
+ ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
516
+ expect(ActionMailer::Base.deliveries.count).to eq(1)
517
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
518
+
519
+ email = ActionMailer::Base.deliveries.last
520
+ expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
521
+ expect(email.body.to_s).to match('ArgumentError: blah')
522
+ expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
523
+ expect(email.to).to eq(custom_recipients)
524
+ end
525
+ end
526
+
527
+ context "ExceptionHandling.ensure_alert" do
528
+ it "log the exception as usual and fire a sensu event" do
529
+ expect(ExceptionHandling::Sensu).to receive(:generate_event).with("Favorite Feature", "test context\nblah")
530
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
531
+ ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
532
+ end
533
+
534
+ it "should not send sensu event if an exception is not raised." do
535
+ expect(ExceptionHandling.logger).to_not receive(:fatal)
536
+ expect(ExceptionHandling::Sensu).to_not receive(:generate_event)
537
+ ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
538
+ end
539
+
540
+ it "log if the sensu event could not be sent" do
541
+ expect(ExceptionHandling::Sensu).to receive(:send_event).with(anything) { raise "Failed to send" }
542
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/first_test_exception/, anything)
543
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to send/, anything)
544
+ ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
545
+ end
546
+
547
+ it "log if the exception message is nil" do
548
+ expect(ExceptionHandling::Sensu).to receive(:generate_event).with("some alert", "test context\n")
549
+ ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
550
+ end
551
+ end
552
+
553
+ context "ExceptionHandling.escalate_to_production_support" do
554
+ it "notify production support" do
555
+ subject = "Runtime Error found!"
556
+ exception = RuntimeError.new("Test")
557
+ recipients = ["prodsupport@example.com"]
558
+
559
+ expect(ExceptionHandling).to receive(:production_support_recipients).and_return(recipients).exactly(2)
560
+ expect(ExceptionHandling).to receive(:escalate).with(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
561
+ ExceptionHandling.escalate_to_production_support(exception, subject)
562
+ end
563
+ end
564
+
565
+ context "exception timestamp" do
566
+ before do
567
+ Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
568
+ end
569
+
570
+ it "include the timestamp when the exception is logged" do
571
+ capture_notifications
572
+
573
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:517033020\) context\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
574
+ b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
575
+ expect(b).to be_nil
576
+
577
+ expect(ExceptionHandling.last_exception_timestamp).to eq(517_033_020)
578
+
579
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
580
+
581
+ expect(sent_notifications.last.enhanced_data['timestamp']).to eq(517_033_020)
582
+ end
583
+ end
584
+
585
+ it "log the error if the exception message is nil" do
586
+ capture_notifications
587
+
588
+ ExceptionHandling.log_error(exception_with_nil_message)
589
+
590
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
591
+ expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
592
+ end
593
+
594
+ it "log the error if the exception message is nil and the exception context is a hash" do
595
+ capture_notifications
596
+
597
+ ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
598
+
599
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
600
+ expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
601
+ end
602
+
603
+ context "Honeybadger integration" do
604
+ context "with Honeybadger not defined" do
605
+ before do
606
+ allow(ExceptionHandling).to receive(:honeybadger_defined?) { false }
607
+ end
608
+
609
+ it "not invoke send_exception_to_honeybadger when log_error is executed" do
610
+ expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
611
+ ExceptionHandling.log_error(exception_1)
612
+ end
613
+
614
+ it "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
615
+ expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
616
+ ExceptionHandling.ensure_safe { raise exception_1 }
617
+ end
618
+ end
619
+
620
+ context "with Honeybadger defined" do
621
+ it "not send_exception_to_honeybadger when log_warning is executed" do
622
+ expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
623
+ ExceptionHandling.log_warning("This should not go to honeybadger")
624
+ end
625
+
626
+ it "not send_exception_to_honeybadger when log_error is called with a Warning" do
627
+ expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
628
+ ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
629
+ end
630
+
631
+ it "invoke send_exception_to_honeybadger when log_error is executed" do
632
+ expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
633
+ ExceptionHandling.log_error(exception_1)
634
+ end
635
+
636
+ it "invoke send_exception_to_honeybadger when log_error_rack is executed" do
637
+ expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
638
+ ExceptionHandling.log_error_rack(exception_1, {}, nil)
639
+ end
640
+
641
+ it "invoke send_exception_to_honeybadger when ensure_safe is executed" do
642
+ expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
643
+ ExceptionHandling.ensure_safe { raise exception_1 }
644
+ end
645
+
646
+ it "specify error message as an empty string when notifying honeybadger if exception message is nil" do
647
+ expect(Honeybadger).to receive(:notify).with(any_args) do |args|
648
+ expect(args[:error_message]).to eq("")
649
+ end
650
+ ExceptionHandling.log_error(exception_with_nil_message)
651
+ end
652
+
653
+ context "with stubbed values" do
654
+ before do
655
+ Time.now_override = Time.now
656
+ @env = { server: "fe98" }
657
+ @parameters = { advertiser_id: 435, controller: "some_controller" }
658
+ @session = { username: "jsmith" }
659
+ @request_uri = "host/path"
660
+ @controller = create_dummy_controller(@env, @parameters, @session, @request_uri)
661
+ allow(ExceptionHandling).to receive(:server_name) { "invoca_fe98" }
662
+
663
+ @exception = StandardError.new("Some Exception")
664
+ @exception.set_backtrace([
665
+ "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
666
+ "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
667
+ ])
668
+ @exception_context = { "SERVER_NAME" => "exceptional.com" }
669
+ end
670
+
671
+ it "send error details and relevant context data to Honeybadger with log_context" do
672
+ honeybadger_data = nil
673
+ expect(Honeybadger).to receive(:notify).with(any_args) do |data|
674
+ honeybadger_data = data
675
+ end
676
+ ExceptionHandling.logger.global_context = { service_name: "rails", region: "AWS-us-east-1" }
677
+ log_context = { log_source: "gem/listen", service_name: "bin/console" }
678
+ ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
679
+ data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
680
+ data[:user_details] = { username: "jsmith" }
681
+ data[:event_response] = "Event successfully received"
682
+ data[:other_section] = "This should not be included in the response"
683
+ end
684
+
685
+ expected_data = {
686
+ error_class: :"Test Exception",
687
+ error_message: "Some Exception",
688
+ controller: "some_controller",
689
+ exception: @exception,
690
+ context: {
691
+ timestamp: Time.now.to_i,
692
+ error_class: "StandardError",
693
+ server: "invoca_fe98",
694
+ exception_context: { "SERVER_NAME" => "exceptional.com" },
695
+ scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
696
+ notes: "this is used by a test",
697
+ user_details: { "username" => "jsmith" },
698
+ request: {
699
+ "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
700
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
701
+ "url" => "host/path"
702
+ },
703
+ session: {
704
+ "key" => nil,
705
+ "data" => { "username" => "jsmith" }
706
+ },
707
+ environment: {
708
+ "SERVER_NAME" => "exceptional.com"
709
+ },
710
+ backtrace: [
711
+ "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
712
+ "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
713
+ ],
714
+ event_response: "Event successfully received",
715
+ log_context: { "service_name" => "bin/console", "region" => "AWS-us-east-1", "log_source" => "gem/listen" }
716
+ }
717
+ }
718
+ expect(honeybadger_data).to eq(expected_data)
719
+ end
720
+
721
+ it "send error details and relevant context data to Honeybadger with empty log_context" do
722
+ honeybadger_data = nil
723
+ expect(Honeybadger).to receive(:notify).with(any_args) do |data|
724
+ honeybadger_data = data
725
+ end
726
+ ExceptionHandling.logger.global_context = {}
727
+ log_context = {}
728
+ ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
729
+ data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
730
+ data[:user_details] = { username: "jsmith" }
731
+ data[:event_response] = "Event successfully received"
732
+ data[:other_section] = "This should not be included in the response"
733
+ end
734
+
735
+ expected_data = {
736
+ error_class: :"Test Exception",
737
+ error_message: "Some Exception",
738
+ controller: "some_controller",
739
+ exception: @exception,
740
+ context: {
741
+ timestamp: Time.now.to_i,
742
+ error_class: "StandardError",
743
+ server: "invoca_fe98",
744
+ exception_context: { "SERVER_NAME" => "exceptional.com" },
745
+ scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
746
+ notes: "this is used by a test",
747
+ user_details: { "username" => "jsmith" },
748
+ request: {
749
+ "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
750
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
751
+ "url" => "host/path"
752
+ },
753
+ session: {
754
+ "key" => nil,
755
+ "data" => { "username" => "jsmith" }
756
+ },
757
+ environment: {
758
+ "SERVER_NAME" => "exceptional.com"
759
+ },
760
+ backtrace: [
761
+ "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
762
+ "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
763
+ ],
764
+ event_response: "Event successfully received"
765
+ }
766
+ }
767
+ expect(honeybadger_data).to eq(expected_data)
768
+ end
769
+ end
770
+
771
+ context "with post_log_error_hook set" do
772
+ after do
773
+ ExceptionHandling.post_log_error_hook = nil
774
+ end
775
+
776
+ it "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
777
+ @honeybadger_status = nil
778
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
779
+ filter_list = {
780
+ NoHoneybadger: {
781
+ error: "suppress Honeybadger notification",
782
+ send_to_honeybadger: false
783
+ }
784
+ }
785
+ allow(File).to receive(:mtime) { incrementing_mtime }
786
+ expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
787
+
788
+ expect(ExceptionHandling).to receive(:send_exception_to_honeybadger_unless_filtered).with(any_args).exactly(1).and_call_original
789
+ expect(Honeybadger).to_not receive(:notify)
790
+ ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
791
+ expect(@honeybadger_status).to eq(:skipped)
792
+ end
793
+
794
+ it "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
795
+ @honeybadger_status = nil
796
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
797
+ expect(Honeybadger).to receive(:notify).with(any_args) { raise "Honeybadger Notification Failure" }
798
+ ExceptionHandling.log_error(exception_1)
799
+ expect(@honeybadger_status).to eq(:failure)
800
+ end
801
+
802
+ it "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
803
+ @honeybadger_status = nil
804
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
805
+ expect(Honeybadger).to receive(:notify).with(any_args) { false }
806
+ ExceptionHandling.log_error(exception_1)
807
+ expect(@honeybadger_status).to eq(:failure)
808
+ end
809
+
810
+ it "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
811
+ @honeybadger_status = nil
812
+ ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
813
+ expect(Honeybadger).to receive(:notify).with(any_args) { '06220c5a-b471-41e5-baeb-de247da45a56' }
814
+ ExceptionHandling.log_error(exception_1)
815
+ expect(@honeybadger_status).to eq(:success)
816
+ end
817
+ end
818
+ end
819
+ end
820
+
821
+ class EventResponse
822
+ def to_s
823
+ "message from to_s!"
824
+ end
825
+ end
826
+
827
+ it "allow sections to have data with just a to_s method" do
828
+ capture_notifications
829
+
830
+ ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
831
+ data.merge!(event_response: EventResponse.new)
832
+ end
833
+
834
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
835
+ expect(sent_notifications.last.enhanced_data['event_response'].to_s).to match(/message from to_s!/)
836
+ end
837
+ end
838
+
839
+ it "return the error ID (timestamp)" do
840
+ result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
841
+ expect(result).to eq(ExceptionHandling.last_exception_timestamp)
842
+ end
843
+
844
+ it "rescue exceptions that happen in log_error" do
845
+ allow(ExceptionHandling).to receive(:make_exception) { raise ArgumentError, "Bad argument" }
846
+ expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
847
+ satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
848
+ anything)
849
+ ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
850
+ end
851
+
852
+ it "rescue exceptions that happen when log_error yields" do
853
+ expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
854
+ satisfy { |context| context['Context message'] },
855
+ anything,
856
+ anything)
857
+ ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
858
+ end
859
+
860
+ context "Exception Filtering" do
861
+ before do
862
+ filter_list = { exception1: { 'error' => "my error message" },
863
+ exception2: { 'error' => "some other message", :session => "misc data" } }
864
+ allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
865
+
866
+ # bump modified time up to get the above filter loaded
867
+ allow(File).to receive(:mtime) { incrementing_mtime }
868
+ end
869
+
870
+ it "handle case where filter list is not found" do
871
+ allow(YAML).to receive(:load_file) { raise Errno::ENOENT, "File not found" }
872
+
873
+ capture_notifications
874
+
875
+ ExceptionHandling.log_error("My error message is in list")
876
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
877
+ end
878
+
879
+ it "log exception and suppress email when exception is on filter list" do
880
+ capture_notifications
881
+
882
+ ExceptionHandling.log_error("Error message is not in list")
883
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
884
+
885
+ sent_notifications.clear
886
+ ExceptionHandling.log_error("My error message is in list")
887
+ expect(sent_notifications.size).to eq(0), sent_notifications.inspect
888
+ end
889
+
890
+ it "allow filtering exception on any text in exception data" do
891
+ filters = { exception1: { session: "data: my extra session data" } }
892
+ allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filters) }
893
+
894
+ capture_notifications
895
+
896
+ ExceptionHandling.log_error("No match here") do |data|
897
+ data[:session] = {
898
+ key: "@session_id",
899
+ data: "my extra session data"
900
+ }
901
+ end
902
+ expect(sent_notifications.size).to eq(0), sent_notifications.inspect
903
+
904
+ ExceptionHandling.log_error("No match here") do |data|
905
+ data[:session] = {
906
+ key: "@session_id",
907
+ data: "my extra session <no match!> data"
908
+ }
909
+ end
910
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
911
+ end
912
+
913
+ it "reload filter list on the next exception if file was modified" do
914
+ capture_notifications
915
+
916
+ ExceptionHandling.log_error("Error message is not in list")
917
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
918
+
919
+ filter_list = { exception1: { 'error' => "Error message is not in list" } }
920
+ allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
921
+ allow(File).to receive(:mtime) { incrementing_mtime }
922
+
923
+ sent_notifications.clear
924
+ ExceptionHandling.log_error("Error message is not in list")
925
+ expect(sent_notifications.size).to eq(0), sent_notifications.inspect
926
+ end
927
+
928
+ it "not consider filter if both error message and body do not match" do
929
+ capture_notifications
930
+
931
+ # error message matches, but not full text
932
+ ExceptionHandling.log_error("some other message")
933
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
934
+
935
+ # now both match
936
+ sent_notifications.clear
937
+ ExceptionHandling.log_error("some other message") do |data|
938
+ data[:session] = { some_random_key: "misc data" }
939
+ end
940
+ expect(sent_notifications.size).to eq(0), sent_notifications.inspect
941
+ end
942
+
943
+ it "skip environment keys not on whitelist" do
944
+ capture_notifications
945
+
946
+ ExceptionHandling.log_error("some message") do |data|
947
+ data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
948
+ end
949
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
950
+
951
+ mail = sent_notifications.last
952
+ environment = mail.enhanced_data['environment']
953
+
954
+ expect(environment["RAILS_SECRETS_YML_CONTENTS"]).to be_nil, environment.inspect # this is not on whitelist).to be_nil
955
+ expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this is
956
+ end
957
+
958
+ it "omit environment defaults" do
959
+ capture_notifications
960
+
961
+ allow(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(anything) { |exception_info| sent_notifications << exception_info }
962
+
963
+ ExceptionHandling.log_error("some message") do |data|
964
+ data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
965
+ end
966
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
967
+ mail = sent_notifications.last
968
+ environment = mail.enhanced_data['environment']
969
+
970
+ expect(environment["SERVER_PORT"]).to be_nil, environment.inspect # this was default).to be_nil
971
+ expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this was not
972
+ end
973
+
974
+ it "reject the filter file if any contain all empty regexes" do
975
+ filter_list = { exception1: { 'error' => "", :session => "" },
976
+ exception2: { 'error' => "is not in list", :session => "" } }
977
+ allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
978
+ allow(File).to receive(:mtime) { incrementing_mtime }
979
+
980
+ capture_notifications
981
+
982
+ ExceptionHandling.log_error("Error message is not in list")
983
+ expect(sent_notifications.size).to eq(1), sent_notifications.inspect
984
+ end
985
+
986
+ it "reload filter file if filename changes" do
987
+ catalog = ExceptionHandling.exception_catalog
988
+ ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
989
+ expect(ExceptionHandling.exception_catalog).to_not eq(catalog)
990
+ end
991
+
992
+ context "Exception Handling Mailer" do
993
+ EXPECTED_SMTP_HASH =
994
+ {
995
+ host: '127.0.0.1',
996
+ domain: 'localhost.localdomain',
997
+ from: 'server@example.com',
998
+ to: 'escalation@example.com'
999
+ }.freeze
1000
+
1001
+ [[true, false], [true, true]].each do |em_flag, synchrony_flag|
1002
+ context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
1003
+ before do
1004
+ ExceptionHandling.eventmachine_safe = em_flag
1005
+ ExceptionHandling.eventmachine_synchrony = synchrony_flag
1006
+ EventMachineStub.block = nil
1007
+ set_test_const('EventMachine', EventMachineStub)
1008
+ set_test_const('EventMachine::Protocols', Module.new)
1009
+ set_test_const('EventMachine::DNS', Module.new)
1010
+ set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
1011
+ end
1012
+
1013
+ after do
1014
+ ExceptionHandling.eventmachine_safe = false
1015
+ ExceptionHandling.eventmachine_synchrony = false
1016
+ end
1017
+
1018
+ it "schedule EventMachine STMP when EventMachine defined" do
1019
+ ActionMailer::Base.deliveries.clear
1020
+
1021
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
1022
+
1023
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1024
+ expect(EventMachineStub.block).to be_truthy
1025
+ EventMachineStub.block.call
1026
+ expect(DNSResolvStub.callback_block).to be_truthy
1027
+ DNSResolvStub.callback_block.call ['127.0.0.1']
1028
+ expect((SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }) .to eq(EXPECTED_SMTP_HASH), SmtpClientStub.send_hash.inspect
1029
+ expect(SmtpClientStub.last_method).to eq((synchrony_flag ? :asend : :send))
1030
+ expect(SmtpClientStub.send_hash[:content]).to match(/Exception to escalate/)
1031
+ assert_emails 0, ActionMailer::Base.deliveries.*.to_s
1032
+ end
1033
+
1034
+ it "pass the content as a proper rfc 2822 message" do
1035
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
1036
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1037
+ expect(EventMachineStub.block).to be_truthy
1038
+ EventMachineStub.block.call
1039
+ expect(DNSResolvStub.callback_block).to be_truthy
1040
+ DNSResolvStub.callback_block.call ['127.0.0.1']
1041
+ expect(content = SmtpClientStub.send_hash[:content]).to be_truthy
1042
+ expect(content).to match(/Content-Transfer-Encoding: 7bit/)
1043
+ expect(content).to match(/\r\n\.\r\n\z/)
1044
+ end
1045
+
1046
+ it "log fatal on EventMachine STMP errback" do
1047
+ ActionMailer::Base.deliveries.clear
1048
+
1049
+ set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
1050
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, anything)
1051
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to email by SMTP: "credential mismatch"/)
1052
+
1053
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1054
+ expect(EventMachineStub.block).to be_truthy
1055
+ EventMachineStub.block.call
1056
+ expect(DNSResolvStub.callback_block).to be_truthy
1057
+ DNSResolvStub.callback_block.call(['127.0.0.1'])
1058
+ SmtpClientErrbackStub.block.call("credential mismatch")
1059
+ expect((SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }).to eq(EXPECTED_SMTP_HASH), SmtpClientErrbackStub.send_hash.inspect end
1060
+
1061
+ it "log fatal on EventMachine dns resolver errback" do
1062
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, anything)
1063
+ expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to resolv DNS for localhost: "softlayer sucks"/)
1064
+
1065
+ ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1066
+ expect(EventMachineStub.block).to be_truthy
1067
+ EventMachineStub.block.call
1068
+ DNSResolvStub.errback_block.call("softlayer sucks")
1069
+ end
1070
+ end
1071
+ end
1072
+ end
1073
+ end
1074
+
1075
+ context "Exception mapping" do
1076
+ before do
1077
+ @data = {
1078
+ environment: {
1079
+ 'HTTP_HOST' => "localhost",
1080
+ 'HTTP_REFERER' => "http://localhost/action/controller/instance",
1081
+ },
1082
+ session: {
1083
+ data: {
1084
+ affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
1085
+ edit_mode: true,
1086
+ advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
1087
+ username_id: defined?(Username) ? Username.first.id : '1',
1088
+ user_id: defined?(User) ? User.first.id : '1',
1089
+ flash: {},
1090
+ impersonated_organization_pk: 'Advertiser_1'
1091
+ }
1092
+ },
1093
+ request: {},
1094
+ 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'"],
1095
+ api_key: "none",
1096
+ error_class: "StandardError",
1097
+ error: 'Some error message'
1098
+ }
1099
+ end
1100
+
1101
+ it "clean backtraces" do
1102
+ begin
1103
+ raise "test exception"
1104
+ rescue => ex
1105
+ backtrace = ex.backtrace
1106
+ end
1107
+ result = ExceptionHandling.send(:clean_backtrace, ex).to_s
1108
+ expect(backtrace).to_not eq(result)
1109
+ end
1110
+
1111
+ it "return entire backtrace if cleaned is emtpy" do
1112
+ begin
1113
+ backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
1114
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
1115
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
1116
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
1117
+ "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
1118
+ "/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. '",
1119
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
1120
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
1121
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
1122
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
1123
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
1124
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
1125
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
1126
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
1127
+ "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
1128
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
1129
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
1130
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
1131
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
1132
+ "(eval):12:in `run_suite'",
1133
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
1134
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
1135
+ "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
1136
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
1137
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
1138
+ "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
1139
+ "-e:1"]
1140
+
1141
+ module ::Rails
1142
+ class BacktraceCleaner
1143
+ def clean(_backtrace)
1144
+ []
1145
+ end
1146
+ end
1147
+ end
1148
+
1149
+ rails = double(Rails)
1150
+ expect(rails).to receive(:backtrace_cleaner) { Rails::BacktraceCleaner.new }
1151
+ rails.backtrace_cleaner
1152
+
1153
+ ex = Exception.new
1154
+ ex.set_backtrace(backtrace)
1155
+ result = ExceptionHandling.send(:clean_backtrace, ex)
1156
+ expect(result).to eq(backtrace)
1157
+ ensure
1158
+ Object.send(:remove_const, :Rails)
1159
+ end
1160
+ end
1161
+ end
1162
+
1163
+ context "log_perodically" do
1164
+ before do
1165
+ Time.now_override = Time.now # Freeze time
1166
+ ExceptionHandling.logger.clear
1167
+ end
1168
+
1169
+ after do
1170
+ Time.now_override = nil
1171
+ end
1172
+
1173
+ it "take in additional logging context and pass them to the logger" do
1174
+ ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
1175
+ expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
1176
+ expect(logged_excluding_reload_filter.last[:context]).to eq({ service_name: 'exception_handling' })
1177
+ end
1178
+
1179
+ it "log immediately when we are expected to log" do
1180
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1181
+ expect(logged_excluding_reload_filter.size).to eq(1)
1182
+
1183
+ Time.now_override = Time.now + 5.minutes
1184
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
1185
+ expect(logged_excluding_reload_filter.size).to eq(1)
1186
+
1187
+ ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
1188
+ expect(logged_excluding_reload_filter.size).to eq(2)
1189
+
1190
+ Time.now_override = Time.now + 26.minutes
1191
+
1192
+ ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1193
+ expect(logged_excluding_reload_filter.size).to eq(3)
1194
+ end
1195
+ end
1196
+ end
1197
+
1198
+ private
1199
+
1200
+ def logged_excluding_reload_filter
1201
+ ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
1202
+ end
1203
+
1204
+ def incrementing_mtime
1205
+ @mtime ||= Time.now
1206
+ @mtime += 1.day
1207
+ end
1208
+
1209
+ def exception_1
1210
+ @exception_1 ||=
1211
+ begin
1212
+ raise StandardError, "Exception 1"
1213
+ rescue => ex
1214
+ ex
1215
+ end
1216
+ end
1217
+
1218
+ def exception_2
1219
+ @exception_2 ||=
1220
+ begin
1221
+ raise StandardError, "Exception 2"
1222
+ rescue => ex
1223
+ ex
1224
+ end
1225
+ end
1226
+ end