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.
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