exception_handling 1.2.1 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +142 -102
  5. data/README.md +11 -2
  6. data/config/exception_filters.yml +2 -0
  7. data/exception_handling.gemspec +6 -10
  8. data/lib/exception_handling.rb +222 -313
  9. data/lib/exception_handling/exception_catalog.rb +8 -6
  10. data/lib/exception_handling/exception_description.rb +8 -6
  11. data/lib/exception_handling/exception_info.rb +272 -0
  12. data/lib/exception_handling/honeybadger_callbacks.rb +42 -0
  13. data/lib/exception_handling/log_stub_error.rb +18 -3
  14. data/lib/exception_handling/mailer.rb +14 -0
  15. data/lib/exception_handling/methods.rb +26 -8
  16. data/lib/exception_handling/testing.rb +2 -2
  17. data/lib/exception_handling/version.rb +3 -1
  18. data/test/helpers/controller_helpers.rb +27 -0
  19. data/test/helpers/exception_helpers.rb +11 -0
  20. data/test/test_helper.rb +42 -19
  21. data/test/unit/exception_handling/exception_catalog_test.rb +19 -0
  22. data/test/unit/exception_handling/exception_description_test.rb +12 -1
  23. data/test/unit/exception_handling/exception_info_test.rb +501 -0
  24. data/test/unit/exception_handling/honeybadger_callbacks_test.rb +85 -0
  25. data/test/unit/exception_handling/log_error_stub_test.rb +26 -3
  26. data/test/unit/exception_handling/mailer_test.rb +39 -14
  27. data/test/unit/exception_handling/methods_test.rb +40 -18
  28. data/test/unit/exception_handling_test.rb +947 -539
  29. data/views/exception_handling/mailer/escalate_custom.html.erb +17 -0
  30. data/views/exception_handling/mailer/exception_notification.html.erb +1 -1
  31. data/views/exception_handling/mailer/log_parser_exception_notification.html.erb +1 -1
  32. metadata +28 -60
@@ -58,8 +58,8 @@ module ExceptionHandling
58
58
  end
59
59
 
60
60
  include ExceptionHandling::Methods
61
-
61
+ set_long_controller_action_timeout 2
62
62
  end
63
63
 
64
64
  end
65
- end
65
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionHandling
2
- VERSION = "1.2.1"
4
+ VERSION = '2.2.1'
3
5
  end
@@ -0,0 +1,27 @@
1
+ module ControllerHelpers
2
+ DummyController = Struct.new(:complete_request_uri, :request, :session)
3
+ DummyRequest = Struct.new(:env, :parameters, :session_options)
4
+
5
+ class DummySession
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ def [](key)
11
+ @data[key]
12
+ end
13
+
14
+ def []=(key, value)
15
+ @data[key] = value
16
+ end
17
+
18
+ def to_hash
19
+ @data
20
+ end
21
+ end
22
+
23
+ def create_dummy_controller(env, parameters, session, request_uri)
24
+ request = DummyRequest.new(env, parameters, DummySession.new(session))
25
+ DummyController.new(request_uri, request, DummySession.new(session))
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module ExceptionHelpers
2
+ def raise_exception_with_nil_message
3
+ raise exception_with_nil_message
4
+ end
5
+
6
+ def exception_with_nil_message
7
+ exception_with_nil_message = RuntimeError.new(nil)
8
+ stub(exception_with_nil_message).message { nil }
9
+ exception_with_nil_message
10
+ end
11
+ end
@@ -1,39 +1,46 @@
1
1
  require 'active_support'
2
2
  require 'active_support/time'
3
3
  require 'active_support/test_case'
4
+ require 'active_model'
4
5
  require 'action_mailer'
6
+ require 'action_dispatch'
5
7
  require 'hobo_support'
6
8
  require 'shoulda'
7
9
  require 'rr'
8
10
  require 'minitest/autorun'
9
11
  require 'pry'
12
+ require 'honeybadger'
13
+ require 'contextual_logger'
10
14
 
11
15
  require 'exception_handling'
12
16
  require 'exception_handling/testing'
13
17
 
14
- class LoggerStub
15
- attr_accessor :logged
18
+ ActiveSupport::TestCase.test_order = :sorted
16
19
 
17
- def initialize
18
- clear
19
- end
20
+ class LoggerStub
21
+ include ContextualLogger
22
+ attr_accessor :logged
20
23
 
21
- def info(message)
22
- logged << message
23
- end
24
+ def initialize
25
+ clear
26
+ end
24
27
 
25
- def warn(message)
26
- logged << message
27
- end
28
+ def info(message, **log_context)
29
+ logged << { message: message, context: log_context }
30
+ end
28
31
 
29
- def fatal(message)
30
- logged << message
31
- end
32
+ def warn(message, **log_context)
33
+ logged << { message: message, context: log_context }
34
+ end
32
35
 
33
- def clear
34
- @logged = []
35
- end
36
- end
36
+ def fatal(message, **log_context)
37
+ logged << { message: message, context: log_context }
38
+ end
39
+
40
+ def clear
41
+ @logged = []
42
+ end
43
+ end
37
44
 
38
45
  class SocketStub
39
46
  attr_accessor :sent, :connected
@@ -56,7 +63,7 @@ class SocketStub
56
63
  end
57
64
 
58
65
  def closed?
59
- !@connceted
66
+ !@connected
60
67
  end
61
68
  end
62
69
 
@@ -79,6 +86,18 @@ class ActiveSupport::TestCase
79
86
  raise "Uh-oh! constant_overrides left over: #{@@constant_overrides.inspect}"
80
87
  end
81
88
 
89
+ unless defined?(Rails) && defined?(Rails.env)
90
+ module ::Rails
91
+ def self.env
92
+ @env ||= 'test'
93
+ end
94
+
95
+ def self.env=(mode)
96
+ @env = mode
97
+ end
98
+ end
99
+ end
100
+
82
101
  Time.now_override = nil
83
102
 
84
103
  ActionMailer::Base.deliveries.clear
@@ -149,6 +168,10 @@ def assert_equal_with_diff arg1, arg2, msg = ''
149
168
  end
150
169
  end
151
170
 
171
+ def require_test_helper(helper_path)
172
+ require_relative "helpers/#{helper_path}"
173
+ end
174
+
152
175
  class Time
153
176
  class << self
154
177
  attr_reader :now_override
@@ -35,6 +35,16 @@ module ExceptionHandling
35
35
  end
36
36
  end
37
37
 
38
+ should "write errors loading the yaml file directly to the log file" do
39
+ @exception_catalog = ExceptionCatalog.new( ExceptionHandling.filter_list_filename )
40
+
41
+ mock(ExceptionHandling).log_error.never
42
+ mock(ExceptionHandling).write_exception_to_log(anything(), "ExceptionCatalog#refresh_filters: ./config/exception_filters.yml", anything())
43
+ mock(@exception_catalog).load_file { raise "noooooo"}
44
+
45
+ @exception_catalog.find({})
46
+ end
47
+
38
48
  end
39
49
 
40
50
  context "with live yaml content" do
@@ -50,8 +60,17 @@ module ExceptionHandling
50
60
  assert !@exception_catalog.find( error: "Scott says unlikely to ever match" )
51
61
  assert !@exception_catalog.find( error: "Scott says unlikely to ever match" )
52
62
  end
63
+ end
53
64
 
65
+ context "with no yaml content" do
66
+ setup do
67
+ @exception_catalog = ExceptionCatalog.new(nil)
68
+ end
54
69
 
70
+ should "not load filter data" do
71
+ mock(ExceptionHandling).write_exception_to_log.with_any_args.never
72
+ @exception_catalog.find( error: "Scott says unlikely to ever match" )
73
+ end
55
74
  end
56
75
 
57
76
  private
@@ -15,6 +15,11 @@ module ExceptionHandling
15
15
  assert @f.match?( :error => "my error message")
16
16
  end
17
17
 
18
+ should "allow wildcards to cross line boundries" do
19
+ @f = ExceptionDescription.new(:filter1, :error => "my error message.*with multiple lines" )
20
+ assert @f.match?( :error => "my error message\nwith more than one, with multiple lines")
21
+ end
22
+
18
23
  should "complain when no regexps have a value" do
19
24
  assert_raise(ArgumentError, "has all blank regexe") { ExceptionDescription.new(:filter1, error: nil) }
20
25
  end
@@ -29,6 +34,12 @@ module ExceptionHandling
29
34
  assert !ExceptionDescription.new(:filter1, error: "my error message" ).send_email
30
35
  end
31
36
 
37
+ should "allow send_to_honeybadger to be specified and have it disabled by default" do
38
+ assert !ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: false).send_to_honeybadger
39
+ assert ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: true).send_to_honeybadger
40
+ assert !ExceptionDescription.new(:filter1, error: "my error message").send_to_honeybadger
41
+ end
42
+
32
43
  should "allow send_metric to be configured" do
33
44
  assert !ExceptionDescription.new(:filter1, error: "my error message", send_metric: false ).send_metric
34
45
  assert ExceptionDescription.new(:filter1, error: "my error message", send_email: true ).send_metric
@@ -46,7 +57,7 @@ module ExceptionHandling
46
57
  end
47
58
 
48
59
  should "allow notes to be recorded" do
49
- assert_equal nil, ExceptionDescription.new(:filter1, error: "my error message" ).notes
60
+ assert_nil ExceptionDescription.new(:filter1, error: "my error message" ).notes
50
61
  assert_equal "a long string", ExceptionDescription.new(:filter1, error: "my error message", notes: "a long string" ).notes
51
62
  end
52
63
 
@@ -0,0 +1,501 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+ require_test_helper 'controller_helpers'
3
+ require_test_helper 'exception_helpers'
4
+
5
+ module ExceptionHandling
6
+ class ExceptionInfoTest < ActiveSupport::TestCase
7
+ include ControllerHelpers
8
+ include ExceptionHelpers
9
+
10
+ context "initialize" do
11
+ setup do
12
+ @exception = StandardError.new("something went wrong")
13
+ @timestamp = Time.now
14
+ @controller = Object.new
15
+ end
16
+
17
+ context "controller_from_context" do
18
+ should "extract controller from context when not specified explicitly" do
19
+ exception_context = {
20
+ "action_controller.instance" => @controller
21
+ }
22
+ exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
23
+ assert_equal @controller, exception_info.controller
24
+ end
25
+
26
+ should "prefer the explicit controller over the one from context" do
27
+ exception_context = {
28
+ "action_controller.instance" => Object.new
29
+ }
30
+ exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp, @controller)
31
+ assert_equal @controller, exception_info.controller
32
+ assert_not_equal exception_context["action_controller.instance"], exception_info.controller
33
+ end
34
+
35
+ should "leave controller unset when not included in the context hash" do
36
+ exception_info = ExceptionInfo.new(@exception, {}, @timestamp)
37
+ assert_nil exception_info.controller
38
+ end
39
+
40
+ should "leave controller unset when context is not in hash format" do
41
+ exception_info = ExceptionInfo.new(@exception, "string context", @timestamp)
42
+ assert_nil exception_info.controller
43
+ end
44
+ end
45
+ end
46
+
47
+ context "data" do
48
+ setup do
49
+ @exception = StandardError.new("something went wrong")
50
+ @timestamp = Time.now
51
+ end
52
+
53
+ should "return a hash with exception specific data including context hash" do
54
+ exception_context = {
55
+ "rack.session" => {
56
+ user_id: 23,
57
+ user_name: "John"
58
+ }
59
+ }
60
+
61
+ exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
62
+ expected_data = {
63
+ "error_class" => "StandardError",
64
+ "error_string" => "StandardError: something went wrong",
65
+ "timestamp" => @timestamp,
66
+ "backtrace" => ["<no backtrace>"],
67
+ "error" => "StandardError: something went wrong",
68
+ "session" => { "user_id" => 23, "user_name" => "John" },
69
+ "environment" => {
70
+ "rack.session" => { "user_id" => 23, "user_name" => "John" }
71
+ }
72
+ }
73
+
74
+ assert_equal_with_diff expected_data, exception_info.data
75
+ end
76
+
77
+ should "generate exception data appropriately if exception message is nil" do
78
+ exception_info = ExceptionInfo.new(exception_with_nil_message, "custom context data", @timestamp)
79
+ exception_data = exception_info.data
80
+ assert_equal "RuntimeError: ", exception_data["error_string"]
81
+ assert_equal "RuntimeError: : custom context data", exception_data["error"]
82
+ end
83
+
84
+ should "return a hash with exception specific data including context string" do
85
+ exception_context = "custom context data"
86
+ exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
87
+ expected_data = {
88
+ "error_class" => "StandardError",
89
+ "error_string" => "StandardError: something went wrong",
90
+ "timestamp" => @timestamp,
91
+ "backtrace" => ["<no backtrace>"],
92
+ "error" => "StandardError: something went wrong: custom context data",
93
+ "environment" => {
94
+ "message" => "custom context data"
95
+ }
96
+ }
97
+
98
+ assert_equal_with_diff expected_data, exception_info.data
99
+ end
100
+
101
+ should "not include enhanced data from controller or custom data callback" do
102
+ env = { server: "fe98" }
103
+ parameters = { advertiser_id: 435 }
104
+ session = { username: "jsmith" }
105
+ request_uri = "host/path"
106
+ controller = create_dummy_controller(env, parameters, session, request_uri)
107
+ data_callback = ->(data) { data[:custom_section] = "value" }
108
+ exception_info = ExceptionInfo.new(@exception, "custom context data", @timestamp, controller, data_callback)
109
+
110
+ dont_allow(exception_info).extract_and_merge_controller_data
111
+ dont_allow(exception_info).customize_from_data_callback
112
+ expected_data = {
113
+ "error_class" => "StandardError",
114
+ "error_string" => "StandardError: something went wrong",
115
+ "timestamp" => @timestamp,
116
+ "backtrace" => ["<no backtrace>"],
117
+ "error" => "StandardError: something went wrong: custom context data",
118
+ "environment" => {
119
+ "message" => "custom context data"
120
+ }
121
+ }
122
+
123
+ assert_equal_with_diff expected_data, exception_info.data
124
+ end
125
+ end
126
+
127
+ context "enhanced_data" do
128
+ setup do
129
+ @exception = StandardError.new("something went wrong")
130
+ @timestamp = Time.now
131
+ @exception_context = {
132
+ "rack.session" => {
133
+ user_id: 23,
134
+ user_name: "John"
135
+ },
136
+ "SERVER_NAME" => "exceptional.com"
137
+ }
138
+ env = { server: "fe98" }
139
+ parameters = { advertiser_id: 435, controller: "dummy", action: "fail" }
140
+ session = { username: "jsmith", id: "session_key" }
141
+ request_uri = "host/path"
142
+ @controller = create_dummy_controller(env, parameters, session, request_uri)
143
+ @data_callback = ->(data) { data[:custom_section] = "check this out" }
144
+ end
145
+
146
+ should "not return a mutable object for the session" do
147
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
148
+ exception_info.enhanced_data["session"]["hello"] = "world"
149
+ assert_nil @controller.session["hello"]
150
+ end
151
+
152
+ should "return a hash with generic exception attributes as well as context data" do
153
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
154
+ expected_data = {
155
+ "error_class" => "StandardError",
156
+ "error_string" => "StandardError: something went wrong",
157
+ "timestamp" => @timestamp,
158
+ "backtrace" => ["<no backtrace>"],
159
+ "error" => "StandardError: something went wrong",
160
+ "session" => { "user_id" => 23, "user_name" => "John" },
161
+ "environment" => { "SERVER_NAME" => "exceptional.com" },
162
+ "location" => { "file" => "<no backtrace>", "line" => nil }
163
+ }
164
+
165
+ assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
166
+ end
167
+
168
+ should "generate exception data appropriately if exception message is nil" do
169
+ exception_with_nil_message = RuntimeError.new(nil)
170
+ stub(exception_with_nil_message).message { nil }
171
+ exception_info = ExceptionInfo.new(exception_with_nil_message, @exception_context, @timestamp)
172
+ exception_data = exception_info.enhanced_data
173
+ assert_equal "RuntimeError: ", exception_data["error_string"]
174
+ assert_equal "RuntimeError: ", exception_data["error"]
175
+ end
176
+
177
+ should "include controller data when available" do
178
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
179
+ expected_data = {
180
+ "error_class" => "StandardError",
181
+ "error_string" => "StandardError: something went wrong",
182
+ "timestamp" => @timestamp,
183
+ "backtrace" => ["<no backtrace>"],
184
+ "error" => "StandardError: something went wrong",
185
+ "session" => { "key" => "session_key", "data" => { "username" => "jsmith", "id" => "session_key" } },
186
+ "environment" => { "SERVER_NAME" => "exceptional.com" },
187
+ "request" => {
188
+ "params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
189
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
190
+ "url" => "host/path"
191
+ },
192
+ "location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
193
+ }
194
+
195
+ assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
196
+ end
197
+
198
+ should "extract controller from rack specific exception context when not provided explicitly" do
199
+ @exception_context["action_controller.instance"] = @controller
200
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
201
+ expected_data = {
202
+ "error_class" => "StandardError",
203
+ "error_string" => "StandardError: something went wrong",
204
+ "timestamp" => @timestamp,
205
+ "backtrace" => ["<no backtrace>"],
206
+ "error" => "StandardError: something went wrong",
207
+ "session" => { "key" => "session_key", "data" => { "username" => "jsmith", "id" => "session_key" } },
208
+ "environment" => { "SERVER_NAME" => "exceptional.com" },
209
+ "request" => {
210
+ "params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
211
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
212
+ "url" => "host/path"
213
+ },
214
+ "location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
215
+ }
216
+
217
+ assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
218
+ end
219
+
220
+ should "add to_s attribute to specific sections that have their content in hash format" do
221
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
222
+ expected_data = {
223
+ "error_class" => "StandardError",
224
+ "error_string" => "StandardError: something went wrong",
225
+ "timestamp" => @timestamp,
226
+ "backtrace" => ["<no backtrace>"],
227
+ "error" => "StandardError: something went wrong",
228
+ "session" => {
229
+ "key" => "session_key",
230
+ "data" => { "username" => "jsmith", "id" => "session_key" },
231
+ "to_s" => "data:\n id: session_key\n username: jsmith\nkey: session_key\n" },
232
+ "environment" => {
233
+ "SERVER_NAME" => "exceptional.com",
234
+ "to_s" => "SERVER_NAME: exceptional.com\n" },
235
+ "request" => {
236
+ "params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
237
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
238
+ "url" => "host/path",
239
+ "to_s" => "params:\n action: fail\n advertiser_id: 435\n controller: dummy\nrails_root: Rails.root not defined. Is this a test environment?\nurl: host/path\n"
240
+ },
241
+ "location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
242
+ }
243
+
244
+ assert_equal_with_diff expected_data, exception_info.enhanced_data
245
+ end
246
+
247
+ should "filter out sensitive parameters like passwords" do
248
+ @controller.request.parameters[:password] = "super_secret"
249
+ @controller.request.parameters[:user] = { "password" => "also super secret", "password_confirmation" => "also super secret" }
250
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
251
+ expected_params = {
252
+ "password" => "[FILTERED]",
253
+ "advertiser_id" => 435, "controller" => "dummy",
254
+ "action" => "fail",
255
+ "user" => {
256
+ "password" => "[FILTERED]",
257
+ "password_confirmation" => "[FILTERED]"
258
+ }
259
+ }
260
+ assert_equal_with_diff expected_params, exception_info.enhanced_data["request"]["params"]
261
+ end
262
+
263
+ should "include the changes from the custom data callback" do
264
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, nil, @data_callback)
265
+ expected_data = {
266
+ "error_class" => "StandardError",
267
+ "error_string" => "StandardError: something went wrong",
268
+ "timestamp" => @timestamp,
269
+ "backtrace" => ["<no backtrace>"],
270
+ "error" => "StandardError: something went wrong",
271
+ "session" => { "user_id" => 23, "user_name" => "John" },
272
+ "environment" => { "SERVER_NAME" => "exceptional.com" },
273
+ "custom_section" => "check this out",
274
+ "location" => { "file" => "<no backtrace>", "line" => nil }
275
+ }
276
+
277
+ assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
278
+ end
279
+
280
+ should "apply the custom_data_hook results" do
281
+ stub(ExceptionHandling).custom_data_hook { ->(data) { data[:custom_hook] = "changes from custom hook" } }
282
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
283
+ expected_data = {
284
+ "error_class" => "StandardError",
285
+ "error_string" => "StandardError: something went wrong",
286
+ "timestamp" => @timestamp,
287
+ "backtrace" => ["<no backtrace>"],
288
+ "error" => "StandardError: something went wrong",
289
+ "session" => { "user_id" => 23, "user_name" => "John" },
290
+ "environment" => { "SERVER_NAME" => "exceptional.com" },
291
+ "custom_hook" => "changes from custom hook",
292
+ "location" => { "file" => "<no backtrace>", "line" => nil }
293
+ }
294
+
295
+ assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
296
+ end
297
+
298
+ should "log info if the custom data hook results in a nil message exception" do
299
+ ExceptionHandling.custom_data_hook = lambda do |data|
300
+ raise_exception_with_nil_message
301
+ end
302
+ log_info_messages = []
303
+ stub(ExceptionHandling.logger).info.with_any_args do |message, _|
304
+ log_info_messages << message
305
+ end
306
+
307
+ exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
308
+ exception_info.enhanced_data
309
+ assert log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }
310
+ ExceptionHandling.custom_data_hook = nil
311
+ end
312
+ end
313
+
314
+ context "exception_description" do
315
+ should "return the exception description from the global exception filter list" do
316
+ exception = StandardError.new("No route matches")
317
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
318
+ description = exception_info.exception_description
319
+ assert_not_nil description
320
+ assert_equal :NoRoute, description.filter_name
321
+ end
322
+
323
+ should "find the description when filter criteria includes section in hash format" do
324
+ env = { server: "fe98" }
325
+ parameters = { advertiser_id: 435, controller: "sessions", action: "fail" }
326
+ session = { username: "jsmith", id: "session_key" }
327
+ request_uri = "host/path"
328
+ controller = create_dummy_controller(env, parameters, session, request_uri)
329
+ exception = StandardError.new("Request to click domain rejected")
330
+ exception_info = ExceptionInfo.new(exception, {}, Time.now, controller)
331
+ assert_equal true, exception_info.enhanced_data[:request].is_a?(Hash)
332
+ description = exception_info.exception_description
333
+ assert_not_nil description
334
+ assert_equal :"Click Request Rejected", description.filter_name
335
+ end
336
+
337
+ should "return same description object for related errors (avoid reloading exception catalog from disk)" do
338
+ exception = StandardError.new("No route matches")
339
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
340
+ description = exception_info.exception_description
341
+
342
+ repeat_ex = StandardError.new("No route matches 2")
343
+ repeat_ex_info = ExceptionInfo.new(repeat_ex, {}, Time.now)
344
+ assert_equal description.object_id, repeat_ex_info.exception_description.object_id
345
+ end
346
+ end
347
+
348
+ context "send_to_honeybadger?" do
349
+ should "be enabled when Honeybadger is defined and exception is not in the filter list" do
350
+ stub(ExceptionHandling).honeybadger? { true }
351
+ exception = StandardError.new("something went wrong")
352
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
353
+ assert_nil exception_info.exception_description
354
+ assert_equal true, exception_info.send_to_honeybadger?
355
+ end
356
+
357
+ should "be enabled when Honeybadger is defined and exception is on the filter list with the flag turned on" do
358
+ stub(ExceptionHandling).honeybadger? { true }
359
+ exception = StandardError.new("No route matches")
360
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
361
+ assert_not_nil exception_info.exception_description
362
+ assert_equal true, exception_info.exception_description.send_to_honeybadger
363
+ assert_equal true, exception_info.send_to_honeybadger?
364
+ end
365
+
366
+ should "be disabled when Honeybadger is defined and exception is on the filter list with the flag turned off" do
367
+ stub(ExceptionHandling).honeybadger? { true }
368
+ exception = StandardError.new("No route matches")
369
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
370
+ assert_not_nil exception_info.exception_description
371
+ stub(exception_info.exception_description).send_to_honeybadger { false }
372
+ assert_equal false, exception_info.send_to_honeybadger?
373
+ end
374
+
375
+ should "be disabled when Honeybadger is not defined" do
376
+ stub(ExceptionHandling).honeybadger? { false }
377
+ exception = StandardError.new("something went wrong")
378
+ exception_info = ExceptionInfo.new(exception, {}, Time.now)
379
+ assert_nil exception_info.exception_description
380
+ assert_equal false, exception_info.send_to_honeybadger?
381
+ end
382
+ end
383
+
384
+ context "honeybadger_context_data" do
385
+ should "return the error details and relevant context data to be used as honeybadger notification context while filtering sensitive data" do
386
+ env = { server: "fe98" }
387
+ parameters = { advertiser_id: 435 }
388
+ session = { username: "jsmith" }
389
+ request_uri = "host/path"
390
+ controller = create_dummy_controller(env, parameters, session, request_uri)
391
+ stub(ExceptionHandling).server_name { "invoca_fe98" }
392
+
393
+ exception = StandardError.new("Some BS")
394
+ exception.set_backtrace([
395
+ "test/unit/exception_handling_test.rb:847:in `exception_1'",
396
+ "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"])
397
+ exception_context = { "SERVER_NAME" => "exceptional.com" }
398
+ data_callback = ->(data) do
399
+ data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
400
+ data[:user_details] = { username: "jsmith" }
401
+ data[:event_response] = "Event successfully received"
402
+ data[:other_section] = "This should not be included in the response"
403
+ end
404
+ timestamp = Time.now
405
+ exception_info = ExceptionInfo.new(exception, exception_context, timestamp, controller, data_callback)
406
+
407
+ expected_data = {
408
+ timestamp: timestamp,
409
+ error_class: "StandardError",
410
+ exception_context: { "SERVER_NAME" => "exceptional.com" },
411
+ server: "invoca_fe98",
412
+ scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
413
+ notes: "this is used by a test",
414
+ user_details: { "username" => "jsmith" },
415
+ request: {
416
+ "params" => { "advertiser_id" => 435 },
417
+ "rails_root" => "Rails.root not defined. Is this a test environment?",
418
+ "url" => "host/path" },
419
+ session: {
420
+ "key" => nil,
421
+ "data" => { "username" => "jsmith" } },
422
+ environment: {
423
+ "SERVER_NAME" => "exceptional.com" },
424
+ backtrace: [
425
+ "test/unit/exception_handling_test.rb:847:in `exception_1'",
426
+ "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"],
427
+ event_response: "Event successfully received"
428
+ }
429
+ assert_equal_with_diff expected_data, exception_info.honeybadger_context_data
430
+ end
431
+
432
+ [ ['Hash', { 'cookie' => 'cookie_context' } ],
433
+ ['String', 'Entering Error State' ],
434
+ ['Array', ['Error1', 'Error2'] ]].each do |klass, value|
435
+
436
+ should "extract context from exception_context when it is a #{klass}" do
437
+ exception = StandardError.new("Exception")
438
+ exception_context = value
439
+ exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
440
+
441
+ assert_equal klass, value.class.name
442
+ assert_equal value, exception_info.honeybadger_context_data[:exception_context]
443
+ end
444
+ end
445
+
446
+ should "filter out sensitive data from exception context such as [password, password_confirmation, oauth_token]" do
447
+ sensitive_data = {
448
+ "password" => "super_secret",
449
+ "password_confirmation" => "super_secret_confirmation",
450
+ "oauth_token" => "super_secret_oauth_token"
451
+ }
452
+
453
+ exception = StandardError.new("boom")
454
+ exception_context = {
455
+ "SERVER_NAME" => "exceptional.com",
456
+ "one_layer" => sensitive_data,
457
+ "two_layers" => {
458
+ "sensitive_data" => sensitive_data
459
+ },
460
+ "rack.request.form_vars" => "username=investor%40invoca.com&password=my_special_password&commit=Log+In",
461
+ "example_without_password" => {
462
+ "rack.request.form_vars" => "username=investor%40invoca.com"
463
+ }
464
+ }.merge(sensitive_data)
465
+
466
+ exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
467
+
468
+ expected_sensitive_data = ["password", "password_confirmation", "oauth_token"].build_hash { |key| [key, "[FILTERED]"] }
469
+ expected_exception_context = {
470
+ "SERVER_NAME" => "exceptional.com",
471
+ "one_layer" => expected_sensitive_data,
472
+ "two_layers" => {
473
+ "sensitive_data" => expected_sensitive_data
474
+ },
475
+ "rack.request.form_vars" => "username=investor%40invoca.com&password=[FILTERED]&commit=Log+In",
476
+ "example_without_password" => {
477
+ "rack.request.form_vars" => "username=investor%40invoca.com"
478
+ }
479
+ }.merge(expected_sensitive_data)
480
+
481
+ assert_equal_with_diff expected_exception_context, exception_info.honeybadger_context_data[:exception_context]
482
+ end
483
+
484
+ should "omit context if exception_context is empty" do
485
+ exception = StandardError.new("Exception")
486
+ exception_context = ""
487
+ exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
488
+
489
+ assert_nil exception_info.honeybadger_context_data[:exception_context]
490
+ end
491
+ end
492
+
493
+ def prepare_data(data)
494
+ data.each do |key, section|
495
+ if section.is_a?(Hash)
496
+ section.delete(:to_s)
497
+ end
498
+ end
499
+ end
500
+ end
501
+ end