airbrake-ruby 2.9.0 → 2.10.0

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