airbrake-ruby 2.9.0 → 2.10.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +40 -18
  3. data/lib/airbrake-ruby/async_sender.rb +0 -6
  4. data/lib/airbrake-ruby/backtrace.rb +0 -10
  5. data/lib/airbrake-ruby/code_hunk.rb +0 -4
  6. data/lib/airbrake-ruby/config.rb +23 -22
  7. data/lib/airbrake-ruby/config/validator.rb +0 -10
  8. data/lib/airbrake-ruby/file_cache.rb +0 -6
  9. data/lib/airbrake-ruby/filter_chain.rb +0 -5
  10. data/lib/airbrake-ruby/filters/context_filter.rb +1 -0
  11. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  12. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  13. data/lib/airbrake-ruby/filters/gem_root_filter.rb +2 -3
  14. data/lib/airbrake-ruby/filters/keys_blacklist.rb +1 -2
  15. data/lib/airbrake-ruby/filters/keys_filter.rb +9 -14
  16. data/lib/airbrake-ruby/filters/keys_whitelist.rb +0 -2
  17. data/lib/airbrake-ruby/filters/root_directory_filter.rb +2 -3
  18. data/lib/airbrake-ruby/filters/system_exit_filter.rb +2 -3
  19. data/lib/airbrake-ruby/filters/thread_filter.rb +2 -4
  20. data/lib/airbrake-ruby/nested_exception.rb +0 -2
  21. data/lib/airbrake-ruby/notice.rb +6 -44
  22. data/lib/airbrake-ruby/notifier.rb +4 -40
  23. data/lib/airbrake-ruby/promise.rb +0 -6
  24. data/lib/airbrake-ruby/response.rb +0 -4
  25. data/lib/airbrake-ruby/sync_sender.rb +0 -4
  26. data/lib/airbrake-ruby/version.rb +1 -3
  27. data/spec/airbrake_spec.rb +71 -140
  28. data/spec/async_sender_spec.rb +9 -0
  29. data/spec/config_spec.rb +4 -0
  30. data/spec/filters/dependency_filter_spec.rb +16 -0
  31. data/spec/filters/exception_attributes_filter_spec.rb +65 -0
  32. data/spec/filters/keys_whitelist_spec.rb +17 -23
  33. data/spec/notice_spec.rb +111 -69
  34. data/spec/notifier_spec.rb +304 -495
  35. data/spec/response_spec.rb +82 -0
  36. data/spec/sync_sender_spec.rb +31 -14
  37. metadata +10 -2
@@ -117,6 +117,15 @@ RSpec.describe Airbrake::AsyncSender do
117
117
  @sender.close
118
118
  expect(@sender).not_to have_workers
119
119
  end
120
+
121
+ it "respawns workers on fork()", skip: %w[jruby rbx].include?(RUBY_ENGINE) do
122
+ pid = fork do
123
+ expect(@sender).to have_workers
124
+ end
125
+ Process.wait(pid)
126
+ @sender.close
127
+ expect(@sender).not_to have_workers
128
+ end
120
129
  end
121
130
 
122
131
  describe "#spawn_workers" do
@@ -25,6 +25,10 @@ RSpec.describe Airbrake::Config do
25
25
  expect(config.app_version).to be_nil
26
26
  end
27
27
 
28
+ it "sets the default versions" do
29
+ expect(config.versions).to be_empty
30
+ end
31
+
28
32
  it "sets the default host" do
29
33
  expect(config.host).to eq('https://airbrake.io')
30
34
  end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Airbrake::Filters::DependencyFilter do
4
+ let(:notice) do
5
+ Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new)
6
+ end
7
+
8
+ describe "#call" do
9
+ it "attaches loaded dependencies to context/versions/dependencies" do
10
+ subject.call(notice)
11
+ expect(notice[:context][:versions][:dependencies]).to include(
12
+ 'airbrake-ruby' => Airbrake::AIRBRAKE_RUBY_VERSION
13
+ )
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Airbrake::Filters::ExceptionAttributesFilter do
4
+ describe "#call" do
5
+ let(:out) { StringIO.new }
6
+ let(:notice) { Airbrake::Notice.new(Airbrake::Config.new, ex) }
7
+
8
+ subject { described_class.new(Logger.new(out)) }
9
+
10
+ context "when #to_airbrake returns a non-Hash object" do
11
+ let(:ex) do
12
+ Class.new(AirbrakeTestError) do
13
+ def to_airbrake
14
+ Object.new
15
+ end
16
+ end.new
17
+ end
18
+
19
+ it "doesn't raise" do
20
+ expect { subject.call(notice) }.not_to raise_error
21
+ expect(notice[:params]).to be_empty
22
+ end
23
+
24
+ it "logs the error" do
25
+ expect { subject.call(notice) }.not_to raise_error
26
+ expect(out.string).to match(/wanted Hash, got Object/)
27
+ end
28
+ end
29
+
30
+ context "when #to_airbrake errors out" do
31
+ let(:ex) do
32
+ Class.new(AirbrakeTestError) do
33
+ def to_airbrake
34
+ 1 / 0
35
+ end
36
+ end.new
37
+ end
38
+
39
+ it "doesn't raise" do
40
+ expect { subject.call(notice) }.not_to raise_error
41
+ expect(notice[:params]).to be_empty
42
+ end
43
+
44
+ it "logs the error" do
45
+ expect { subject.call(notice) }.not_to raise_error
46
+ expect(out.string).to match(/#to_airbrake failed.+ZeroDivisionError/)
47
+ end
48
+ end
49
+
50
+ context "when #to_airbrake returns a hash" do
51
+ let(:ex) do
52
+ Class.new(AirbrakeTestError) do
53
+ def to_airbrake
54
+ { params: { foo: '1' } }
55
+ end
56
+ end.new
57
+ end
58
+
59
+ it "merges parameters with the notice" do
60
+ subject.call(notice)
61
+ expect(notice[:params]).to eq(foo: '1')
62
+ end
63
+ end
64
+ end
65
+ end
@@ -146,31 +146,25 @@ RSpec.describe Airbrake::Filters::KeysWhitelist do
146
146
  end
147
147
 
148
148
  context "and it is recursive" do
149
- let(:patterns) { %w[bingo bango] }
150
-
151
- it "errors when nested hashes are not filtered" do
152
- bongo = { bingo: {} }
153
- bongo[:bingo][:bango] = bongo
149
+ let(:patterns) { ['bingo'] }
150
+
151
+ it "raises error (MRI)", skip: (
152
+ # MRI 2.3 & 2.4 may segfault on Circle CI. Example build:
153
+ # https://circleci.com/workflow-run/c112358c-e7bf-4789-9eb2-4891ea84da68
154
+ RUBY_ENGINE == 'ruby' && RUBY_VERSION =~ /\A2\.[34]\.\d+\z/
155
+ ) do
156
+ bongo = {}
157
+ bongo[:bingo] = bongo
154
158
  notice[:params] = bongo
155
159
 
156
- if RUBY_ENGINE == 'jruby'
157
- # JRuby might raise two different exceptions, which represent the
158
- # same thing. One is a Java exception, the other is a Ruby
159
- # exception. It's probably a JRuby bug:
160
- # https://github.com/jruby/jruby/issues/1903
161
- begin
162
- expect do
163
- subject.call(notice)
164
- end.to raise_error(SystemStackError)
165
- rescue RSpec::Expectations::ExpectationNotMetError
166
- expect do
167
- subject.call(notice)
168
- end.to raise_error(java.lang.StackOverflowError)
169
- end
170
- else
171
- expect do
172
- subject.call(notice)
173
- end.to raise_error(SystemStackError)
160
+ begin
161
+ expect { subject.call(notice) }.to raise_error(SystemStackError)
162
+ rescue RSpec::Expectations::ExpectationNotMetError => ex
163
+ # JRuby might raise two different exceptions, which represent the same
164
+ # thing. One is a Java exception, the other is a Ruby exception.
165
+ # Likely a bug: https://github.com/jruby/jruby/issues/1903
166
+ raise ex unless RUBY_ENGINE == 'jruby'
167
+ expect { subject.call(notice) }.to raise_error(java.lang.StackOverflowError)
174
168
  end
175
169
  end
176
170
  end
@@ -5,75 +5,6 @@ RSpec.describe Airbrake::Notice do
5
5
  described_class.new(Airbrake::Config.new, AirbrakeTestError.new, bingo: '1')
6
6
  end
7
7
 
8
- describe "#new" do
9
- let(:params) do
10
- { bingo: 'bango', bongo: 'bish' }
11
- end
12
-
13
- let(:ex) { airbrake_exception_class.new(params) }
14
-
15
- context "given an exception class, which supports #to_airbrake" do
16
- context "and when #to_airbrake returns a non-Hash object" do
17
- let(:airbrake_exception_class) do
18
- Class.new(AirbrakeTestError) do
19
- def to_airbrake
20
- Object.new
21
- end
22
- end
23
- end
24
-
25
- it "rescues the error, logs it and doesn't modify the payload" do
26
- out = StringIO.new
27
- config = Airbrake::Config.new(logger: Logger.new(out))
28
- notice = nil
29
-
30
- expect { notice = described_class.new(config, ex) }.not_to raise_error
31
- expect(out.string).to match(/#to_airbrake failed:.+Object.+must be a Hash/)
32
- expect(notice[:params]).to be_empty
33
- end
34
- end
35
-
36
- context "and when #to_airbrake errors out" do
37
- let(:airbrake_exception_class) do
38
- Class.new(AirbrakeTestError) do
39
- def to_airbrake
40
- 1 / 0
41
- end
42
- end
43
- end
44
-
45
- it "rescues the error, logs it and doesn't modify the payload" do
46
- out = StringIO.new
47
- config = Airbrake::Config.new(logger: Logger.new(out))
48
- notice = nil
49
-
50
- expect { notice = described_class.new(config, ex) }.not_to raise_error
51
- expect(out.string).to match(/#to_airbrake failed: ZeroDivisionError/)
52
- expect(notice[:params]).to be_empty
53
- end
54
- end
55
-
56
- context "and when #to_airbrake succeeds" do
57
- let(:airbrake_exception_class) do
58
- Class.new(AirbrakeTestError) do
59
- def initialize(params)
60
- @params = params
61
- end
62
-
63
- def to_airbrake
64
- { params: @params }
65
- end
66
- end
67
- end
68
-
69
- it "merges the parameters with the notice" do
70
- notice = described_class.new(Airbrake::Config.new, ex)
71
- expect(notice[:params]).to eq(params)
72
- end
73
- end
74
- end
75
- end
76
-
77
8
  describe "#to_json" do
78
9
  context "app_version" do
79
10
  context "when missing" do
@@ -99,6 +30,23 @@ RSpec.describe Airbrake::Notice do
99
30
  end
100
31
  end
101
32
 
33
+ context "when versions is empty" do
34
+ it "doesn't set the 'versions' payload" do
35
+ expect(notice.to_json).not_to match(
36
+ /"context":{"versions":{"dep":"1.2.3"}}/
37
+ )
38
+ end
39
+ end
40
+
41
+ context "when versions is not empty" do
42
+ it "sets the 'versions' payload" do
43
+ notice[:context][:versions] = { 'dep' => '1.2.3' }
44
+ expect(notice.to_json).to match(
45
+ /"context":{.*"versions":{"dep":"1.2.3"}.*}/
46
+ )
47
+ end
48
+ end
49
+
102
50
  context "truncation" do
103
51
  shared_examples 'payloads' do |size, msg|
104
52
  it msg do
@@ -189,6 +137,86 @@ RSpec.describe Airbrake::Notice do
189
137
  end
190
138
  end
191
139
 
140
+ context "given a closed IO object" do
141
+ context "and when it is not monkey-patched by ActiveSupport" do
142
+ it "is not getting truncated" do
143
+ notice[:params] = { obj: IO.new(0).tap(&:close) }
144
+ expect(notice.to_json).to match(/"obj":"#<IO:0x.+>"/)
145
+ end
146
+ end
147
+
148
+ context "and when it is monkey-patched by ActiveSupport" do
149
+ # Instances of this class contain a closed IO object assigned to an
150
+ # instance variable. Normally, the JSON gem, which we depend on can
151
+ # parse closed IO objects. However, because ActiveSupport monkey-patches
152
+ # #to_json and calls #to_a on them, they raise IOError when we try to
153
+ # serialize them.
154
+ #
155
+ # @see https://goo.gl/0A3xNC
156
+ class ObjectWithIoIvars
157
+ def initialize
158
+ @bongo = Tempfile.new('bongo').tap(&:close)
159
+ end
160
+
161
+ # @raise [NotImplementedError] when inside a Rails environment
162
+ def to_json(*)
163
+ raise NotImplementedError
164
+ end
165
+ end
166
+
167
+ # @see ObjectWithIoIvars
168
+ class ObjectWithNestedIoIvars
169
+ def initialize
170
+ @bish = ObjectWithIoIvars.new
171
+ end
172
+
173
+ # @see ObjectWithIoIvars#to_json
174
+ def to_json(*)
175
+ raise NotImplementedError
176
+ end
177
+ end
178
+
179
+ context "and also when it's a closed Tempfile" do
180
+ it "doesn't fail" do
181
+ notice[:params] = { obj: Tempfile.new('bongo').tap(&:close) }
182
+ expect(notice.to_json).to match(/"obj":"#<(Temp)?file:0x.+>"/i)
183
+ end
184
+ end
185
+
186
+ context "and also when it's an IO ivar" do
187
+ it "doesn't fail" do
188
+ notice[:params] = { obj: ObjectWithIoIvars.new }
189
+ expect(notice.to_json).to match(/"obj":".+ObjectWithIoIvars.+"/)
190
+ end
191
+
192
+ context "and when it's deeply nested inside a hash" do
193
+ it "doesn't fail" do
194
+ notice[:params] = { a: { b: { c: ObjectWithIoIvars.new } } }
195
+ expect(notice.to_json).to match(
196
+ /"params":{"a":{"b":{"c":".+ObjectWithIoIvars.+"}}.*}/
197
+ )
198
+ end
199
+ end
200
+
201
+ context "and when it's deeply nested inside an array" do
202
+ it "doesn't fail" do
203
+ notice[:params] = { a: [[ObjectWithIoIvars.new]] }
204
+ expect(notice.to_json).to match(
205
+ /"params":{"a":\[\[".+ObjectWithIoIvars.+"\]\].*}/
206
+ )
207
+ end
208
+ end
209
+ end
210
+
211
+ context "and also when it's a non-IO ivar, which contains an IO ivar itself" do
212
+ it "doesn't fail" do
213
+ notice[:params] = { obj: ObjectWithNestedIoIvars.new }
214
+ expect(notice.to_json).to match(/"obj":".+ObjectWithNested.+"/)
215
+ end
216
+ end
217
+ end
218
+ end
219
+
192
220
  it "overwrites the 'notifier' payload with the default values" do
193
221
  notice[:notifier] = { name: 'bingo', bango: 'bongo' }
194
222
 
@@ -209,6 +237,20 @@ RSpec.describe Airbrake::Notice do
209
237
  expect(notice.to_json).
210
238
  to match(%r|"environment":{"program_name":.+/rspec.*|)
211
239
  end
240
+
241
+ it "contains errors" do
242
+ expect(notice.to_json).
243
+ to match(/"errors":\[{"type":"AirbrakeTestError","message":"App crash/)
244
+ end
245
+
246
+ it "contains a backtrace" do
247
+ expect(notice.to_json).
248
+ to match(%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|)
249
+ end
250
+
251
+ it "contains params" do
252
+ expect(notice.to_json).to match(/"params":{"bingo":"1"}/)
253
+ end
212
254
  end
213
255
 
214
256
  describe "#[]" do
@@ -1,628 +1,437 @@
1
1
  require 'spec_helper'
2
2
 
3
+ # rubocop:disable Layout/DotPosition
3
4
  RSpec.describe Airbrake::Notifier do
4
- def expect_a_request_with_body(body)
5
- expect(a_request(:post, endpoint).with(body: body)).to have_been_made.once
5
+ let(:user_params) do
6
+ { project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
6
7
  end
7
8
 
8
- let(:project_id) { 105138 }
9
- let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
10
- let(:localhost) { 'http://localhost:8080' }
11
- let(:endpoint) { "https://airbrake.io/api/v3/projects/#{project_id}/notices" }
12
-
13
- let(:airbrake_params) do
14
- { project_id: project_id,
15
- project_key: project_key,
16
- logger: Logger.new(StringIO.new) }
17
- end
18
-
19
- let(:ex) { AirbrakeTestError.new }
20
-
21
- before do
22
- # rubocop:disable Metrics/LineLength
23
- body = '{"id":"00054414-b147-6ffa-85d6-1524d83362a6","url":"http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6"}'
24
- # rubocop:enable Metrics/LineLength
25
- stub_request(:post, endpoint).to_return(status: 201, body: body)
26
- @airbrake = described_class.new(airbrake_params)
27
- end
9
+ subject { described_class.new(user_params) }
28
10
 
29
11
  describe "#new" do
30
- context "raises error if" do
31
- example ":project_id is not provided" do
32
- expect { described_class.new(project_key: project_key) }.
33
- to raise_error(Airbrake::Error, ':project_id is required')
34
- end
12
+ describe "default filter addition" do
13
+ before { allow_any_instance_of(Airbrake::FilterChain).to receive(:add_filter) }
35
14
 
36
- example ":project_key is not provided" do
37
- expect { described_class.new(project_id: project_id) }.
38
- to raise_error(Airbrake::Error, ':project_key is required')
15
+ it "appends the context filter" do
16
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
17
+ .with(instance_of(Airbrake::Filters::ContextFilter))
18
+ described_class.new(user_params)
39
19
  end
40
20
 
41
- example "neither :project_id nor :project_key are provided" do
42
- expect { described_class.new({}) }.
43
- to raise_error(Airbrake::Error, ':project_id is required')
21
+ it "appends the exception attributes filter" do
22
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
23
+ .with(instance_of(Airbrake::Filters::ExceptionAttributesFilter))
24
+ described_class.new(user_params)
44
25
  end
45
- end
46
-
47
- context "when the argument is Airbrake::Config" do
48
- it "uses it instead of the hash" do
49
- airbrake = described_class.new(
50
- Airbrake::Config.new(project_id: 123, project_key: '321')
51
- )
52
- config = airbrake.instance_variable_get(:@config)
53
- expect(config.project_id).to eq(123)
54
- expect(config.project_key).to eq('321')
55
- end
56
- end
57
- end
58
26
 
59
- describe "#notify_sync" do
60
- it "returns a hash with error id & url" do
61
- expect(@airbrake.notify_sync(ex)).to(
62
- eq(
63
- 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
64
- 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
65
- )
66
- )
67
- end
68
-
69
- describe "first argument" do
70
- context "when it is a Notice" do
71
- it "sends the argument" do
72
- notice = @airbrake.build_notice(ex)
73
- @airbrake.notify_sync(notice)
74
-
75
- # rubocop:disable Metrics/LineLength
76
- expected_body = %r|
77
- {"errors":\[{"type":"AirbrakeTestError","message":"App\scrashed!","backtrace":\[
78
- {"file":"[\w/-]+/spec/spec_helper.rb","line":\d+,"function":"<top\s\(required\)>"},
79
- {"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"},
80
- {"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"}
81
- |x
82
- # rubocop:enable Metrics/LineLength
83
-
84
- expect(
85
- a_request(:post, endpoint).
86
- with(body: expected_body)
87
- ).to have_been_made.once
27
+ context "when user config has some whitelist keys" do
28
+ it "appends the whitelist filter" do
29
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
30
+ .with(instance_of(Airbrake::Filters::KeysWhitelist))
31
+ described_class.new(user_params.merge(whitelist_keys: ['foo']))
88
32
  end
89
-
90
- it "appends provided params to the notice" do
91
- notice = @airbrake.build_notice(ex)
92
- @airbrake.notify_sync(notice, bingo: 'bango')
93
-
94
- expect_a_request_with_body(/"params":{.*"bingo":"bango".*}/)
95
- end
96
- end
97
- end
98
-
99
- describe "request" do
100
- before do
101
- @airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish')
102
33
  end
103
34
 
104
- it "is being made over HTTPS" do
105
- expect(
106
- a_request(:post, endpoint).
107
- with { |req| req.uri.port == 443 }
108
- ).to have_been_made.once
109
- end
110
-
111
- describe "headers" do
112
- def expect_a_request_with_headers(headers)
113
- expect(
114
- a_request(:post, endpoint).
115
- with(headers: headers)
116
- ).to have_been_made.once
117
- end
118
-
119
- it "POSTs JSON to Airbrake" do
120
- expect_a_request_with_headers('Content-Type' => 'application/json')
121
- end
122
-
123
- it "sets User-Agent" do
124
- ua = "airbrake-ruby/#{Airbrake::AIRBRAKE_RUBY_VERSION} Ruby/#{RUBY_VERSION}"
125
- expect_a_request_with_headers('User-Agent' => ua)
35
+ context "when user config doesn't have any whitelist keys" do
36
+ it "doesn't append the whitelist filter" do
37
+ expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
38
+ .with(instance_of(Airbrake::Filters::KeysWhitelist))
39
+ described_class.new(user_params)
126
40
  end
127
41
  end
128
42
 
129
- describe "body" do
130
- it "features 'notifier'" do
131
- expect_a_request_with_body(/"notifier":{"name":"airbrake-ruby"/)
132
- end
133
-
134
- it "features 'context'" do
135
- expect_a_request_with_body(/"context":{.*"os":"[\.\w-]+"/)
136
- end
137
-
138
- it "features 'errors'" do
139
- expect_a_request_with_body(
140
- /"errors":\[{"type":"AirbrakeTestError","message":"App crash/
141
- )
142
- end
143
-
144
- it "features 'backtrace'" do
145
- expect_a_request_with_body(
146
- %r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|
147
- )
43
+ context "when user config has some blacklist keys" do
44
+ it "appends the blacklist filter" do
45
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
46
+ .with(instance_of(Airbrake::Filters::KeysBlacklist))
47
+ described_class.new(user_params.merge(blacklist_keys: ['bar']))
148
48
  end
149
-
150
- it "features 'params'" do
151
- expect_a_request_with_body(
152
- /"params":{"bingo":\["bango"\],"bongo":"bish".*}/
153
- )
154
- end
155
- end
156
- end
157
-
158
- describe "response body when it is" do
159
- before do
160
- @stdout = StringIO.new
161
- params = {
162
- logger: Logger.new(@stdout).tap { |l| l.level = Logger::DEBUG }
163
- }
164
- @airbrake = described_class.new(airbrake_params.merge(params))
165
49
  end
166
50
 
167
- shared_examples "HTTP codes" do |code, body, expected_output|
168
- it "logs error #{code}" do
169
- stub_request(:post, endpoint).to_return(status: code, body: body)
170
-
171
- expect(@stdout.string).to be_empty
172
-
173
- response = @airbrake.notify_sync(ex)
174
-
175
- expect(@stdout.string).to match(expected_output)
176
- expect(response).to be_a Hash
177
-
178
- if response['message']
179
- expect(response['message']).to satisfy do |error|
180
- error.is_a?(Exception) || error.is_a?(String)
181
- end
182
- end
51
+ context "when user config doesn't have any blacklist keys" do
52
+ it "doesn't append the blacklist filter" do
53
+ expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
54
+ .with(instance_of(Airbrake::Filters::KeysBlacklist))
55
+ described_class.new(user_params)
183
56
  end
184
57
  end
185
58
 
186
- context "a hash with response and invalid status" do
187
- include_examples 'HTTP codes', 200,
188
- '{"id":"1","url":"https://airbrake.io/locate/1"}',
189
- %r{unexpected code \(200\). Body: .+url":"https://airbrake.+}
190
- end
191
-
192
- context "an empty page" do
193
- include_examples 'HTTP codes', 200, '',
194
- /ERROR -- : .+ unexpected code \(200\). Body: \[EMPTY_BODY\]/
59
+ context "when user config specifies a root directory" do
60
+ it "appends the root directory filter" do
61
+ expect_any_instance_of(Airbrake::FilterChain).to receive(:add_filter)
62
+ .with(instance_of(Airbrake::Filters::RootDirectoryFilter))
63
+ described_class.new(user_params.merge(root_directory: '/foo'))
64
+ end
195
65
  end
196
66
 
197
- context "a valid body with code 201" do
198
- include_examples 'HTTP codes', 201,
199
- '{"id":"1","url":"https://airbrake.io/locate/1"}',
200
- %r|DEBUG -- : .+url"=>"https://airbrake.io/locate/1"}|
67
+ context "when user config doesn't specify a root directory" do
68
+ it "doesn't append the root directory filter" do
69
+ expect_any_instance_of(Airbrake::Config).to receive(:root_directory)
70
+ .and_return(nil)
71
+ expect_any_instance_of(Airbrake::FilterChain).not_to receive(:add_filter)
72
+ .with(instance_of(Airbrake::Filters::RootDirectoryFilter))
73
+ described_class.new(user_params)
74
+ end
201
75
  end
76
+ end
202
77
 
203
- context "a non-parseable page" do
204
- include_examples 'HTTP codes', 400, 'bingo bango bongo',
205
- /ERROR -- : .+unexpected token at 'bingo.+'\)\. Body: bingo.+/
206
- end
78
+ context "when user config doesn't contain a project id" do
79
+ let(:user_config) { { project_id: nil } }
207
80
 
208
- context "error 400" do
209
- include_examples 'HTTP codes', 400, '{"message": "Invalid Content-Type header."}',
210
- /ERROR -- : .+ Invalid Content-Type header\./
81
+ it "raises error" do
82
+ expect { described_class.new(user_config) }
83
+ .to raise_error(Airbrake::Error, ':project_id is required')
211
84
  end
85
+ end
212
86
 
213
- context "error 401" do
214
- include_examples 'HTTP codes', 401,
215
- '{"message":"Project not found or access denied."}',
216
- /ERROR -- : .+ Project not found or access denied./
217
- end
87
+ context "when user config doesn't contain a project key" do
88
+ let(:user_config) { { project_id: 1, project_key: nil } }
218
89
 
219
- context "error 420" do
220
- include_examples 'HTTP codes', 420,
221
- '{"message":"Project is rate limited."}',
222
- /ERROR -- : .+ Project is rate limited./
90
+ it "raises error" do
91
+ expect { described_class.new(user_config) }
92
+ .to raise_error(Airbrake::Error, ':project_key is required')
223
93
  end
94
+ end
95
+ end
224
96
 
225
- context "the IP rate limit message" do
226
- include_examples 'HTTP codes', 429, '{"message": "IP is rate limited"}',
227
- /ERROR -- : .+ IP is rate limited/
228
- end
97
+ describe "#notify" do
98
+ let(:endpoint) { 'https://airbrake.io/api/v3/projects/1/notices' }
229
99
 
230
- context "the internal server error" do
231
- include_examples 'HTTP codes', 500, 'Internal Server Error',
232
- /ERROR -- : .+ unexpected code \(500\). Body: Internal.+ Error/
233
- end
100
+ subject { described_class.new(user_params) }
234
101
 
235
- context "too long it truncates it and" do
236
- include_examples 'HTTP codes', 123, '123 ' * 1000,
237
- /ERROR -- : .+ unexpected code \(123\). Body: .+ 123 123 1\.\.\./
238
- end
102
+ let(:body) do
103
+ {
104
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
105
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
106
+ }.to_json
239
107
  end
240
108
 
241
- describe "connection timeout" do
242
- it "logs the error when it occurs" do
243
- stub_request(:post, endpoint).to_timeout
109
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body) }
244
110
 
245
- stderr = StringIO.new
246
- params = airbrake_params.merge(logger: Logger.new(stderr))
247
- airbrake = described_class.new(params)
111
+ it "returns a promise" do
112
+ expect(subject.notify('ex')).to be_an(Airbrake::Promise)
113
+ sleep 1
114
+ end
248
115
 
249
- airbrake.notify_sync(ex)
116
+ it "refines the notice object" do
117
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
118
+ notice = subject.build_notice('ex')
119
+ subject.notify(notice)
120
+ expect(notice[:params]).to eq(foo: 'bar')
121
+ sleep 1
122
+ end
250
123
 
251
- expect(stderr.string).
252
- to match(/ERROR -- : .+ HTTP error: execution expired/)
124
+ context "when a notice is not ignored" do
125
+ it "yields the notice" do
126
+ expect { |b| subject.notify('ex', &b) }
127
+ .to yield_with_args(Airbrake::Notice)
128
+ sleep 1
253
129
  end
254
130
  end
255
131
 
256
- describe "unicode payload" do
257
- context "with valid strings" do
258
- it "works correctly" do
259
- @airbrake.notify_sync(ex, unicode: "ü ö ä Ä Ü Ö ß привет €25.00 한글")
132
+ context "when a notice is ignored" do
133
+ before { subject.add_filter(&:ignore!) }
260
134
 
261
- expect(
262
- a_request(:post, endpoint).
263
- with(body: /"unicode":"ü ö ä Ä Ü Ö ß привет €25.00 한글"/)
264
- ).to have_been_made.once
265
- end
135
+ it "doesn't yield the notice" do
136
+ expect { |b| subject.notify('ex', &b) }
137
+ .not_to yield_with_args(Airbrake::Notice)
266
138
  end
267
139
 
268
- context "with invalid strings" do
269
- it "doesn't raise error when string has invalid encoding" do
270
- expect do
271
- @airbrake.notify_sync('bingo', bongo: "bango\xAE")
272
- end.not_to raise_error
273
- end
274
-
275
- it "doesn't raise error when string has valid encoding, but invalid characters" do
276
- # Shenanigans to get a bad ASCII-8BIT string. Direct conversion raises error.
277
- encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
278
- bad_string = Base64.decode64(encoded)
279
-
280
- expect do
281
- @airbrake.notify_sync('bingo', bongo: bad_string)
282
- end.not_to raise_error
283
- end
140
+ it "returns a rejected promise" do
141
+ value = subject.notify('ex').value
142
+ expect(value['error']).to match(/was marked as ignored/)
284
143
  end
285
144
  end
286
145
 
287
- describe "a closed IO object" do
288
- context "outside of the Rails environment" do
289
- it "is not getting truncated" do
290
- @airbrake.notify_sync(ex, bingo: IO.new(0).tap(&:close))
291
-
292
- expect(
293
- a_request(:post, endpoint).with(body: /"bingo":"#<IO:0x.+>"/)
294
- ).to have_been_made.once
295
- end
146
+ context "when async sender has workers" do
147
+ it "sends an exception asynchronously" do
148
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:send)
149
+ subject.notify('foo', bingo: 'bango')
296
150
  end
151
+ end
297
152
 
298
- context "inside the Rails environment" do
299
- ##
300
- # Instances of this class contain a closed IO object assigned to an instance
301
- # variable. Normally, the JSON gem, which we depend on can parse closed IO
302
- # objects. However, because ActiveSupport monkey-patches #to_json and calls
303
- # #to_a on them, they raise IOError when we try to serialize them.
304
- #
305
- # @see https://goo.gl/0A3xNC
306
- class ObjectWithIoIvars
307
- def initialize
308
- @bongo = Tempfile.new('bongo').tap(&:close)
309
- end
310
-
311
- # @raise [NotImplementedError] when inside a Rails environment
312
- def to_json(*)
313
- raise NotImplementedError
314
- end
315
- end
316
-
317
- ##
318
- # @see ObjectWithIoIvars
319
- class ObjectWithNestedIoIvars
320
- def initialize
321
- @bish = ObjectWithIoIvars.new
322
- end
323
-
324
- # @see ObjectWithIoIvars#to_json
325
- def to_json(*)
326
- raise NotImplementedError
327
- end
328
- end
329
-
330
- shared_examples 'truncation' do |params, expected|
331
- it "filters it out" do
332
- @airbrake.notify_sync(ex, params)
333
-
334
- expect(
335
- a_request(:post, endpoint).with(body: expected)
336
- ).to have_been_made.once
337
- end
338
- end
339
-
340
- context "which is an instance of" do
341
- context "Tempfile" do
342
- params = { bango: Tempfile.new('bongo').tap(&:close) }
343
- include_examples 'truncation', params, /"bango":"#<(Temp)?file:0x.+>"/i
344
- end
345
-
346
- context "a non-IO class but with" do
347
- context "IO ivars" do
348
- params = { bongo: ObjectWithIoIvars.new }
349
- include_examples 'truncation', params, /"bongo":".+ObjectWithIoIvars.+"/
350
- end
351
-
352
- context "a non-IO ivar, which contains an IO ivar itself" do
353
- params = { bish: ObjectWithNestedIoIvars.new }
354
- include_examples 'truncation', params, /"bish":".+ObjectWithNested.+"/
355
- end
356
- end
357
- end
358
-
359
- context "which is deeply nested inside a hash" do
360
- params = { bingo: { bango: { bongo: ObjectWithIoIvars.new } } }
361
- include_examples(
362
- 'truncation',
363
- params,
364
- /"params":{"bingo":{"bango":{"bongo":".+ObjectWithIoIvars.+"}}.*}/
365
- )
366
- end
367
-
368
- context "which is deeply nested inside an array" do
369
- params = { bingo: [[ObjectWithIoIvars.new]] }
370
- include_examples(
371
- 'truncation',
372
- params,
373
- /"params":{"bingo":\[\[".+ObjectWithIoIvars.+"\]\].*}/
374
- )
375
- end
153
+ context "when async sender doesn't have workers" do
154
+ it "sends an exception synchronously" do
155
+ expect_any_instance_of(Airbrake::AsyncSender)
156
+ .to receive(:has_workers?).and_return(false)
157
+ expect_any_instance_of(Airbrake::SyncSender).to receive(:send)
158
+ subject.notify('foo', bingo: 'bango')
376
159
  end
377
160
  end
378
161
 
379
- describe "block argument" do
380
- context "when a notice is not ignored" do
381
- it "yields the notice" do
382
- @airbrake.notify_sync(ex) { |notice| notice[:params][:bingo] = :bango }
383
- expect_a_request_with_body(/params":{.*"bingo":"bango".*}/)
384
- end
162
+ context "when the provided environment is ignored" do
163
+ subject do
164
+ described_class.new(
165
+ user_params.merge(environment: 'test', ignore_environments: ['test'])
166
+ )
385
167
  end
386
168
 
387
- context "when a notice is ignored before entering the block" do
388
- it "doesn't call the given block" do
389
- @airbrake.add_filter(&:ignore!)
390
- @airbrake.notify_sync(ex) { |n| n[:params][:bingo] = :bango }
391
- expect(
392
- a_request(:post, endpoint).
393
- with(body: /params":{.*"bingo":"bango".*}/)
394
- ).not_to have_been_made
395
- end
169
+ it "doesn't send an notice" do
170
+ expect_any_instance_of(Airbrake::AsyncSender).not_to receive(:send)
171
+ subject.notify('foo', bingo: 'bango')
396
172
  end
397
173
 
398
- context "and when a notice is ignored inside the block" do
399
- it "doesn't send the notice" do
400
- @airbrake.notify_sync(ex, &:ignore!)
401
- expect(a_request(:post, endpoint)).not_to have_been_made
402
- end
174
+ it "returns a rejected promise" do
175
+ promise = subject.notify('foo', bingo: 'bango')
176
+ expect(promise.value).to eq('error' => "The 'test' environment is ignored")
403
177
  end
404
178
  end
405
179
  end
406
180
 
407
- describe "#notify" do
408
- it "sends an exception asynchronously" do
409
- @airbrake.notify(ex, bingo: 'bango')
410
-
411
- sleep 1
181
+ describe "#notify_sync" do
182
+ let(:endpoint) { 'https://airbrake.io/api/v3/projects/1/notices' }
412
183
 
413
- expect_a_request_with_body(/params":{"bingo":"bango".*}/)
184
+ let(:user_params) do
185
+ { project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
414
186
  end
415
187
 
416
- it "returns a promise" do
417
- expect(@airbrake.notify(ex)).to be_an(Airbrake::Promise)
418
- sleep 1
188
+ let(:body) do
189
+ {
190
+ 'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
191
+ 'url' => 'http://localhost/locate/00054414-b147-6ffa-85d6-1524d83362a6'
192
+ }
419
193
  end
420
194
 
421
- it "falls back to synchronous delivery when the async sender is dead" do
422
- out = StringIO.new
423
- notifier = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
424
- async_sender = notifier.instance_variable_get(:@async_sender)
195
+ before { stub_request(:post, endpoint).to_return(status: 201, body: body.to_json) }
425
196
 
426
- expect(async_sender).to have_workers
427
- async_sender.instance_variable_get(:@workers).list.each(&:kill)
428
- sleep 1
429
- expect(async_sender).not_to have_workers
430
-
431
- notifier.notify('bango')
432
- expect(out.string).to match(/falling back to sync delivery/)
433
-
434
- notifier.close
197
+ it "returns a reponse hash" do
198
+ expect(subject.notify_sync('ex')).to eq(body)
435
199
  end
436
200
 
437
- it "respawns workers on fork()", skip: %w[jruby rbx].include?(RUBY_ENGINE) do
438
- out = StringIO.new
439
- notifier = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
440
-
441
- notifier.notify('bingo', bingo: 'bango')
442
- sleep 1
443
- expect(out.string).not_to match(/falling back to sync delivery/)
444
- expect_a_request_with_body(/"bingo":"bango"/)
445
-
446
- pid = fork do
447
- expect(notifier.instance_variable_get(:@async_sender)).to have_workers
448
- notifier.notify('bango', bongo: 'bish')
449
- sleep 1
450
- expect(out.string).not_to match(/falling back to sync delivery/)
451
- expect_a_request_with_body(/"bingo":"bango"/)
452
- end
453
-
454
- Process.wait(pid)
455
- notifier.close
456
- expect(notifier.instance_variable_get(:@async_sender)).not_to have_workers
201
+ it "refines the notice object" do
202
+ subject.add_filter { |n| n[:params] = { foo: 'bar' } }
203
+ notice = subject.build_notice('ex')
204
+ subject.notify_sync(notice)
205
+ expect(notice[:params]).to eq(foo: 'bar')
457
206
  end
458
- end
459
-
460
- describe "#add_filter" do
461
- it "filters notices" do
462
- @airbrake.add_filter do |notice|
463
- notice[:params][:password] = '[Filtered]'.freeze if notice[:params][:password]
464
- end
465
-
466
- @airbrake.notify_sync(ex, password: 's4kr4t')
467
207
 
208
+ it "sends an exception synchronously" do
209
+ subject.notify_sync('foo', bingo: 'bango')
468
210
  expect(
469
- a_request(:post, endpoint).
470
- with(body: /params":{"password":"\[Filtered\]".*}/)
211
+ a_request(:post, endpoint).with(
212
+ body: /"params":{.*"bingo":"bango".*}/
213
+ )
471
214
  ).to have_been_made.once
472
215
  end
473
216
 
474
- it "accepts multiple filters" do
475
- %i[bingo bongo bash].each do |key|
476
- @airbrake.add_filter do |notice|
477
- notice[:params][key] = '[Filtered]'.freeze if notice[:params][key]
478
- end
217
+ context "when a notice is not ignored" do
218
+ it "yields the notice" do
219
+ expect { |b| subject.notify_sync('ex', &b) }
220
+ .to yield_with_args(Airbrake::Notice)
479
221
  end
480
-
481
- @airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish', bash: 'bosh')
482
-
483
- # rubocop:disable Metrics/LineLength
484
- body = /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]","bash":"\[Filtered\]".*}/
485
- # rubocop:enable Metrics/LineLength
486
-
487
- expect(
488
- a_request(:post, endpoint).
489
- with(body: body)
490
- ).to have_been_made.once
491
222
  end
492
223
 
493
- it "ignores all notices" do
494
- @airbrake.add_filter(&:ignore!)
224
+ context "when a notice is ignored" do
225
+ before { subject.add_filter(&:ignore!) }
495
226
 
496
- @airbrake.notify_sync(ex)
227
+ it "doesn't yield the notice" do
228
+ expect { |b| subject.notify_sync('ex', &b) }
229
+ .not_to yield_with_args(Airbrake::Notice)
230
+ end
497
231
 
498
- expect(a_request(:post, endpoint)).not_to have_been_made
232
+ it "returns an error hash" do
233
+ response = subject.notify_sync('ex')
234
+ expect(response['error']).to match(/was marked as ignored/)
235
+ end
499
236
  end
500
237
 
501
- it "ignores specific notices" do
502
- @airbrake.add_filter do |notice|
503
- notice.ignore! if notice[:errors][0][:type] == 'RuntimeError'
238
+ context "when the provided environment is ignored" do
239
+ subject do
240
+ described_class.new(
241
+ user_params.merge(environment: 'test', ignore_environments: ['test'])
242
+ )
504
243
  end
505
244
 
506
- @airbrake.notify_sync(RuntimeError.new('Not caring!'))
507
- expect(a_request(:post, endpoint)).not_to have_been_made
245
+ it "doesn't send an notice" do
246
+ expect_any_instance_of(Airbrake::SyncSender).not_to receive(:send)
247
+ subject.notify_sync('foo', bingo: 'bango')
248
+ end
508
249
 
509
- @airbrake.notify_sync(ex)
510
- expect(a_request(:post, endpoint)).to have_been_made.once
250
+ it "returns an error hash" do
251
+ expect(subject.notify_sync('foo'))
252
+ .to eq('error' => "The 'test' environment is ignored")
253
+ end
511
254
  end
255
+ end
512
256
 
513
- it "ignores descendant classes" do
514
- descendant = Class.new(AirbrakeTestError)
515
-
516
- @airbrake.add_filter do |notice|
517
- notice.ignore! if notice.stash[:exception].is_a?(AirbrakeTestError)
257
+ describe "#add_filter" do
258
+ context "given a block" do
259
+ it "appends a new filter to the filter chain" do
260
+ notifier = subject
261
+ b = proc {}
262
+ expect_any_instance_of(Airbrake::FilterChain)
263
+ .to receive(:add_filter) { |*args| expect(args.last).to be(b) }
264
+ notifier.add_filter(&b)
518
265
  end
266
+ end
519
267
 
520
- @airbrake.notify_sync(descendant.new('Not caring!'))
521
- expect(a_request(:post, endpoint)).not_to have_been_made
522
-
523
- @airbrake.notify_sync(RuntimeError.new('Catch me if you can!'))
524
- expect(a_request(:post, endpoint)).to have_been_made.once
268
+ context "given a class" do
269
+ it "appends a new filter to the filter chain" do
270
+ notifier = subject
271
+ klass = Class.new
272
+ expect_any_instance_of(Airbrake::FilterChain)
273
+ .to receive(:add_filter).with(klass)
274
+ notifier.add_filter(klass)
275
+ end
525
276
  end
526
277
  end
527
278
 
528
279
  describe "#build_notice" do
529
- it "builds a notice from exception" do
530
- expect(@airbrake.build_notice(ex)).to be_an Airbrake::Notice
280
+ context "when given exception is another notice" do
281
+ it "merges params with the notice" do
282
+ notice = subject.build_notice('ex')
283
+ other = subject.build_notice(notice, foo: 'bar')
284
+ expect(other[:params]).to eq(foo: 'bar')
285
+ end
286
+
287
+ it "it returns the provided notice" do
288
+ notice = subject.build_notice('ex')
289
+ other = subject.build_notice(notice, foo: 'bar')
290
+ expect(other).to eq(notice)
291
+ end
531
292
  end
532
293
 
533
- context "given a non-exception with calculated internal frames only" do
294
+ context "when given exception is an Exception" do
534
295
  it "prevents mutation of passed-in params hash" do
535
- params = { only_this_item: true }
536
- notice = @airbrake.build_notice(RuntimeError.new('bingo'), params)
537
- notice[:params][:extra_item] = :not_in_original_params
538
- expect(params).to eq(only_this_item: true)
296
+ params = { immutable: true }
297
+ notice = subject.build_notice('ex', params)
298
+ notice[:params][:mutable] = true
299
+ expect(params).to eq(immutable: true)
300
+ end
301
+
302
+ context "and also when it doesn't have own backtrace" do
303
+ context "and when the generated backtrace consists only of library frames" do
304
+ it "returns the full generated backtrace" do
305
+ backtrace = [
306
+ "/lib/airbrake-ruby/a.rb:84:in `build_notice'",
307
+ "/lib/airbrake-ruby/b.rb:124:in `send_notice'"
308
+ ]
309
+ allow(Kernel).to receive(:caller).and_return(backtrace)
310
+
311
+ notice = subject.build_notice(Exception.new)
312
+
313
+ expect(notice[:errors].first[:backtrace]).to eq(
314
+ [
315
+ { file: '/lib/airbrake-ruby/a.rb', line: 84, function: 'build_notice' },
316
+ { file: '/lib/airbrake-ruby/b.rb', line: 124, function: 'send_notice' }
317
+ ]
318
+ )
319
+ end
320
+ end
321
+
322
+ context "and when the generated backtrace consists of mixed frames" do
323
+ it "returns the filtered backtrace" do
324
+ backtrace = [
325
+ "/airbrake-ruby/lib/airbrake-ruby/a.rb:84:in `b'",
326
+ "/airbrake-ruby/lib/foo/b.rb:84:in `build'",
327
+ "/airbrake-ruby/lib/bar/c.rb:124:in `send'"
328
+ ]
329
+ allow(Kernel).to receive(:caller).and_return(backtrace)
330
+
331
+ notice = subject.build_notice(Exception.new)
332
+
333
+ expect(notice[:errors].first[:backtrace]).to eq(
334
+ [
335
+ { file: '/airbrake-ruby/lib/foo/b.rb', line: 84, function: 'build' },
336
+ { file: '/airbrake-ruby/lib/bar/c.rb', line: 124, function: 'send' }
337
+ ]
338
+ )
339
+ end
340
+ end
341
+ end
342
+ end
343
+
344
+ # TODO: this seems to be bugged. Fix later.
345
+ context "when given exception is a Java exception", skip: true do
346
+ before do
347
+ expect(Airbrake::Backtrace).to receive(:java_exception?).and_return(true)
539
348
  end
540
349
 
541
- it "returns the internal frames nevertheless" do
350
+ it "automatically generates the backtrace" do
542
351
  backtrace = [
543
- "/airbrake-ruby/lib/airbrake-ruby/notifier.rb:84:in `build_notice'",
544
- "/airbrake-ruby/lib/airbrake-ruby/notifier.rb:124:in `send_notice'",
545
- "/airbrake-ruby/lib/airbrake-ruby/notifier.rb:52:in `notify_sync'"
352
+ "org/jruby/RubyKernel.java:998:in `eval'",
353
+ "/ruby/stdlib/irb/workspace.rb:87:in `evaluate'",
354
+ "/ruby/stdlib/irb.rb:489:in `block in eval_input'"
546
355
  ]
356
+ allow(Kernel).to receive(:caller).and_return(backtrace)
357
+
358
+ notice = subject.build_notice(Exception.new)
547
359
 
548
360
  # rubocop:disable Metrics/LineLength
549
- parsed_backtrace = [
550
- { file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 84, function: 'build_notice' },
551
- { file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 124, function: 'send_notice' },
552
- { file: '/airbrake-ruby/lib/airbrake-ruby/notifier.rb', line: 52, function: 'notify_sync' }
553
- ]
361
+ expect(notice[:errors].first[:backtrace]).to eq(
362
+ [
363
+ { file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
364
+ { file: '/ruby/stdlib/irb/workspace.rb', line: 87, function: 'evaluate' },
365
+ { file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' }
366
+ ]
367
+ )
554
368
  # rubocop:enable Metrics/LineLength
555
-
556
- allow(Kernel).to receive(:caller).and_return(backtrace)
557
-
558
- notice = @airbrake.build_notice('bingo')
559
- expect(notice[:errors][0][:backtrace]).to eq(parsed_backtrace)
560
369
  end
561
370
  end
562
- end
563
-
564
- describe "#close" do
565
- context "when using #notify on a closed notifier" do
566
- it "raises error" do
567
- notifier = described_class.new(airbrake_params)
568
- notifier.close
569
371
 
570
- expect { notifier.notify(AirbrakeTestError.new) }.
571
- to raise_error(Airbrake::Error, /closed Airbrake instance/)
372
+ context "when async sender is closed" do
373
+ before do
374
+ expect_any_instance_of(Airbrake::AsyncSender)
375
+ .to receive(:closed?).and_return(true)
572
376
  end
573
- end
574
377
 
575
- context "at program exit when it was closed manually" do
576
- it "doesn't raise error", skip: RUBY_ENGINE == 'jruby' do
577
- expect do
578
- Process.wait(fork { described_class.new(airbrake_params) })
579
- end.not_to raise_error
378
+ it "raises error" do
379
+ expect { subject.build_notice(Exception.new) }.to raise_error(
380
+ Airbrake::Error,
381
+ 'attempted to build Exception with closed Airbrake instance'
382
+ )
580
383
  end
581
384
  end
582
385
  end
583
386
 
584
- describe "#create_deploy" do
585
- let(:deploy_endpoint) do
586
- "https://airbrake.io/api/v4/projects/#{project_id}/deploys?key=#{project_key}"
587
- end
588
-
589
- before do
590
- stub_request(:post, deploy_endpoint).to_return(status: 201, body: '{"id":"123"}')
387
+ describe "#close" do
388
+ it "sends the close message to async sender" do
389
+ expect_any_instance_of(Airbrake::AsyncSender).to receive(:close)
390
+ subject.close
591
391
  end
392
+ end
592
393
 
593
- it "sends a request to the deploy API" do
594
- @airbrake.create_deploy({})
595
- expect(a_request(:post, deploy_endpoint)).to have_been_made.once
394
+ describe "#create_deploy" do
395
+ it "returns a promise" do
396
+ stub_request(:post, "https://airbrake.io/api/v4/projects/1/deploys?key=abc")
397
+ .to_return(status: 201, body: '')
398
+ expect(subject.create_deploy({})).to be_an(Airbrake::Promise)
596
399
  end
597
400
 
598
- context "when a host contains paths" do
599
- let(:deploy_host) { "https://example.net/errbit/" }
600
-
601
- let(:deploy_endpoint) do
602
- "#{deploy_host}api/v4/projects/#{project_id}/deploys?key=#{project_key}"
401
+ context "when environment is configured" do
402
+ it "prefers the passed environment to the config env" do
403
+ expect_any_instance_of(Airbrake::SyncSender).to receive(:send).with(
404
+ { environment: 'barenv' },
405
+ instance_of(Airbrake::Promise),
406
+ URI('https://airbrake.io/api/v4/projects/1/deploys?key=abc')
407
+ )
408
+ described_class.new(
409
+ user_params.merge(environment: 'fooenv')
410
+ ).create_deploy(environment: 'barenv')
603
411
  end
412
+ end
604
413
 
605
- it "sends a request to the deploy API" do
606
- airbrake = described_class.new(airbrake_params.merge(host: deploy_host))
607
- airbrake.create_deploy({})
608
- expect(a_request(:post, deploy_endpoint)).to have_been_made.once
414
+ context "when environment is not configured" do
415
+ it "sets the environment from the config" do
416
+ expect_any_instance_of(Airbrake::SyncSender).to receive(:send).with(
417
+ { environment: 'fooenv' },
418
+ instance_of(Airbrake::Promise),
419
+ URI('https://airbrake.io/api/v4/projects/1/deploys?key=abc')
420
+ )
421
+ subject.create_deploy(environment: 'fooenv')
609
422
  end
610
423
  end
611
424
  end
612
425
 
613
426
  describe "#configured?" do
614
- subject { described_class.new(airbrake_params) }
615
427
  it { is_expected.to be_configured }
616
428
  end
617
429
 
618
430
  describe "#merge_context" do
619
431
  it "merges the provided context with the notice object" do
620
- @airbrake.merge_context(apples: 'oranges')
621
- @airbrake.notify_sync('oops')
622
- expect(
623
- a_request(:post, endpoint).
624
- with(body: /"params":{"airbrake_context":{"apples":"oranges"}/)
625
- ).to have_been_made.once
432
+ expect_any_instance_of(Hash).to receive(:merge!).with(apples: 'oranges')
433
+ subject.merge_context(apples: 'oranges')
626
434
  end
627
435
  end
628
436
  end
437
+ # rubocop:enable Layout/DotPosition