party_fouls 1.5.6
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.
- checksums.yaml +7 -0
- data/README.md +268 -0
- data/Rakefile +19 -0
- data/lib/generators/party_foul/install_generator.rb +38 -0
- data/lib/generators/party_foul/templates/party_foul.rb +39 -0
- data/lib/party_foul/exception_handler.rb +106 -0
- data/lib/party_foul/issue_renderers/base.rb +187 -0
- data/lib/party_foul/issue_renderers/rack.rb +54 -0
- data/lib/party_foul/issue_renderers/rackless.rb +25 -0
- data/lib/party_foul/issue_renderers/rails.rb +35 -0
- data/lib/party_foul/issue_renderers.rb +5 -0
- data/lib/party_foul/middleware.rb +32 -0
- data/lib/party_foul/processors/base.rb +11 -0
- data/lib/party_foul/processors/delayed_job.rb +16 -0
- data/lib/party_foul/processors/resque.rb +16 -0
- data/lib/party_foul/processors/sidekiq.rb +17 -0
- data/lib/party_foul/processors/sync.rb +11 -0
- data/lib/party_foul/processors.rb +2 -0
- data/lib/party_foul/rackless_exception_handler.rb +17 -0
- data/lib/party_foul/version.rb +3 -0
- data/lib/party_foul.rb +92 -0
- data/test/generator_test.rb +26 -0
- data/test/party_foul/configure_test.rb +37 -0
- data/test/party_foul/exception_handler_test.rb +205 -0
- data/test/party_foul/issue_renderers/base_test.rb +210 -0
- data/test/party_foul/issue_renderers/rack_test.rb +80 -0
- data/test/party_foul/issue_renderers/rackless_test.rb +29 -0
- data/test/party_foul/issue_renderers/rails_test.rb +83 -0
- data/test/party_foul/middleware_test.rb +48 -0
- data/test/party_foul/rackless_exception_handler_test.rb +33 -0
- data/test/test_helper.rb +42 -0
- data/test/tmp/config/initializers/party_foul.rb +39 -0
- metadata +214 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
class PartyFoul::IssueRenderers::Rack < PartyFoul::IssueRenderers::Base
|
2
|
+
|
3
|
+
def request
|
4
|
+
@request ||= ::Rack::Request.new(env)
|
5
|
+
end
|
6
|
+
|
7
|
+
def comment_options
|
8
|
+
super.merge(URL: url, Params: params, Session: session, 'IP Address' => ip_address_locator, 'HTTP Headers' => http_headers)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Rack params
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
def params
|
15
|
+
request.params
|
16
|
+
end
|
17
|
+
|
18
|
+
# Link to IP address geolocator of the client who triggered the exception
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
def ip_address_locator
|
22
|
+
"<a href='http://ipinfo.io/#{request.ip}'>#{request.ip}</a>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def url
|
26
|
+
"[#{request.request_method}] #{env['REQUEST_URI']}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# The session hash for the client at the time of the exception
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
def session
|
33
|
+
request.session
|
34
|
+
end
|
35
|
+
|
36
|
+
# HTTP Headers hash from the request. Headers can be filtered out by
|
37
|
+
# adding matching key names to {PartyFoul.blacklisted_headers}
|
38
|
+
#
|
39
|
+
# @return [Hash]
|
40
|
+
def http_headers
|
41
|
+
{
|
42
|
+
Version: env['HTTP_VERSION'],
|
43
|
+
'User Agent' => request.user_agent,
|
44
|
+
'Accept Encoding' => env['HTTP_ACCEPT_ENCODING'],
|
45
|
+
Accept: env['HTTP_ACCEPT'],
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def raw_title
|
52
|
+
%{(#{exception.class}) "#{exception.message}"}
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'party_foul/issue_renderers/base'
|
2
|
+
|
3
|
+
class PartyFoul::IssueRenderers::Rackless < PartyFoul::IssueRenderers::Base
|
4
|
+
# env in a rackless environment is expected to contain three keys:
|
5
|
+
# class: name of the class that raised the exception
|
6
|
+
# method: name of the method that raised the exception
|
7
|
+
# params: parameters passed to the method that raised the exception
|
8
|
+
|
9
|
+
# Rails params hash. Filtered parms are respected.
|
10
|
+
#
|
11
|
+
# @return [Hash]
|
12
|
+
def params
|
13
|
+
env[:params]
|
14
|
+
end
|
15
|
+
|
16
|
+
def comment_options
|
17
|
+
super.merge(Params: params)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def raw_title
|
23
|
+
%{#{env[:class]}##{env[:method]} (#{exception.class}) "#{exception.message}"}
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class PartyFoul::IssueRenderers::Rails < PartyFoul::IssueRenderers::Rack
|
2
|
+
# Rails params hash. Filtered parms are respected.
|
3
|
+
#
|
4
|
+
# @return [Hash]
|
5
|
+
def params
|
6
|
+
parameter_filter = ActionDispatch::Http::ParameterFilter.new(env["action_dispatch.parameter_filter"])
|
7
|
+
parameter_filter.filter(env['action_dispatch.request.parameters'] || {})
|
8
|
+
end
|
9
|
+
|
10
|
+
# Rails session hash. Filtered parms are respected.
|
11
|
+
#
|
12
|
+
# @return [Hash]
|
13
|
+
def session
|
14
|
+
parameter_filter = ActionDispatch::Http::ParameterFilter.new(env['action_dispatch.parameter_filter'])
|
15
|
+
parameter_filter.filter(env['rack.session'] || { } )
|
16
|
+
end
|
17
|
+
|
18
|
+
# The timestamp when the exception occurred. Will use Time.current when available to record
|
19
|
+
# the time with the proper timezone
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
def occurred_at
|
23
|
+
@occurred_at ||= Time.current.strftime('%B %d, %Y %H:%M:%S %z')
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def app_root
|
29
|
+
Rails.root.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def raw_title
|
33
|
+
%{#{env['action_controller.instance'].class}##{(env['action_dispatch.request.parameters'] || {})['action']} (#{exception.class}) "#{exception.message}"}
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PartyFoul
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
@app.call(env)
|
9
|
+
rescue Exception => captured_exception
|
10
|
+
if allow_handling?(captured_exception)
|
11
|
+
PartyFoul::ExceptionHandler.handle(captured_exception, env)
|
12
|
+
end
|
13
|
+
raise captured_exception
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def allow_handling?(captured_exception)
|
19
|
+
!PartyFoul.blacklisted_exceptions.find do |blacklisted_exception|
|
20
|
+
names = blacklisted_exception.split('::')
|
21
|
+
names.shift if names.empty? || names.first.empty?
|
22
|
+
|
23
|
+
constant = Object
|
24
|
+
names.each do |name|
|
25
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
constant === captured_exception
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class PartyFoul::Processors::Base
|
2
|
+
# Passes the exception and rack env data to the ExceptionHandler and
|
3
|
+
# runs everything synchronously. This base class method must be
|
4
|
+
# overriden by any inheriting class.
|
5
|
+
#
|
6
|
+
# @param [Exception, Hash]
|
7
|
+
# @return [NotImplementedError]
|
8
|
+
def self.handle(exception, env)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'party_foul/processors/base'
|
2
|
+
|
3
|
+
class PartyFoul::Processors::DelayedJob < PartyFoul::Processors::Base
|
4
|
+
@queue = 'party_foul'
|
5
|
+
|
6
|
+
# Passes the exception and rack env data to DelayedJob to be processed later
|
7
|
+
#
|
8
|
+
# @param [Exception, Hash]
|
9
|
+
def self.handle(exception, env)
|
10
|
+
new.delay(queue: @queue).perform(Marshal.dump(exception), Marshal.dump(env))
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform(exception, env)
|
14
|
+
PartyFoul::ExceptionHandler.new(Marshal.load(exception), Marshal.load(env)).run
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'party_foul/processors/base'
|
2
|
+
|
3
|
+
class PartyFoul::Processors::Resque < PartyFoul::Processors::Base
|
4
|
+
@queue = :party_foul
|
5
|
+
|
6
|
+
# Passes the exception and rack env data to Resque to be processed later
|
7
|
+
#
|
8
|
+
# @param [Exception, Hash]
|
9
|
+
def self.handle(exception, env)
|
10
|
+
Resque.enqueue(PartyFoul::Processors::Resque, Marshal.dump(exception), Marshal.dump(env))
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.perform(exception, env)
|
14
|
+
PartyFoul::ExceptionHandler.new(Marshal.load(exception), Marshal.load(env)).run
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'party_foul/processors/base'
|
2
|
+
|
3
|
+
class PartyFoul::Processors::Sidekiq < PartyFoul::Processors::Base
|
4
|
+
include Sidekiq::Worker
|
5
|
+
sidekiq_options queue: 'party_foul'
|
6
|
+
|
7
|
+
# Passes the exception and rack env data to Sidekiq to be processed later
|
8
|
+
#
|
9
|
+
# @param [Exception, Hash]
|
10
|
+
def self.handle(exception, env)
|
11
|
+
perform_async(Marshal.dump(exception), Marshal.dump(env))
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(exception, env)
|
15
|
+
PartyFoul::ExceptionHandler.new(Marshal.load(exception), Marshal.load(env)).run
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'party_foul/processors/base'
|
2
|
+
|
3
|
+
class PartyFoul::Processors::Sync < PartyFoul::Processors::Base
|
4
|
+
# Passes the exception and rack env data to the ExceptionHandler and
|
5
|
+
# runs everything synchronously.
|
6
|
+
#
|
7
|
+
# @param [Exception, Hash]
|
8
|
+
def self.handle(exception, env)
|
9
|
+
PartyFoul::ExceptionHandler.new(exception, env).run
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class PartyFoul::RacklessExceptionHandler < PartyFoul::ExceptionHandler
|
2
|
+
# This handler will pass the exception and working environment from Rack off to a processor.
|
3
|
+
# The default PartyFoul processor will work synchronously. Processor adapters can be written
|
4
|
+
# to push this logic to a background job if desired.
|
5
|
+
#
|
6
|
+
# @param [Exception, Hash]
|
7
|
+
def self.handle(exception, env)
|
8
|
+
self.new(exception, clean_env(env)).run
|
9
|
+
end
|
10
|
+
|
11
|
+
# Uses the Rackless IssueRenderer for a rackless environment
|
12
|
+
#
|
13
|
+
# @param [Exception, Hash]
|
14
|
+
def initialize(exception, env)
|
15
|
+
self.rendered_issue = PartyFoul::IssueRenderers::Rackless.new(exception, env)
|
16
|
+
end
|
17
|
+
end
|
data/lib/party_foul.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
|
3
|
+
module PartyFoul
|
4
|
+
class << self
|
5
|
+
attr_accessor :github, :oauth_token, :owner, :repo, :additional_labels, :comment_limit, :title_prefix
|
6
|
+
attr_writer :branch, :web_url, :api_endpoint, :processor, :blacklisted_exceptions
|
7
|
+
end
|
8
|
+
|
9
|
+
# The git branch that is used for linking in the stack trace
|
10
|
+
#
|
11
|
+
# @return [String] Defaults to 'master' if not set
|
12
|
+
def self.branch
|
13
|
+
@branch ||= 'master'
|
14
|
+
end
|
15
|
+
|
16
|
+
# The web url for GitHub. This is only interesting for Enterprise
|
17
|
+
# users
|
18
|
+
#
|
19
|
+
# @return [String] Defaults to 'https://github.com' if not set
|
20
|
+
def self.web_url
|
21
|
+
@web_url ||= 'https://github.com'
|
22
|
+
end
|
23
|
+
|
24
|
+
# The api endpoint for GitHub. This is only interesting for Enterprise
|
25
|
+
# users
|
26
|
+
#
|
27
|
+
# @return [String] Defaults to 'https://api.github.com' if not set
|
28
|
+
def self.api_endpoint
|
29
|
+
@api_endpoint ||= 'https://api.github.com'
|
30
|
+
end
|
31
|
+
|
32
|
+
# The processor to be used when handling the exception. Defaults to a
|
33
|
+
# synchrons processor
|
34
|
+
#
|
35
|
+
# @return [Class] Defaults to 'PartyFoul::Processors:Sync
|
36
|
+
def self.processor
|
37
|
+
@processor ||= PartyFoul::Processors::Sync
|
38
|
+
end
|
39
|
+
|
40
|
+
# The collection of exceptions that should not be captured. Members of
|
41
|
+
# the collection must be string representations of the exception. For
|
42
|
+
# example:
|
43
|
+
#
|
44
|
+
# # This is good
|
45
|
+
# ['ActiveRecord::RecordNotFound']
|
46
|
+
#
|
47
|
+
# # This is not
|
48
|
+
# [ActiveRecord::RecordNotFound]
|
49
|
+
#
|
50
|
+
# @return [Array]
|
51
|
+
def self.blacklisted_exceptions
|
52
|
+
@blacklisted_exceptions || []
|
53
|
+
end
|
54
|
+
|
55
|
+
# The GitHub path to the repo
|
56
|
+
# Built using {.owner} and {.repo}
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def self.repo_path
|
60
|
+
"#{owner}/#{repo}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# The url of the repository. Built using the {.web_url} and {.repo_path}
|
64
|
+
# values
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
def self.repo_url
|
68
|
+
"#{web_url}/#{repo_path}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# The configure block for PartyFoul. Use to initialize settings
|
72
|
+
#
|
73
|
+
# PartyFoul.configure do |config|
|
74
|
+
# config.owner 'dockyard'
|
75
|
+
# config.repo 'test_app'
|
76
|
+
# config.oauth_token = ENV['oauth_token']
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# Will also setup for GitHub api connections
|
80
|
+
#
|
81
|
+
# @param [Block]
|
82
|
+
def self.configure
|
83
|
+
yield self
|
84
|
+
self.github = Octokit::Client.new access_token: oauth_token, api_endpoint: api_endpoint
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
require 'party_foul/exception_handler'
|
89
|
+
require 'party_foul/rackless_exception_handler'
|
90
|
+
require 'party_foul/issue_renderers'
|
91
|
+
require 'party_foul/middleware'
|
92
|
+
require 'party_foul/processors'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'rails/generators/test_case'
|
3
|
+
require 'generators/party_foul/install_generator'
|
4
|
+
|
5
|
+
class PartyFoul::GeneratorTest < Rails::Generators::TestCase
|
6
|
+
destination File.expand_path('../tmp', __FILE__)
|
7
|
+
tests PartyFoul::InstallGenerator
|
8
|
+
|
9
|
+
test 'it copies the initializer' do
|
10
|
+
owner = 'test_owner'
|
11
|
+
repo = 'test_repo'
|
12
|
+
octokit = mock('Octokit::Client')
|
13
|
+
octokit.expects(:create_authorization).with(scopes: ['repo'], note: 'PartyFoul test_owner/test_repo', note_url: 'http://example.com/test_owner/test_repo').returns(sawyer_resource({token: 'test_token'}))
|
14
|
+
Octokit::Client.stubs(:new).with(:login => 'test_login', :password => 'test_password', :api_endpoint => 'http://api.example.com').returns(octokit)
|
15
|
+
::Readline.stubs(:readline).returns('test_login').then.returns('test_password').then.returns(owner).then.returns(repo).then.returns('http://api.example.com').then.returns('http://example.com').then.returns('')
|
16
|
+
run_generator
|
17
|
+
|
18
|
+
assert_file 'config/initializers/party_foul.rb' do |initializer|
|
19
|
+
assert_match(/config\.api_endpoint\s+=\s'http:\/\/api\.example\.com'/, initializer)
|
20
|
+
assert_match(/config\.web_url\s+=\s'http:\/\/example\.com'/, initializer)
|
21
|
+
assert_match(/config\.owner\s+=\s'test_owner'/, initializer)
|
22
|
+
assert_match(/config\.repo\s+=\s'test_repo'/, initializer)
|
23
|
+
assert_match(/config\.oauth_token\s+=\s'test_token'/, initializer)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe 'Party Foul Confg' do
|
4
|
+
|
5
|
+
after do
|
6
|
+
clean_up_party
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'sets the proper config variables' do
|
10
|
+
PartyFoul.configure do |config|
|
11
|
+
config.blacklisted_exceptions = ['StandardError']
|
12
|
+
config.oauth_token = 'test_token'
|
13
|
+
config.web_url = 'http://example.com'
|
14
|
+
config.api_endpoint = 'http://api.example.com'
|
15
|
+
config.owner = 'test_owner'
|
16
|
+
config.repo = 'test_repo'
|
17
|
+
config.branch = 'master'
|
18
|
+
config.comment_limit = 10
|
19
|
+
end
|
20
|
+
|
21
|
+
PartyFoul.blacklisted_exceptions.must_equal ['StandardError']
|
22
|
+
PartyFoul.github.must_be_instance_of Octokit::Client
|
23
|
+
PartyFoul.github.access_token.must_equal 'test_token'
|
24
|
+
PartyFoul.github.api_endpoint.must_equal 'http://api.example.com/'
|
25
|
+
PartyFoul.owner.must_equal 'test_owner'
|
26
|
+
PartyFoul.repo.must_equal 'test_repo'
|
27
|
+
PartyFoul.repo_path.must_equal 'test_owner/test_repo'
|
28
|
+
PartyFoul.repo_url.must_equal 'http://example.com/test_owner/test_repo'
|
29
|
+
PartyFoul.branch.must_equal 'master'
|
30
|
+
PartyFoul.comment_limit.must_equal 10
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'has default values' do
|
34
|
+
PartyFoul.web_url.must_equal 'https://github.com'
|
35
|
+
PartyFoul.branch.must_equal 'master'
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe 'Party Foul Exception Handler' do
|
4
|
+
before do
|
5
|
+
PartyFoul.configure do |config|
|
6
|
+
config.oauth_token = 'abcdefg1234567890'
|
7
|
+
config.owner = 'test_owner'
|
8
|
+
config.repo = 'test_repo'
|
9
|
+
end
|
10
|
+
|
11
|
+
PartyFoul.stubs(:branch).returns('deploy')
|
12
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:title).returns('Test Title')
|
13
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:fingerprint).returns('test_fingerprint')
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when error is new' do
|
17
|
+
it 'will open a new error on GitHub' do
|
18
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:body).returns('Test Body')
|
19
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:comment).returns('Test Comment')
|
20
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(no_search_results)
|
21
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:closed').returns(no_search_results)
|
22
|
+
PartyFoul.github.expects(:create_issue).with('test_owner/test_repo', 'Test Title', 'Test Body', labels: ['bug']).returns( {number: 1} )
|
23
|
+
PartyFoul.github.expects(:references).with('test_owner/test_repo', 'heads/deploy').returns( sawyer_resource({object: {sha: 'abcdefg1234567890'}}) )
|
24
|
+
PartyFoul.github.expects(:add_comment).with('test_owner/test_repo', 1, 'Test Comment')
|
25
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when additional labels are configured' do
|
29
|
+
before do
|
30
|
+
PartyFoul.configure do |config|
|
31
|
+
config.additional_labels = ['custom', 'label']
|
32
|
+
end
|
33
|
+
end
|
34
|
+
after do
|
35
|
+
clean_up_party
|
36
|
+
end
|
37
|
+
it 'will open a new error on GitHub with the additional labels' do
|
38
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:body).returns('Test Body')
|
39
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:comment).returns('Test Comment')
|
40
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(no_search_results)
|
41
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:closed').returns(no_search_results)
|
42
|
+
PartyFoul.github.expects(:create_issue).with('test_owner/test_repo', 'Test Title', 'Test Body', :labels => ['bug', 'custom', 'label']).returns( { number: 1 } )
|
43
|
+
PartyFoul.github.expects(:references).with('test_owner/test_repo', 'heads/deploy').returns( sawyer_resource({object: {sha: 'abcdefg1234567890'}}) )
|
44
|
+
PartyFoul.github.expects(:add_comment).with('test_owner/test_repo', 1, 'Test Comment')
|
45
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when a proc for additional labels are configured' do
|
50
|
+
before do
|
51
|
+
PartyFoul.configure do |config|
|
52
|
+
config.additional_labels = Proc.new do |exception, env|
|
53
|
+
if env[:http_host] =~ /beta\./
|
54
|
+
['beta']
|
55
|
+
elsif exception.message =~ /Database/
|
56
|
+
['database_error']
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:body).returns('Test Body')
|
61
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:comment).returns('Test Comment')
|
62
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(no_search_results)
|
63
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:closed').returns(no_search_results)
|
64
|
+
PartyFoul.github.expects(:references).with('test_owner/test_repo', 'heads/deploy').returns( sawyer_resource({object: {sha: 'abcdefg1234567890'}}) )
|
65
|
+
PartyFoul.github.expects(:add_comment).with('test_owner/test_repo', 1, 'Test Comment')
|
66
|
+
end
|
67
|
+
after do
|
68
|
+
clean_up_party
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'will open a new error on GitHub with the default labels if no additional labels are returned from the proc' do
|
72
|
+
PartyFoul.github.expects(:create_issue).with('test_owner/test_repo', 'Test Title', 'Test Body', :labels => ['bug']).returns({ number: 1 })
|
73
|
+
PartyFoul::ExceptionHandler.new(stub(:message => ''), {}).run
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'will open a new error on GitHub with the additional labels based on the exception message' do
|
77
|
+
PartyFoul.github.expects(:create_issue).with('test_owner/test_repo', 'Test Title', 'Test Body', :labels => ['bug', 'database_error']).returns({ number: 1 })
|
78
|
+
PartyFoul::ExceptionHandler.new(stub(:message => 'Database'), {}).run
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'will open a new error on GitHub with the additional labels based on the env' do
|
82
|
+
PartyFoul.github.expects(:create_issue).with('test_owner/test_repo', 'Test Title', 'Test Body', :labels => ['bug', 'beta']).returns({ number: 1 })
|
83
|
+
PartyFoul::ExceptionHandler.new(stub(:message => ''), {:http_host => 'beta.example.com'}).run
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when error is not new' do
|
89
|
+
before do
|
90
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:update_body).returns('New Body')
|
91
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:comment).returns('Test Comment')
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'and open' do
|
95
|
+
before do
|
96
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(
|
97
|
+
sawyer_resource({total_count: 1, incomplete_results: false, items:[{title: 'Test Title', body: 'Test Body', state: 'open', number: 1}]}) )
|
98
|
+
PartyFoul.github.expects(:update_issue).with('test_owner/test_repo', 1, 'Test Title', 'New Body', state: 'open')
|
99
|
+
PartyFoul.github.expects(:references).with('test_owner/test_repo', 'heads/deploy').returns(
|
100
|
+
sawyer_resource({object: {sha: 'abcdefg1234567890'}}) )
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'will update the issue' do
|
104
|
+
PartyFoul.github.expects(:add_comment).with('test_owner/test_repo', 1, 'Test Comment')
|
105
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
106
|
+
end
|
107
|
+
|
108
|
+
it "doesn't post a comment if the limit has been met" do
|
109
|
+
PartyFoul.comment_limit = 10
|
110
|
+
PartyFoul::ExceptionHandler.any_instance.expects(:occurrence_count).returns(10)
|
111
|
+
PartyFoul.github.expects(:add_comment).never
|
112
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'and closed' do
|
117
|
+
it 'will update the count on the body and re-open the issue' do
|
118
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(no_search_results)
|
119
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:closed').returns(
|
120
|
+
sawyer_resource({total_count: 1, incomplete_results: false, items:[{title: 'Test Title', body: 'Test Body', state: 'closed', number: 1, labels: [{url: 'https://api.github.com/repos/test_owner/test_repo/labels/staging', name: 'staging', color: 'f29513'}]}]}) )
|
121
|
+
PartyFoul.github.expects(:update_issue).with('test_owner/test_repo', 1, 'Test Title', 'New Body', state: 'open', labels: ['bug', 'regression', 'staging'])
|
122
|
+
PartyFoul.github.expects(:add_comment).with('test_owner/test_repo', 1, 'Test Comment')
|
123
|
+
PartyFoul.github.expects(:references).with('test_owner/test_repo', 'heads/deploy').returns(sawyer_resource({object: {sha: 'abcdefg1234567890'}}))
|
124
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when issue is marked as "wontfix"' do
|
130
|
+
it 'does nothing' do
|
131
|
+
PartyFoul::IssueRenderers::Rails.any_instance.stubs(:body).returns('Test Body')
|
132
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:open').returns(no_search_results)
|
133
|
+
PartyFoul.github.expects(:search_issues).with('test_fingerprint repo:test_owner/test_repo state:closed').returns(
|
134
|
+
sawyer_resource({total_count: 1, incomplete_results: false, items:[{title: 'Test Title', body: 'Test Body', state: 'closed', number: 1, labels: [{url: 'https://api.github.com/repos/test_owner/test_repo/labels/wontfix', name: 'wontfix', color: 'f29513'}]}]}) )
|
135
|
+
PartyFoul.github.expects(:create_issue).never
|
136
|
+
PartyFoul.github.expects(:update_issue).never
|
137
|
+
PartyFoul.github.expects(:add_comment).never
|
138
|
+
PartyFoul.github.expects(:references).never
|
139
|
+
PartyFoul::ExceptionHandler.new(nil, {}).run
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#occurrence_count' do
|
144
|
+
before do
|
145
|
+
@handler = PartyFoul::ExceptionHandler.new(nil, {})
|
146
|
+
end
|
147
|
+
|
148
|
+
it "returns the count" do
|
149
|
+
@handler.send(:occurrence_count, "<th>Count</th><td>1</td>").must_equal 1
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns 0 if no count is found" do
|
153
|
+
@handler.send(:occurrence_count, "Unexpected Body").must_equal 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe '#comment_limit_met?' do
|
158
|
+
before do
|
159
|
+
@handler = PartyFoul::ExceptionHandler.new(nil, {})
|
160
|
+
end
|
161
|
+
|
162
|
+
context "with no limit" do
|
163
|
+
it "returns false when there is no limit" do
|
164
|
+
PartyFoul.configure do |config|
|
165
|
+
config.comment_limit = nil
|
166
|
+
end
|
167
|
+
@handler.send(:comment_limit_met?, "").must_equal false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context "with a limit" do
|
172
|
+
before do
|
173
|
+
PartyFoul.configure do |config|
|
174
|
+
config.comment_limit = 10
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it "returns false when there is a limit that has not been hit" do
|
179
|
+
@handler.stubs(:occurrence_count).returns(1)
|
180
|
+
@handler.send(:comment_limit_met?, "").must_equal false
|
181
|
+
end
|
182
|
+
|
183
|
+
it "returns true if the limit has been hit" do
|
184
|
+
@handler.stubs(:occurrence_count).returns(10)
|
185
|
+
@handler.send(:comment_limit_met?, "").must_equal true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe '.clean_env' do
|
191
|
+
context 'when env contains file reference' do
|
192
|
+
before do
|
193
|
+
@env = {
|
194
|
+
'action_dispatch.request.parameters' => {
|
195
|
+
:@tempfile => File.open(__FILE__)
|
196
|
+
}
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
it "marshal dump shouldn't reject values when they contain File" do
|
201
|
+
PartyFoul::ExceptionHandler.send(:clean_env, @env)['action_dispatch.request.parameters'].must_be_instance_of Hash
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|