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