party_fouls 1.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +268 -0
  3. data/Rakefile +19 -0
  4. data/lib/generators/party_foul/install_generator.rb +38 -0
  5. data/lib/generators/party_foul/templates/party_foul.rb +39 -0
  6. data/lib/party_foul/exception_handler.rb +106 -0
  7. data/lib/party_foul/issue_renderers/base.rb +187 -0
  8. data/lib/party_foul/issue_renderers/rack.rb +54 -0
  9. data/lib/party_foul/issue_renderers/rackless.rb +25 -0
  10. data/lib/party_foul/issue_renderers/rails.rb +35 -0
  11. data/lib/party_foul/issue_renderers.rb +5 -0
  12. data/lib/party_foul/middleware.rb +32 -0
  13. data/lib/party_foul/processors/base.rb +11 -0
  14. data/lib/party_foul/processors/delayed_job.rb +16 -0
  15. data/lib/party_foul/processors/resque.rb +16 -0
  16. data/lib/party_foul/processors/sidekiq.rb +17 -0
  17. data/lib/party_foul/processors/sync.rb +11 -0
  18. data/lib/party_foul/processors.rb +2 -0
  19. data/lib/party_foul/rackless_exception_handler.rb +17 -0
  20. data/lib/party_foul/version.rb +3 -0
  21. data/lib/party_foul.rb +92 -0
  22. data/test/generator_test.rb +26 -0
  23. data/test/party_foul/configure_test.rb +37 -0
  24. data/test/party_foul/exception_handler_test.rb +205 -0
  25. data/test/party_foul/issue_renderers/base_test.rb +210 -0
  26. data/test/party_foul/issue_renderers/rack_test.rb +80 -0
  27. data/test/party_foul/issue_renderers/rackless_test.rb +29 -0
  28. data/test/party_foul/issue_renderers/rails_test.rb +83 -0
  29. data/test/party_foul/middleware_test.rb +48 -0
  30. data/test/party_foul/rackless_exception_handler_test.rb +33 -0
  31. data/test/test_helper.rb +42 -0
  32. data/test/tmp/config/initializers/party_foul.rb +39 -0
  33. 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,5 @@
1
+ module PartyFoul::IssueRenderers; end
2
+ require 'party_foul/issue_renderers/base'
3
+ require 'party_foul/issue_renderers/rack'
4
+ require 'party_foul/issue_renderers/rails'
5
+ require 'party_foul/issue_renderers/rackless'
@@ -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,2 @@
1
+ module PartyFoul::Processors; end
2
+ require 'party_foul/processors/sync'
@@ -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
@@ -0,0 +1,3 @@
1
+ module PartyFoul
2
+ VERSION = '1.5.6'.freeze
3
+ 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