exception_handling 1.2.1 → 2.2.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 (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