exception_handling 3.0.pre.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/pipeline.yml +36 -0
  4. data/.gitignore +3 -0
  5. data/.rspec +3 -0
  6. data/.ruby-version +1 -1
  7. data/.tool-versions +1 -0
  8. data/Appraisals +13 -0
  9. data/CHANGELOG.md +150 -0
  10. data/Gemfile +10 -16
  11. data/Gemfile.lock +65 -128
  12. data/README.md +51 -19
  13. data/Rakefile +8 -11
  14. data/exception_handling.gemspec +11 -13
  15. data/gemfiles/rails_5.gemfile +16 -0
  16. data/gemfiles/rails_6.gemfile +16 -0
  17. data/gemfiles/rails_7.gemfile +16 -0
  18. data/lib/exception_handling/escalate_callback.rb +19 -0
  19. data/lib/exception_handling/exception_info.rb +15 -11
  20. data/lib/exception_handling/log_stub_error.rb +2 -1
  21. data/lib/exception_handling/logging_methods.rb +21 -0
  22. data/lib/exception_handling/testing.rb +9 -12
  23. data/lib/exception_handling/version.rb +1 -1
  24. data/lib/exception_handling.rb +83 -173
  25. data/{test → spec}/helpers/exception_helpers.rb +2 -2
  26. data/spec/rake_test_warning_false.rb +20 -0
  27. data/{test/test_helper.rb → spec/spec_helper.rb} +63 -66
  28. data/spec/unit/exception_handling/escalate_callback_spec.rb +81 -0
  29. data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
  30. data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
  31. data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +170 -114
  32. data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
  33. data/spec/unit/exception_handling/logging_methods_spec.rb +38 -0
  34. data/spec/unit/exception_handling_spec.rb +1063 -0
  35. metadata +62 -91
  36. data/lib/exception_handling/honeybadger_callbacks.rb +0 -59
  37. data/lib/exception_handling/mailer.rb +0 -70
  38. data/lib/exception_handling/methods.rb +0 -101
  39. data/lib/exception_handling/sensu.rb +0 -28
  40. data/semaphore_ci/setup.sh +0 -3
  41. data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
  42. data/test/unit/exception_handling/exception_description_test.rb +0 -82
  43. data/test/unit/exception_handling/honeybadger_callbacks_test.rb +0 -122
  44. data/test/unit/exception_handling/mailer_test.rb +0 -98
  45. data/test/unit/exception_handling/methods_test.rb +0 -84
  46. data/test/unit/exception_handling/sensu_test.rb +0 -52
  47. data/test/unit/exception_handling_test.rb +0 -1109
  48. data/views/exception_handling/mailer/escalate_custom.html.erb +0 -17
  49. data/views/exception_handling/mailer/escalation_notification.html.erb +0 -17
  50. data/views/exception_handling/mailer/log_parser_exception_notification.html.erb +0 -82
  51. /data/{test → spec}/helpers/controller_helpers.rb +0 -0
@@ -1,15 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support'
4
- require 'active_support/time'
5
- require 'active_support/test_case'
6
- require 'active_model'
7
- require 'action_mailer'
8
- require 'action_dispatch'
9
- require 'hobo_support'
10
- require 'shoulda'
11
- require 'rr'
12
- require 'minitest/autorun'
3
+ require 'rspec'
4
+ require 'rspec/mocks'
5
+ require 'rspec_junit_formatter'
6
+
13
7
  require 'pry'
14
8
  require 'honeybadger'
15
9
  require 'contextual_logger'
@@ -17,26 +11,31 @@ require 'contextual_logger'
17
11
  require 'exception_handling'
18
12
  require 'exception_handling/testing'
19
13
 
20
- ActiveSupport::TestCase.test_order = :sorted
21
-
22
14
  class LoggerStub
23
- include ContextualLogger
24
- attr_accessor :logged
15
+ include ContextualLogger::LoggerMixin
16
+ attr_accessor :logged, :level
25
17
 
26
18
  def initialize
19
+ @level = Logger::Severity::DEBUG
20
+ @progname = nil
21
+ @logdev = nil
27
22
  clear
28
23
  end
29
24
 
25
+ def debug(message, log_context = {})
26
+ logged << { message: message, context: log_context, severity: 'DEBUG' }
27
+ end
28
+
30
29
  def info(message, log_context = {})
31
- logged << { message: message, context: log_context }
30
+ logged << { message: message, context: log_context, severity: 'INFO' }
32
31
  end
33
32
 
34
33
  def warn(message, log_context = {})
35
- logged << { message: message, context: log_context }
34
+ logged << { message: message, context: log_context, severity: 'WARN' }
36
35
  end
37
36
 
38
37
  def fatal(message, log_context = {})
39
- logged << { message: message, context: log_context }
38
+ logged << { message: message, context: log_context, severity: 'FATAL' }
40
39
  end
41
40
 
42
41
  def clear
@@ -75,50 +74,26 @@ def dont_stub_log_error
75
74
  true
76
75
  end
77
76
 
78
- ActionMailer::Base.delivery_method = :test
79
-
80
- _ = ActiveSupport
81
- _ = ActiveSupport::TestCase
82
-
83
- class ActiveSupport::TestCase
84
- @@constant_overrides = []
85
-
86
- setup do
87
- unless @@constant_overrides.nil? || @@constant_overrides.empty?
88
- raise "Uh-oh! constant_overrides left over: #{@@constant_overrides.inspect}"
89
- end
90
-
91
- unless defined?(Rails) && defined?(Rails.env)
92
- module ::Rails
93
- class << self
94
- attr_writer :env
77
+ module TestHelper
78
+ @constant_overrides = []
79
+ class << self
80
+ attr_accessor :constant_overrides
81
+ end
95
82
 
96
- def env
97
- @env ||= 'test'
98
- end
99
- end
100
- end
83
+ def setup_constant_overrides
84
+ unless TestHelper.constant_overrides.nil? || TestHelper.constant_overrides.empty?
85
+ raise "Uh-oh! constant_overrides left over: #{TestHelper.constant_overrides.inspect}"
101
86
  end
102
87
 
103
88
  Time.now_override = nil
104
89
 
105
- ActionMailer::Base.deliveries.clear
106
-
107
- ExceptionHandling.email_environment = 'Test'
108
- ExceptionHandling.sender_address = 'server@example.com'
109
- ExceptionHandling.exception_recipients = 'exceptions@example.com'
110
- ExceptionHandling.escalation_recipients = 'escalation@example.com'
90
+ ExceptionHandling.environment = 'not_test'
111
91
  ExceptionHandling.server_name = 'server'
112
92
  ExceptionHandling.filter_list_filename = "./config/exception_filters.yml"
113
- ExceptionHandling.eventmachine_safe = false
114
- ExceptionHandling.eventmachine_synchrony = false
115
- ExceptionHandling.sensu_host = "127.0.0.1"
116
- ExceptionHandling.sensu_port = 3030
117
- ExceptionHandling.sensu_prefix = ""
118
93
  end
119
94
 
120
- teardown do
121
- @@constant_overrides&.reverse&.each do |parent_module, k, v|
95
+ def teardown_constant_overrides
96
+ TestHelper.constant_overrides&.reverse&.each do |parent_module, k, v|
122
97
  ExceptionHandling.ensure_safe "constant cleanup #{k.inspect}, #{parent_module}(#{parent_module.class})::#{v.inspect}(#{v.class})" do
123
98
  silence_warnings do
124
99
  if v == :never_defined
@@ -129,7 +104,7 @@ class ActiveSupport::TestCase
129
104
  end
130
105
  end
131
106
  end
132
- @@constant_overrides = []
107
+ TestHelper.constant_overrides = []
133
108
  end
134
109
 
135
110
  def set_test_const(const_name, value)
@@ -149,27 +124,17 @@ class ActiveSupport::TestCase
149
124
  end
150
125
  end
151
126
 
152
- @@constant_overrides << [final_parent_module, final_const_name, original_value]
127
+ TestHelper.constant_overrides << [final_parent_module, final_const_name, original_value]
153
128
 
154
129
  silence_warnings { final_parent_module.const_set(final_const_name, value) }
155
130
  end
156
-
157
- def assert_emails(expected, message = nil)
158
- if block_given?
159
- original_count = ActionMailer::Base.deliveries.size
160
- yield
161
- else
162
- original_count = 0
163
- end
164
- assert_equal expected, ActionMailer::Base.deliveries.size - original_count, "wrong number of emails#{': ' + message.to_s if message}"
165
- end
166
131
  end
167
132
 
168
133
  def assert_equal_with_diff(arg1, arg2, msg = '')
169
134
  if arg1 == arg2
170
- assert true # To keep the assertion count accurate
135
+ expect(true).to be_truthy # To keep the assertion count accurate
171
136
  else
172
- assert_equal arg1, arg2, "#{msg}\n#{Diff.compare(arg1, arg2)}"
137
+ expect(arg1).to eq(arg2), "#{msg}\n#{Diff.compare(arg1, arg2)}"
173
138
  end
174
139
  end
175
140
 
@@ -200,3 +165,35 @@ class Time
200
165
  end
201
166
  end
202
167
  end
168
+
169
+ RSpec.configure do |config|
170
+ config.add_formatter(RspecJunitFormatter, 'spec/reports/rspec.xml')
171
+ config.include TestHelper
172
+
173
+ config.before(:each) do
174
+ setup_constant_overrides
175
+ unless defined?(Rails) && defined?(Rails.env)
176
+ module Rails
177
+ class << self
178
+ attr_writer :env
179
+
180
+ def env
181
+ @env ||= 'test'
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ config.after(:each) do
189
+ teardown_constant_overrides
190
+ end
191
+
192
+ config.mock_with :rspec do |mocks|
193
+ mocks.verify_partial_doubles = true
194
+ end
195
+
196
+ config.expect_with(:rspec, :test_unit)
197
+
198
+ RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 2_000
199
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'exception_handling/escalate_callback'
4
+ require File.expand_path('../../spec_helper', __dir__)
5
+
6
+ module ExceptionHandling
7
+ describe EscalateCallback do
8
+ before do
9
+ class TestGem
10
+ class << self
11
+ attr_accessor :logger
12
+ end
13
+ include Escalate.mixin
14
+ end
15
+ TestGem.logger = logger
16
+ Escalate.clear_on_escalate_callbacks
17
+ end
18
+
19
+ after do
20
+ Escalate.clear_on_escalate_callbacks
21
+ end
22
+
23
+ let(:exception) do
24
+ raise "boom!"
25
+ rescue => ex
26
+ ex
27
+ end
28
+ let(:location_message) { "happened in TestGem" }
29
+ let(:context_hash) { { cuid: 'AABBCD' } }
30
+ let(:logger) { double("logger") }
31
+
32
+ describe '.register_if_configured!' do
33
+ context 'when already configured' do
34
+ before do
35
+ @original_logger = ExceptionHandling.logger
36
+ ExceptionHandling.logger = ::Logger.new('/dev/null').extend(ContextualLogger::LoggerMixin)
37
+ end
38
+
39
+ after do
40
+ ExceptionHandling.logger = @original_logger
41
+ end
42
+
43
+ it 'registers a callback' do
44
+ EscalateCallback.register_if_configured!
45
+
46
+ expect(logger).to_not receive(:error)
47
+ expect(logger).to_not receive(:fatal)
48
+ expect(ExceptionHandling).to receive(:log_error).with(exception, location_message, context_hash)
49
+
50
+ TestGem.escalate(exception, location_message, context: context_hash)
51
+ end
52
+ end
53
+
54
+ context 'when not yet configured' do
55
+ before do
56
+ @original_logger = ExceptionHandling.logger
57
+ ExceptionHandling.logger = nil
58
+ end
59
+
60
+ after do
61
+ ExceptionHandling.logger = @original_logger
62
+ end
63
+
64
+ it 'registers a callback once the logger is set' do
65
+ EscalateCallback.register_if_configured!
66
+
67
+ expect(Escalate.on_escalate_callbacks).to be_empty
68
+
69
+ ExceptionHandling.logger = ::Logger.new('/dev/null').extend(ContextualLogger::LoggerMixin)
70
+ expect(Escalate.on_escalate_callbacks).to_not be_empty
71
+
72
+ expect(logger).to_not receive(:error)
73
+ expect(logger).to_not receive(:fatal)
74
+ expect(ExceptionHandling).to receive(:log_error).with(exception, location_message, context_hash)
75
+
76
+ TestGem.escalate(exception, location_message, context: context_hash)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../../spec_helper', __dir__)
4
+
5
+ module ExceptionHandling
6
+ describe ExceptionCatalog do
7
+
8
+ context "With stubbed yaml content" do
9
+ before do
10
+ filter_list = { exception1: { error: "my error message" },
11
+ exception2: { error: "some other message", session: "misc data" } }
12
+ allow(YAML).to receive(:load_file) { filter_list }
13
+
14
+ # bump modified time up to get the above filter loaded
15
+ allow(File).to receive(:mtime) { incrementing_mtime }
16
+ end
17
+
18
+ context "with loaded data" do
19
+ before do
20
+ allow(File).to receive(:mtime) { incrementing_mtime }
21
+ @exception_catalog = ExceptionCatalog.new(ExceptionHandling.filter_list_filename)
22
+ @exception_catalog.send :load_file
23
+ end
24
+
25
+ it "have loaded filters" do
26
+ expect(@exception_catalog.instance_eval("@filters").size).to eq(2)
27
+ end
28
+
29
+ it "find messages in the catalog" do
30
+ expect(!@exception_catalog.find(error: "Scott says unlikely to ever match")).to be_truthy
31
+ end
32
+
33
+ it "find matching data" do
34
+ exception_description = @exception_catalog.find(error: "this is my error message, which should match something")
35
+ expect(exception_description).to be_truthy
36
+ expect(exception_description.filter_name).to eq(:exception1)
37
+ end
38
+ end
39
+
40
+ it "write errors loading the yaml file directly to the log file" do
41
+ @exception_catalog = ExceptionCatalog.new(ExceptionHandling.filter_list_filename)
42
+
43
+ expect(ExceptionHandling).to receive(:log_error).never
44
+ expect(ExceptionHandling).to receive(:write_exception_to_log).with(anything, "ExceptionCatalog#refresh_filters: ./config/exception_filters.yml", any_args)
45
+ expect(@exception_catalog).to receive(:load_file) { raise "noooooo" }
46
+
47
+ @exception_catalog.find({})
48
+ end
49
+ end
50
+
51
+ context "with live yaml content" do
52
+ before do
53
+ @filename = File.expand_path('../../../config/exception_filters.yml', __dir__)
54
+ @exception_catalog = ExceptionCatalog.new(@filename)
55
+ expect do
56
+ @exception_catalog.send :load_file
57
+ end.not_to raise_error
58
+ end
59
+
60
+ it "load the filter data" do
61
+ expect(!@exception_catalog.find(error: "Scott says unlikely to ever match")).to be_truthy
62
+ expect(!@exception_catalog.find(error: "Scott says unlikely to ever match")).to be_truthy
63
+ end
64
+ end
65
+
66
+ context "with no yaml content" do
67
+ before do
68
+ @exception_catalog = ExceptionCatalog.new(nil)
69
+ end
70
+
71
+ it "not load filter data" do
72
+ expect(ExceptionHandling).to receive(:write_exception_to_log).with(any_args).never
73
+ @exception_catalog.find(error: "Scott says unlikely to ever match")
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def incrementing_mtime
80
+ @mtime ||= Time.now
81
+ @mtime += 1.day
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../../spec_helper', __dir__)
4
+
5
+ module ExceptionHandling
6
+ describe ExceptionDescription do
7
+
8
+ context "Filter" do
9
+ it "allow direct matching of strings" do
10
+ @f = ExceptionDescription.new(:filter1, error: "my error message")
11
+ expect(@f.match?('error' => "my error message")).to be_truthy
12
+ end
13
+
14
+ it "allow direct matching of strings on with symbol keys" do
15
+ @f = ExceptionDescription.new(:filter1, error: "my error message")
16
+ expect(@f.match?(error: "my error message")).to be_truthy
17
+ end
18
+
19
+ it "allow wildcards to cross line boundries" do
20
+ @f = ExceptionDescription.new(:filter1, error: "my error message.*with multiple lines")
21
+ expect(@f.match?(error: "my error message\nwith more than one, with multiple lines")).to be_truthy
22
+ end
23
+
24
+ it "complain when no regexps have a value" do
25
+ expect { ExceptionDescription.new(:filter1, error: nil) }.to raise_exception(ArgumentError, /has all blank regexes/)
26
+ end
27
+
28
+ it "report when an invalid key is passed" do
29
+ expect { ExceptionDescription.new(:filter1, error: "my error message", not_a_parameter: false) }.to raise_exception(ArgumentError, "Unknown section: not_a_parameter")
30
+ end
31
+
32
+ it "allow send_to_honeybadger to be specified and have it disabled by default" do
33
+ expect(!ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: false).send_to_honeybadger).to be_truthy
34
+ expect(ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: true).send_to_honeybadger).to be_truthy
35
+ expect(!ExceptionDescription.new(:filter1, error: "my error message").send_to_honeybadger).to be_truthy
36
+ end
37
+
38
+ it "allow send_metric to be configured" do
39
+ expect(!ExceptionDescription.new(:filter1, error: "my error message", send_metric: false).send_metric).to be_truthy
40
+ expect(ExceptionDescription.new(:filter1, error: "my error message").send_metric).to be_truthy
41
+ end
42
+
43
+ it "provide metric name" do
44
+ expect(ExceptionDescription.new(:filter1, error: "my error message").metric_name).to eq("filter1")
45
+ expect(ExceptionDescription.new(:filter1, error: "my error message", metric_name: :some_other_metric_name).metric_name).to eq("some_other_metric_name")
46
+ end
47
+
48
+ it "replace spaces in metric name" do
49
+ @f = ExceptionDescription.new(:"filter has spaces", error: "my error message")
50
+ expect(@f.metric_name).to eq( "filter_has_spaces")
51
+ end
52
+
53
+ it "allow notes to be recorded" do
54
+ expect(ExceptionDescription.new(:filter1, error: "my error message").notes).to be_nil
55
+ expect(ExceptionDescription.new(:filter1, error: "my error message", notes: "a long string").notes).to eq("a long string")
56
+ end
57
+
58
+ it "not consider config options in the filter set" do
59
+ expect(ExceptionDescription.new(:filter1, error: "my error message", send_metric: false).match?(error: "my error message")).to be_truthy
60
+ expect(ExceptionDescription.new(:filter1, error: "my error message", metric_name: "false").match?(error: "my error message")).to be_truthy
61
+ expect(ExceptionDescription.new(:filter1, error: "my error message", notes: "hey").match?(error: "my error message")).to be_truthy
62
+ end
63
+
64
+ it "provide exception details" do
65
+ exception_description = ExceptionDescription.new(:filter1, error: "my error message", notes: "hey")
66
+
67
+ expected = { "send_metric" => true, "metric_name" => "filter1", "notes" => "hey" }
68
+
69
+ expect(exception_description.exception_data).to eq( expected)
70
+ end
71
+
72
+ it "match multiple email addresses" do
73
+ mobi = "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for 'mcc@mobistreak.com'"
74
+ credit = "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for 'damon@thecreditpros.com'"
75
+
76
+ exception_description = ExceptionDescription.new(:filter1, error: "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for '(mcc\@mobistreak|damon\@thecreditpros).com'")
77
+ expect(exception_description.match?(error: mobi)).to be_truthy
78
+ expect(exception_description.match?(error: credit)).to be_truthy
79
+ end
80
+ end
81
+ end
82
+ end