exception_handling 3.0.pre.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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