airbrake-ruby 1.0.0.rc.1
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.
- checksums.yaml +7 -0
- data/lib/airbrake-ruby.rb +292 -0
- data/lib/airbrake-ruby/async_sender.rb +90 -0
- data/lib/airbrake-ruby/backtrace.rb +75 -0
- data/lib/airbrake-ruby/config.rb +120 -0
- data/lib/airbrake-ruby/filter_chain.rb +86 -0
- data/lib/airbrake-ruby/filters.rb +10 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +37 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +65 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +37 -0
- data/lib/airbrake-ruby/notice.rb +207 -0
- data/lib/airbrake-ruby/notifier.rb +145 -0
- data/lib/airbrake-ruby/payload_truncator.rb +141 -0
- data/lib/airbrake-ruby/response.rb +53 -0
- data/lib/airbrake-ruby/sync_sender.rb +76 -0
- data/lib/airbrake-ruby/version.rb +7 -0
- data/spec/airbrake_spec.rb +177 -0
- data/spec/async_sender_spec.rb +121 -0
- data/spec/backtrace_spec.rb +77 -0
- data/spec/config_spec.rb +67 -0
- data/spec/filter_chain_spec.rb +157 -0
- data/spec/notice_spec.rb +190 -0
- data/spec/notifier_spec.rb +690 -0
- data/spec/notifier_spec/options_spec.rb +217 -0
- data/spec/payload_truncator_spec.rb +458 -0
- data/spec/spec_helper.rb +98 -0
- metadata +158 -0
@@ -0,0 +1,690 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe Airbrake::Notifier do
|
5
|
+
def expect_a_request_with_body(body)
|
6
|
+
expect(a_request(:post, endpoint).with(body: body)).to have_been_made.once
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:project_id) { 105138 }
|
10
|
+
let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
|
11
|
+
let(:localhost) { 'http://localhost:8080' }
|
12
|
+
|
13
|
+
let(:endpoint) do
|
14
|
+
"https://airbrake.io/api/v3/projects/#{project_id}/notices?key=#{project_key}"
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:airbrake_params) do
|
18
|
+
{ project_id: project_id,
|
19
|
+
project_key: project_key,
|
20
|
+
logger: Logger.new(StringIO.new) }
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:ex) { AirbrakeTestError.new }
|
24
|
+
|
25
|
+
before do
|
26
|
+
stub_request(:post, endpoint).to_return(status: 201, body: '{}')
|
27
|
+
@airbrake = described_class.new(airbrake_params)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#new" do
|
31
|
+
context "raises error if" do
|
32
|
+
example ":project_id is not provided" do
|
33
|
+
expect { described_class.new(project_id: project_id) }.
|
34
|
+
to raise_error(Airbrake::Error,
|
35
|
+
'both :project_id and :project_key are required')
|
36
|
+
end
|
37
|
+
|
38
|
+
example ":project_key is not provided" do
|
39
|
+
expect { described_class.new(project_key: project_key) }.
|
40
|
+
to raise_error(Airbrake::Error,
|
41
|
+
'both :project_id and :project_key are required')
|
42
|
+
end
|
43
|
+
|
44
|
+
example "neither :project_id nor :project_key are provided" do
|
45
|
+
expect { described_class.new({}) }.
|
46
|
+
to raise_error(Airbrake::Error,
|
47
|
+
'both :project_id and :project_key are required')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when the argument is Airbrake::Config" do
|
52
|
+
it "uses it instead of the hash" do
|
53
|
+
airbrake = described_class.new(
|
54
|
+
Airbrake::Config.new(project_id: 123, project_key: '321')
|
55
|
+
)
|
56
|
+
config = airbrake.instance_variable_get(:@config)
|
57
|
+
expect(config.project_id).to eq(123)
|
58
|
+
expect(config.project_key).to eq('321')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#notify_sync" do
|
64
|
+
describe "first argument" do
|
65
|
+
context "when it is a Notice" do
|
66
|
+
it "sends the argument" do
|
67
|
+
notice = @airbrake.build_notice(ex)
|
68
|
+
@airbrake.notify_sync(notice)
|
69
|
+
|
70
|
+
# rubocop:disable Metrics/LineLength
|
71
|
+
expected_body = %r|
|
72
|
+
{"errors":\[{"type":"AirbrakeTestError","message":"App\scrashed!","backtrace":\[
|
73
|
+
{"file":"[\w/-]+/spec/spec_helper.rb","line":\d+,"function":"<top\s\(required\)>"},
|
74
|
+
{"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"},
|
75
|
+
{"file":"[\w/\-\.]+/rubygems/core_ext/kernel_require\.rb","line":\d+,"function":"require"}
|
76
|
+
|x
|
77
|
+
# rubocop:enable Metrics/LineLength
|
78
|
+
|
79
|
+
expect(
|
80
|
+
a_request(:post, endpoint).
|
81
|
+
with(body: expected_body)
|
82
|
+
).to have_been_made.once
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "request" do
|
88
|
+
before do
|
89
|
+
@airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish')
|
90
|
+
end
|
91
|
+
|
92
|
+
it "is being made over HTTPS" do
|
93
|
+
expect(
|
94
|
+
a_request(:post, endpoint).
|
95
|
+
with { |req| req.uri.port == 443 }
|
96
|
+
).to have_been_made.once
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "headers" do
|
100
|
+
def expect_a_request_with_headers(headers)
|
101
|
+
expect(
|
102
|
+
a_request(:post, endpoint).
|
103
|
+
with(headers: headers)
|
104
|
+
).to have_been_made.once
|
105
|
+
end
|
106
|
+
|
107
|
+
it "POSTs JSON to Airbrake" do
|
108
|
+
expect_a_request_with_headers('Content-Type' => 'application/json')
|
109
|
+
end
|
110
|
+
|
111
|
+
it "sets User-Agent" do
|
112
|
+
ua = "airbrake-ruby/#{Airbrake::AIRBRAKE_RUBY_VERSION} Ruby/#{RUBY_VERSION}"
|
113
|
+
expect_a_request_with_headers('User-Agent' => ua)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "body" do
|
118
|
+
it "features 'notifier'" do
|
119
|
+
expect_a_request_with_body(/"notifier":{"name":"airbrake-ruby"/)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "features 'context'" do
|
123
|
+
expect_a_request_with_body(/"context":{.*"os":"[\w-]+"/)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "features 'errors'" do
|
127
|
+
expect_a_request_with_body(
|
128
|
+
/"errors":\[{"type":"AirbrakeTestError","message":"App crash/
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "features 'backtrace'" do
|
133
|
+
expect_a_request_with_body(
|
134
|
+
%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "features 'params'" do
|
139
|
+
expect_a_request_with_body(
|
140
|
+
/"params":{"bingo":\["bango"\],"bongo":"bish"}/
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "response body when it is" do
|
147
|
+
before do
|
148
|
+
@stdout = StringIO.new
|
149
|
+
params = {
|
150
|
+
logger: Logger.new(@stdout).tap { |l| l.level = Logger::DEBUG }
|
151
|
+
}
|
152
|
+
@airbrake = described_class.new(airbrake_params.merge(params))
|
153
|
+
end
|
154
|
+
|
155
|
+
shared_examples "HTTP codes" do |code, body, expected_output|
|
156
|
+
it "logs error #{code}" do
|
157
|
+
stub_request(:post, endpoint).to_return(status: code, body: body)
|
158
|
+
|
159
|
+
expect(@stdout.string).to be_empty
|
160
|
+
|
161
|
+
response = @airbrake.notify_sync(ex)
|
162
|
+
|
163
|
+
expect(@stdout.string).to match(expected_output)
|
164
|
+
expect(response).to be_a Hash
|
165
|
+
|
166
|
+
if response['error']
|
167
|
+
expect(response['error']).to satisfy do |error|
|
168
|
+
error.is_a?(Exception) || error.is_a?(String)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context "a hash with response and invalid status" do
|
175
|
+
include_examples 'HTTP codes', 200,
|
176
|
+
'{"id":"1","url":"https://airbrake.io/locate/1"}',
|
177
|
+
%r{unexpected code \(200\). Body: .+url":"https://airbrake.+}
|
178
|
+
end
|
179
|
+
|
180
|
+
context "an empty page" do
|
181
|
+
include_examples 'HTTP codes', 200, '',
|
182
|
+
/ERROR -- : .+ unexpected code \(200\). Body: \[EMPTY_BODY\]/
|
183
|
+
end
|
184
|
+
|
185
|
+
context "a valid body with code 201" do
|
186
|
+
include_examples 'HTTP codes', 201,
|
187
|
+
'{"id":"1","url":"https://airbrake.io/locate/1"}',
|
188
|
+
%r|DEBUG -- : .+url"=>"https://airbrake.io/locate/1"}|
|
189
|
+
end
|
190
|
+
|
191
|
+
context "a non-parseable page" do
|
192
|
+
include_examples 'HTTP codes', 400, 'bingo bango bongo',
|
193
|
+
/ERROR -- : .+unexpected token at 'bingo.+'\)\. Body: bingo.+/
|
194
|
+
end
|
195
|
+
|
196
|
+
context "error 400" do
|
197
|
+
include_examples 'HTTP codes', 400, '{"error": "Invalid Content-Type header."}',
|
198
|
+
/ERROR -- : .+ Invalid Content-Type header\./
|
199
|
+
end
|
200
|
+
|
201
|
+
context "error 401" do
|
202
|
+
include_examples 'HTTP codes', 401,
|
203
|
+
'{"error":"Project not found or access denied."}',
|
204
|
+
/ERROR -- : .+ Project not found or access denied./
|
205
|
+
end
|
206
|
+
|
207
|
+
context "the rate-limit message" do
|
208
|
+
include_examples 'HTTP codes', 429, '{"error": "Project is rate limited."}',
|
209
|
+
/ERROR -- : .+ Project is rate limited.+/
|
210
|
+
end
|
211
|
+
|
212
|
+
context "the internal server error" do
|
213
|
+
include_examples 'HTTP codes', 500, 'Internal Server Error',
|
214
|
+
/ERROR -- : .+ unexpected code \(500\). Body: Internal.+ Error/
|
215
|
+
end
|
216
|
+
|
217
|
+
context "too long it truncates it and" do
|
218
|
+
include_examples 'HTTP codes', 123, '123 ' * 1000,
|
219
|
+
/ERROR -- : .+ unexpected code \(123\). Body: .+ 123 123 1\.\.\./
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "connection timeout" do
|
224
|
+
it "logs the error when it occurs" do
|
225
|
+
stub_request(:post, endpoint).to_timeout
|
226
|
+
|
227
|
+
stderr = StringIO.new
|
228
|
+
params = airbrake_params.merge(logger: Logger.new(stderr))
|
229
|
+
airbrake = described_class.new(params)
|
230
|
+
|
231
|
+
airbrake.notify_sync(ex)
|
232
|
+
|
233
|
+
expect(stderr.string).
|
234
|
+
to match(/ERROR -- : .+ HTTP error: execution expired/)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "unicode payload" do
|
239
|
+
context "with valid strings" do
|
240
|
+
it "works correctly" do
|
241
|
+
@airbrake.notify_sync(ex, unicode: "ü ö ä Ä Ü Ö ß привет €25.00 한글")
|
242
|
+
|
243
|
+
expect(
|
244
|
+
a_request(:post, endpoint).
|
245
|
+
with(body: /"unicode":"ü ö ä Ä Ü Ö ß привет €25.00 한글"/)
|
246
|
+
).to have_been_made.once
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "with invalid strings" do
|
251
|
+
it "doesn't raise error when string has invalid encoding" do
|
252
|
+
expect do
|
253
|
+
@airbrake.notify_sync('bingo', bongo: "bango\xAE")
|
254
|
+
end.not_to raise_error
|
255
|
+
end
|
256
|
+
|
257
|
+
it "doesn't raise error when string has valid encoding, but invalid characters" do
|
258
|
+
# Shenanigans to get a bad ASCII-8BIT string. Direct conversion raises error.
|
259
|
+
encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
|
260
|
+
bad_string = Base64.decode64(encoded)
|
261
|
+
|
262
|
+
expect do
|
263
|
+
@airbrake.notify_sync('bingo', bongo: bad_string)
|
264
|
+
end.not_to raise_error
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "a closed IO object" do
|
270
|
+
context "outside of the Rails environment" do
|
271
|
+
it "is not getting truncated" do
|
272
|
+
@airbrake.notify_sync(ex, bingo: IO.new(0).tap(&:close))
|
273
|
+
|
274
|
+
expect(
|
275
|
+
a_request(:post, endpoint).with(body: /"bingo":"#<IO:0x.+>"/)
|
276
|
+
).to have_been_made.once
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context "inside the Rails environment" do
|
281
|
+
##
|
282
|
+
# Instances of this class contain a closed IO object assigned to an instance
|
283
|
+
# variable. Normally, the JSON gem, which we depend on can parse closed IO
|
284
|
+
# objects. However, because ActiveSupport monkey-patches #to_json and calls
|
285
|
+
# #to_a on them, they raise IOError when we try to serialize them.
|
286
|
+
#
|
287
|
+
# @see https://goo.gl/0A3xNC
|
288
|
+
class ObjectWithIoIvars
|
289
|
+
def initialize
|
290
|
+
@bongo = Tempfile.new('bongo').tap(&:close)
|
291
|
+
end
|
292
|
+
|
293
|
+
# @raise [NotImplementedError] when inside a Rails environment
|
294
|
+
def to_json(*)
|
295
|
+
raise NotImplementedError
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# @see ObjectWithIoIvars
|
301
|
+
class ObjectWithNestedIoIvars
|
302
|
+
def initialize
|
303
|
+
@bish = ObjectWithIoIvars.new
|
304
|
+
end
|
305
|
+
|
306
|
+
# @see ObjectWithIoIvars#to_json
|
307
|
+
def to_json(*)
|
308
|
+
raise NotImplementedError
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
shared_examples 'truncation' do |params, expected|
|
313
|
+
it "filters it out" do
|
314
|
+
@airbrake.notify_sync(ex, params)
|
315
|
+
|
316
|
+
expect(
|
317
|
+
a_request(:post, endpoint).with(body: expected)
|
318
|
+
).to have_been_made.once
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context "which is an instance of" do
|
323
|
+
context "Tempfile" do
|
324
|
+
params = { bango: Tempfile.new('bongo').tap(&:close) }
|
325
|
+
include_examples 'truncation', params, /"bango":"#<(Temp)?file:0x.+>"/i
|
326
|
+
end
|
327
|
+
|
328
|
+
context "a non-IO class but with" do
|
329
|
+
context "IO ivars" do
|
330
|
+
params = { bongo: ObjectWithIoIvars.new }
|
331
|
+
include_examples 'truncation', params, /"bongo":".+ObjectWithIoIvars.+"/
|
332
|
+
end
|
333
|
+
|
334
|
+
context "a non-IO ivar, which contains an IO ivar itself" do
|
335
|
+
params = { bish: ObjectWithNestedIoIvars.new }
|
336
|
+
include_examples 'truncation', params, /"bish":".+ObjectWithNested.+"/
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
context "which is deeply nested inside a hash" do
|
342
|
+
params = { bingo: { bango: { bongo: ObjectWithIoIvars.new } } }
|
343
|
+
include_examples(
|
344
|
+
'truncation',
|
345
|
+
params,
|
346
|
+
/"params":{"bingo":{"bango":{"bongo":".+ObjectWithIoIvars.+"}}}/
|
347
|
+
)
|
348
|
+
end
|
349
|
+
|
350
|
+
context "which is deeply nested inside an array" do
|
351
|
+
params = { bingo: [[ObjectWithIoIvars.new]] }
|
352
|
+
include_examples(
|
353
|
+
'truncation',
|
354
|
+
params,
|
355
|
+
/"params":{"bingo":\[\[".+ObjectWithIoIvars.+"\]\]}/
|
356
|
+
)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
describe "#notify" do
|
363
|
+
it "sends an exception asynchronously" do
|
364
|
+
@airbrake.notify(ex, bingo: 'bango')
|
365
|
+
|
366
|
+
sleep 1
|
367
|
+
|
368
|
+
expect_a_request_with_body(/params":{"bingo":"bango"}/)
|
369
|
+
end
|
370
|
+
|
371
|
+
it "returns nil" do
|
372
|
+
expect(@airbrake.notify(ex)).to be_nil
|
373
|
+
sleep 1
|
374
|
+
end
|
375
|
+
|
376
|
+
it "falls back to synchronous delivery when the async sender is dead" do
|
377
|
+
out = StringIO.new
|
378
|
+
|
379
|
+
airbrake = described_class.new(airbrake_params.merge(logger: Logger.new(out)))
|
380
|
+
airbrake.
|
381
|
+
instance_variable_get(:@async_sender).
|
382
|
+
instance_variable_get(:@workers).
|
383
|
+
list.
|
384
|
+
each(&:kill)
|
385
|
+
|
386
|
+
sleep 1
|
387
|
+
|
388
|
+
expect(airbrake.notify('bingo')).to be_nil
|
389
|
+
expect(out.string).to match(/falling back to sync delivery/)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe "#add_filter" do
|
394
|
+
it "filters notices" do
|
395
|
+
@airbrake.add_filter do |notice|
|
396
|
+
if notice[:params][:password]
|
397
|
+
notice[:params][:password] = '[Filtered]'.freeze
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
@airbrake.notify_sync(ex, password: 's4kr4t')
|
402
|
+
|
403
|
+
expect(
|
404
|
+
a_request(:post, endpoint).
|
405
|
+
with(body: /params":{"password":"\[Filtered\]"}/)
|
406
|
+
).to have_been_made.once
|
407
|
+
end
|
408
|
+
|
409
|
+
it "accepts multiple filters" do
|
410
|
+
[:bingo, :bongo, :bash].each do |key|
|
411
|
+
@airbrake.add_filter do |notice|
|
412
|
+
notice[:params][key] = '[Filtered]'.freeze if notice[:params][key]
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
@airbrake.notify_sync(ex, bingo: ['bango'], bongo: 'bish', bash: 'bosh')
|
417
|
+
|
418
|
+
# rubocop:disable Metrics/LineLength
|
419
|
+
body = /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]","bash":"\[Filtered\]"}/
|
420
|
+
# rubocop:enable Metrics/LineLength
|
421
|
+
|
422
|
+
expect(
|
423
|
+
a_request(:post, endpoint).
|
424
|
+
with(body: body)
|
425
|
+
).to have_been_made.once
|
426
|
+
end
|
427
|
+
|
428
|
+
it "ignores all notices" do
|
429
|
+
@airbrake.add_filter(&:ignore!)
|
430
|
+
|
431
|
+
@airbrake.notify_sync(ex)
|
432
|
+
|
433
|
+
expect(a_request(:post, endpoint)).not_to have_been_made
|
434
|
+
end
|
435
|
+
|
436
|
+
it "ignores specific notices" do
|
437
|
+
@airbrake.add_filter do |notice|
|
438
|
+
notice.ignore! if notice[:errors][0][:type] == 'RuntimeError'
|
439
|
+
end
|
440
|
+
|
441
|
+
@airbrake.notify_sync(RuntimeError.new('Not caring!'))
|
442
|
+
expect(a_request(:post, endpoint)).not_to have_been_made
|
443
|
+
|
444
|
+
@airbrake.notify_sync(ex)
|
445
|
+
expect(a_request(:post, endpoint)).to have_been_made.once
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
describe "#blacklist_keys" do
|
450
|
+
describe "the list of arguments" do
|
451
|
+
it "accepts regexps" do
|
452
|
+
@airbrake.blacklist_keys(/\Abin/)
|
453
|
+
|
454
|
+
@airbrake.notify_sync(ex, bingo: 'bango')
|
455
|
+
|
456
|
+
expect(
|
457
|
+
a_request(:post, endpoint).
|
458
|
+
with(body: /"params":{"bingo":"\[Filtered\]"}/)
|
459
|
+
).to have_been_made.once
|
460
|
+
end
|
461
|
+
|
462
|
+
it "accepts symbols" do
|
463
|
+
@airbrake.blacklist_keys(:bingo)
|
464
|
+
|
465
|
+
@airbrake.notify_sync(ex, bingo: 'bango')
|
466
|
+
|
467
|
+
expect(
|
468
|
+
a_request(:post, endpoint).
|
469
|
+
with(body: /"params":{"bingo":"\[Filtered\]"}/)
|
470
|
+
).to have_been_made.once
|
471
|
+
end
|
472
|
+
|
473
|
+
it "accepts strings" do
|
474
|
+
@airbrake.blacklist_keys('bingo')
|
475
|
+
|
476
|
+
@airbrake.notify_sync(ex, bingo: 'bango')
|
477
|
+
|
478
|
+
expect(
|
479
|
+
a_request(:post, endpoint).
|
480
|
+
with(body: /"params":{"bingo":"\[Filtered\]"}/)
|
481
|
+
).to have_been_made.once
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
describe "hash values" do
|
486
|
+
context "non-recursive" do
|
487
|
+
it "filters nested hashes" do
|
488
|
+
@airbrake.blacklist_keys('bish')
|
489
|
+
|
490
|
+
@airbrake.notify_sync(ex, bongo: { bish: 'bash' })
|
491
|
+
|
492
|
+
expect(
|
493
|
+
a_request(:post, endpoint).
|
494
|
+
with(body: /"params":{"bongo":{"bish":"\[Filtered\]"}}/)
|
495
|
+
).to have_been_made.once
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
context "recursive" do
|
500
|
+
it "filters recursive hashes" do
|
501
|
+
@airbrake.blacklist_keys('bango')
|
502
|
+
|
503
|
+
bongo = { bingo: {} }
|
504
|
+
bongo[:bingo][:bango] = bongo
|
505
|
+
|
506
|
+
@airbrake.notify_sync(ex, bongo)
|
507
|
+
|
508
|
+
expect(
|
509
|
+
a_request(:post, endpoint).
|
510
|
+
with(body: /"params":{"bingo":{"bango":"\[Filtered\]"}}/)
|
511
|
+
).to have_been_made.once
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
it "filters query parameters correctly" do
|
517
|
+
@airbrake.blacklist_keys(%w(bish))
|
518
|
+
|
519
|
+
notice = @airbrake.build_notice(ex)
|
520
|
+
notice[:context][:url] = 'http://localhost:3000/crash?foo=bar&baz=bongo&bish=bash'
|
521
|
+
|
522
|
+
@airbrake.notify_sync(notice)
|
523
|
+
|
524
|
+
# rubocop:disable Metrics/LineLength
|
525
|
+
expected_body =
|
526
|
+
%r("context":{.*"url":"http://localhost:3000/crash\?foo=bar&baz=bongo&bish=\[Filtered\]".*})
|
527
|
+
# rubocop:enable Metrics/LineLength
|
528
|
+
|
529
|
+
expect(
|
530
|
+
a_request(:post, endpoint).
|
531
|
+
with(body: expected_body)
|
532
|
+
).to have_been_made.once
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
describe "#whitelist_keys" do
|
537
|
+
describe "the list of arguments" do
|
538
|
+
it "accepts regexes" do
|
539
|
+
@airbrake.whitelist_keys(/\Abin/)
|
540
|
+
|
541
|
+
@airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
|
542
|
+
|
543
|
+
body = /"params":{"bingo":"bango","bongo":"\[Filtered\]","bash":"\[Filtered\]"}/
|
544
|
+
|
545
|
+
expect(
|
546
|
+
a_request(:post, endpoint).
|
547
|
+
with(body: body)
|
548
|
+
).to have_been_made.once
|
549
|
+
end
|
550
|
+
|
551
|
+
it "accepts symbols" do
|
552
|
+
@airbrake.whitelist_keys(:bongo)
|
553
|
+
|
554
|
+
@airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
|
555
|
+
|
556
|
+
body = /"params":{"bingo":"\[Filtered\]","bongo":"bish","bash":"\[Filtered\]"}/
|
557
|
+
|
558
|
+
expect(
|
559
|
+
a_request(:post, endpoint).
|
560
|
+
with(body: body)
|
561
|
+
).to have_been_made.once
|
562
|
+
end
|
563
|
+
|
564
|
+
it "accepts strings" do
|
565
|
+
@airbrake.whitelist_keys('bash')
|
566
|
+
|
567
|
+
@airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
|
568
|
+
|
569
|
+
body = /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]","bash":"bosh"}/
|
570
|
+
|
571
|
+
expect(
|
572
|
+
a_request(:post, endpoint).
|
573
|
+
with(body: body)
|
574
|
+
).to have_been_made.once
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
describe "hash values" do
|
579
|
+
context "non-recursive" do
|
580
|
+
it "filters out everything but the provided keys" do
|
581
|
+
@airbrake.whitelist_keys(%w(bongo bish))
|
582
|
+
|
583
|
+
@airbrake.notify_sync(ex, bingo: 'bango', bongo: { bish: 'bash' })
|
584
|
+
|
585
|
+
expect(
|
586
|
+
a_request(:post, endpoint).
|
587
|
+
with(body: /"params":{"bingo":"\[Filtered\]","bongo":{"bish":"bash"}}/)
|
588
|
+
).to have_been_made.once
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
context "recursive" do
|
593
|
+
it "errors when nested hashes are not filtered" do
|
594
|
+
@airbrake.whitelist_keys(%w(bingo bango))
|
595
|
+
|
596
|
+
bongo = { bingo: {} }
|
597
|
+
bongo[:bingo][:bango] = bongo
|
598
|
+
|
599
|
+
if RUBY_ENGINE == 'jruby'
|
600
|
+
# JRuby might raise two different exceptions, which represent the
|
601
|
+
# same thing. One is a Java exception, the other is a Ruby
|
602
|
+
# exception. It's probably a JRuby bug:
|
603
|
+
# https://github.com/jruby/jruby/issues/1903
|
604
|
+
begin
|
605
|
+
expect do
|
606
|
+
@airbrake.notify_sync(ex, bongo)
|
607
|
+
end.to raise_error(SystemStackError)
|
608
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
609
|
+
expect do
|
610
|
+
@airbrake.notify_sync(ex, bongo)
|
611
|
+
end.to raise_error(java.lang.StackOverflowError)
|
612
|
+
end
|
613
|
+
else
|
614
|
+
expect do
|
615
|
+
@airbrake.notify_sync(ex, bongo)
|
616
|
+
end.to raise_error(SystemStackError)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
describe "context/url" do
|
623
|
+
it "filters query parameters correctly" do
|
624
|
+
@airbrake.whitelist_keys(%w(bish))
|
625
|
+
|
626
|
+
notice = @airbrake.build_notice(ex)
|
627
|
+
notice[:context][:url] = 'http://localhost:3000/crash?foo=bar&baz=bongo&bish=bash'
|
628
|
+
|
629
|
+
@airbrake.notify_sync(notice)
|
630
|
+
|
631
|
+
# rubocop:disable Metrics/LineLength
|
632
|
+
expected_body =
|
633
|
+
%r("context":{.*"url":"http://localhost:3000/crash\?foo=\[Filtered\]&baz=\[Filtered\]&bish=bash".*})
|
634
|
+
# rubocop:enable Metrics/LineLength
|
635
|
+
|
636
|
+
expect(
|
637
|
+
a_request(:post, endpoint).
|
638
|
+
with(body: expected_body)
|
639
|
+
).to have_been_made.once
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
describe "#build_notice" do
|
645
|
+
it "builds a notice from exception" do
|
646
|
+
expect(@airbrake.build_notice(ex)).to be_an Airbrake::Notice
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
describe "#close" do
|
651
|
+
shared_examples 'close' do |method|
|
652
|
+
it "raises error" do
|
653
|
+
@airbrake.close
|
654
|
+
expect { method.call(@airbrake) }.
|
655
|
+
to raise_error(Airbrake::Error, /closed Airbrake instance/)
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
context "when using #notify" do
|
660
|
+
include_examples 'close', proc { |a| a.notify(AirbrakeTestError.new) }
|
661
|
+
end
|
662
|
+
|
663
|
+
context "when using #send_notice" do
|
664
|
+
include_examples 'close', proc { |a|
|
665
|
+
notice = a.build_notice(AirbrakeTestError.new)
|
666
|
+
a.send_notice(notice)
|
667
|
+
}
|
668
|
+
end
|
669
|
+
|
670
|
+
context "at program exit when it was closed manually" do
|
671
|
+
it "doesn't raise error", skip: RUBY_ENGINE == 'jruby' do
|
672
|
+
expect do
|
673
|
+
Process.wait(fork { described_class.new(airbrake_params) })
|
674
|
+
end.not_to raise_error
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
describe "#create_deploy" do
|
680
|
+
let(:deploy_endpoint) do
|
681
|
+
"https://airbrake.io/api/v4/projects/#{project_id}/deploys?key=#{project_key}"
|
682
|
+
end
|
683
|
+
|
684
|
+
it "sends a request to the deploy API" do
|
685
|
+
stub_request(:post, deploy_endpoint).to_return(status: 201, body: '{"id":"123"}')
|
686
|
+
@airbrake.create_deploy({})
|
687
|
+
expect(a_request(:post, deploy_endpoint)).to have_been_made.once
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|