airbrake-ruby 1.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,217 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Airbrake::Notifier do
4
+ let(:project_id) { 105138 }
5
+ let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
6
+ let(:localhost) { 'http://localhost:8080' }
7
+
8
+ let(:endpoint) do
9
+ "https://airbrake.io/api/v3/projects/#{project_id}/notices?key=#{project_key}"
10
+ end
11
+
12
+ let(:airbrake_params) do
13
+ { project_id: project_id,
14
+ project_key: project_key,
15
+ logger: Logger.new(StringIO.new) }
16
+ end
17
+
18
+ let(:ex) { AirbrakeTestError.new }
19
+
20
+ before do
21
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
22
+ @airbrake = described_class.new(airbrake_params)
23
+ end
24
+
25
+ describe "options" do
26
+ describe ":host" do
27
+ context "when custom" do
28
+ shared_examples 'endpoint' do |host, endpoint, title|
29
+ example(title) do
30
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
31
+ @airbrake = described_class.new(airbrake_params.merge(host: host))
32
+ @airbrake.notify_sync(ex)
33
+
34
+ expect(a_request(:post, endpoint)).to have_been_made.once
35
+ end
36
+ end
37
+
38
+ path = '/api/v3/projects/105138/notices?key=fd04e13d806a90f96614ad8e529b2822'
39
+
40
+ context "given a full host" do
41
+ include_examples('endpoint', localhost = 'http://localhost:8080',
42
+ URI.join(localhost, path),
43
+ "sends notices to the specified host's endpoint")
44
+ end
45
+
46
+ context "given a full host" do
47
+ include_examples('endpoint', localhost = 'http://localhost',
48
+ URI.join(localhost, path),
49
+ "assumes port 80 by default")
50
+ end
51
+
52
+ context "given a host without scheme" do
53
+ include_examples 'endpoint', localhost = 'localhost:8080',
54
+ URI.join("https://#{localhost}", path),
55
+ "assumes https by default"
56
+ end
57
+
58
+ context "given only hostname" do
59
+ include_examples 'endpoint', localhost = 'localhost',
60
+ URI.join("https://#{localhost}", path),
61
+ "assumes https and port 80 by default"
62
+ end
63
+ end
64
+ end
65
+
66
+ describe ":root_directory" do
67
+ it "filters out frames" do
68
+ params = airbrake_params.merge(root_directory: '/home/kyrylo/code')
69
+ airbrake = described_class.new(params)
70
+ airbrake.notify_sync(ex)
71
+
72
+ expect(
73
+ a_request(:post, endpoint).
74
+ with(body: %r|{"file":"\[PROJECT_ROOT\]/airbrake/ruby/spec/airbrake_spec.+|)
75
+ ).to have_been_made.once
76
+ end
77
+
78
+ context "when present and is a" do
79
+ shared_examples 'root directory' do |dir|
80
+ it "being included into the notice's payload" do
81
+ params = airbrake_params.merge(root_directory: dir)
82
+ airbrake = described_class.new(params)
83
+ airbrake.notify_sync(ex)
84
+
85
+ expect(
86
+ a_request(:post, endpoint).
87
+ with(body: %r{"rootDirectory":"/bingo/bango"})
88
+ ).to have_been_made.once
89
+ end
90
+ end
91
+
92
+ context "String" do
93
+ include_examples 'root directory', '/bingo/bango'
94
+ end
95
+
96
+ context "Pathname" do
97
+ include_examples 'root directory', Pathname.new('/bingo/bango')
98
+ end
99
+ end
100
+ end
101
+
102
+ describe ":proxy" do
103
+ let(:proxy) do
104
+ WEBrick::HTTPServer.new(
105
+ Port: 0,
106
+ Logger: WEBrick::Log.new('/dev/null'),
107
+ AccessLog: []
108
+ )
109
+ end
110
+
111
+ let(:requests) { Queue.new }
112
+
113
+ let(:proxy_params) do
114
+ { host: 'localhost',
115
+ port: proxy.config[:Port],
116
+ user: 'user',
117
+ password: 'password' }
118
+ end
119
+
120
+ before do
121
+ proxy.mount_proc '/' do |req, res|
122
+ requests << req
123
+ res.status = 201
124
+ res.body = "OK\n"
125
+ end
126
+
127
+ Thread.new { proxy.start }
128
+
129
+ params = airbrake_params.merge(
130
+ proxy: proxy_params,
131
+ host: "http://localhost:#{proxy.config[:Port]}"
132
+ )
133
+
134
+ @airbrake = described_class.new(params)
135
+ end
136
+
137
+ after { proxy.stop }
138
+
139
+ it "is being used if configured" do
140
+ @airbrake.notify_sync(ex)
141
+
142
+ proxied_request = requests.pop
143
+
144
+ expect(proxied_request.header['proxy-authorization'].first).
145
+ to eq('Basic dXNlcjpwYXNzd29yZA==')
146
+
147
+ # rubocop:disable Metrics/LineLength
148
+ expect(proxied_request.request_line).
149
+ to eq("POST http://localhost:#{proxy.config[:Port]}/api/v3/projects/105138/notices?key=fd04e13d806a90f96614ad8e529b2822 HTTP/1.1\r\n")
150
+ # rubocop:enable Metrics/LineLength
151
+ end
152
+ end
153
+
154
+ describe ":environment" do
155
+ context "when present" do
156
+ it "being included into the notice's payload" do
157
+ params = airbrake_params.merge(environment: :production)
158
+ airbrake = described_class.new(params)
159
+ airbrake.notify_sync(ex)
160
+
161
+ expect(
162
+ a_request(:post, endpoint).
163
+ with(body: /"context":{.*"environment":"production".*}/)
164
+ ).to have_been_made.once
165
+ end
166
+ end
167
+ end
168
+
169
+ describe ":ignore_environments" do
170
+ shared_examples 'sent notice' do |params|
171
+ it "sends a notice" do
172
+ airbrake = described_class.new(airbrake_params.merge(params))
173
+ airbrake.notify_sync(ex)
174
+
175
+ expect(a_request(:post, endpoint)).to have_been_made
176
+ end
177
+ end
178
+
179
+ shared_examples 'ignored notice' do |params|
180
+ it "ignores exceptions occurring in envs that were not configured" do
181
+ airbrake = described_class.new(airbrake_params.merge(params))
182
+ airbrake.notify_sync(ex)
183
+
184
+ expect(a_request(:post, endpoint)).not_to have_been_made
185
+ end
186
+ end
187
+
188
+ context "when env is set and ignore_environments doesn't mention it" do
189
+ params = {
190
+ environment: :development,
191
+ ignore_environments: [:production]
192
+ }
193
+
194
+ include_examples 'sent notice', params
195
+ end
196
+
197
+ context "when the current env and notify envs are the same" do
198
+ params = {
199
+ environment: :development,
200
+ ignore_environments: [:production, :development]
201
+ }
202
+
203
+ include_examples 'ignored notice', params
204
+ end
205
+
206
+ context "when the current env is not set and notify envs are present" do
207
+ params = { ignore_environments: [:production, :development] }
208
+
209
+ include_examples 'sent notice', params
210
+ end
211
+
212
+ context "when the current env is set and notify envs aren't" do
213
+ include_examples 'sent notice', environment: :development
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,458 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Airbrake::PayloadTruncator do
5
+ let(:max_size) { 1000 }
6
+ let(:truncated_len) { '[Truncated]'.length }
7
+ let(:max_len) { max_size + truncated_len }
8
+
9
+ before do
10
+ @truncator = described_class.new(max_size, Logger.new('/dev/null'))
11
+ end
12
+
13
+ describe ".truncate_error" do
14
+ let(:error) do
15
+ { type: 'AirbrakeTestError', message: 'App crashed!', backtrace: [] }
16
+ end
17
+
18
+ before do
19
+ @stdout = StringIO.new
20
+ end
21
+
22
+ describe "error backtrace" do
23
+ before do
24
+ backtrace = Array.new(size) do
25
+ { file: 'foo.rb', line: 23, function: '<main>' }
26
+ end
27
+
28
+ @error = error.merge(backtrace: backtrace)
29
+ described_class.new(max_size, Logger.new(@stdout)).truncate_error(@error)
30
+ end
31
+
32
+ context "when long" do
33
+ let(:size) { 2003 }
34
+
35
+ it "truncates the backtrace to the max size" do
36
+ expect(@error[:backtrace].size).to eq(1000)
37
+ end
38
+
39
+ it "logs the about the number of truncated frames" do
40
+ expect(@stdout.string).
41
+ to match(/INFO -- .+ dropped 1003 frame\(s\) from AirbrakeTestError/)
42
+ end
43
+ end
44
+
45
+ context "when short" do
46
+ let(:size) { 999 }
47
+
48
+ it "does not truncate the backtrace" do
49
+ expect(@error[:backtrace].size).to eq(size)
50
+ end
51
+
52
+ it "doesn't log anything" do
53
+ expect(@stdout.string).to be_empty
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "error message" do
59
+ before do
60
+ @error = error.merge(message: message)
61
+ described_class.new(max_size, Logger.new(@stdout)).truncate_error(@error)
62
+ end
63
+
64
+ context "when long" do
65
+ let(:message) { 'App crashed!' * 2000 }
66
+
67
+ it "truncates the message" do
68
+ expect(@error[:message].length).to eq(max_len)
69
+ end
70
+
71
+ it "logs about the truncated string" do
72
+ expect(@stdout.string).
73
+ to match(/INFO -- .+ truncated the message of AirbrakeTestError/)
74
+ end
75
+ end
76
+
77
+ context "when short" do
78
+ let(:message) { 'App crashed!' }
79
+ let(:msg_len) { message.length }
80
+
81
+ it "doesn't truncate the message" do
82
+ expect(@error[:message].length).to eq(msg_len)
83
+ end
84
+
85
+ it "doesn't log about the truncated string" do
86
+ expect(@stdout.string).to be_empty
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ describe ".truncate_object" do
93
+ describe "given a hash with short values" do
94
+ let(:params) do
95
+ { bingo: 'bango', bongo: 'bish', bash: 'bosh' }
96
+ end
97
+
98
+ it "doesn't get truncated" do
99
+ @truncator.truncate_object(params)
100
+ expect(params).to eq(bingo: 'bango', bongo: 'bish', bash: 'bosh')
101
+ end
102
+ end
103
+
104
+ describe "given a hash with a lot of elements" do
105
+ context "the elements of which are also hashes with a lot of elements" do
106
+ let(:params) do
107
+ Hash[(0...4124).each_cons(2).to_a].tap do |h|
108
+ h[0] = Hash[(0...4124).each_cons(2).to_a]
109
+ end
110
+ end
111
+
112
+ it "truncates all the hashes to the max allowed size" do
113
+ expect(params.size).to eq(4123)
114
+ expect(params[0].size).to eq(4123)
115
+
116
+ @truncator.truncate_object(params)
117
+
118
+ expect(params.size).to eq(1000)
119
+ expect(params[0].size).to eq(1000)
120
+ end
121
+ end
122
+ end
123
+
124
+ describe "given a set with a lot of elements" do
125
+ context "the elements of which are also sets with a lot of elements" do
126
+ let(:params) do
127
+ row = (0...4124).each_cons(2)
128
+ set = Set.new(row.to_a.unshift(row.to_a))
129
+ { bingo: set }
130
+ end
131
+
132
+ it "truncates all the sets to the max allowed size" do
133
+ expect(params[:bingo].size).to eq(4124)
134
+ expect(params[:bingo].to_a[0].size).to eq(4123)
135
+
136
+ @truncator.truncate_object(params)
137
+
138
+ expect(params[:bingo].size).to eq(1000)
139
+ expect(params[:bingo].to_a[0].size).to eq(1000)
140
+ end
141
+ end
142
+
143
+ context "including recursive sets" do
144
+ let(:params) do
145
+ a = Set.new
146
+ a << a << :bango
147
+ { bingo: a }
148
+ end
149
+
150
+ it "prevents recursion" do
151
+ @truncator.truncate_object(params)
152
+
153
+ expect(params).to eq(bingo: Set.new(['[Circular]', :bango]))
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "given an array with a lot of elements" do
159
+ context "the elements of which are also arrays with a lot of elements" do
160
+ let(:params) do
161
+ row = (0...4124).each_cons(2)
162
+ { bingo: row.to_a.unshift(row.to_a) }
163
+ end
164
+
165
+ it "truncates all the arrays to the max allowed size" do
166
+ expect(params[:bingo].size).to eq(4124)
167
+ expect(params[:bingo][0].size).to eq(4123)
168
+
169
+ @truncator.truncate_object(params)
170
+
171
+ expect(params[:bingo].size).to eq(1000)
172
+ expect(params[:bingo][0].size).to eq(1000)
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "given a hash with long values" do
178
+ context "which are strings" do
179
+ let(:params) do
180
+ { bingo: 'bango' * 2000, bongo: 'bish', bash: 'bosh' * 1000 }
181
+ end
182
+
183
+ it "truncates only long strings" do
184
+ expect(params[:bingo].length).to eq(10_000)
185
+ expect(params[:bongo].length).to eq(4)
186
+ expect(params[:bash].length).to eq(4000)
187
+
188
+ @truncator.truncate_object(params)
189
+
190
+ expect(params[:bingo].length).to eq(max_len)
191
+ expect(params[:bongo].length).to eq(4)
192
+ expect(params[:bash].length).to eq(max_len)
193
+ end
194
+ end
195
+
196
+ context "which are arrays" do
197
+ context "of long strings" do
198
+ let(:params) do
199
+ { bingo: ['foo', 'bango' * 2000, 'bar', 'piyo' * 2000, 'baz'],
200
+ bongo: 'bish',
201
+ bash: 'bosh' * 1000 }
202
+ end
203
+
204
+ it "truncates long strings in the array, but not short ones" do
205
+ expect(params[:bingo].map(&:length)).to eq([3, 10_000, 3, 8_000, 3])
206
+ expect(params[:bongo].length).to eq(4)
207
+ expect(params[:bash].length).to eq(4000)
208
+
209
+ @truncator.truncate_object(params)
210
+
211
+ expect(params[:bingo].map(&:length)).to eq([3, max_len, 3, max_len, 3])
212
+ expect(params[:bongo].length).to eq(4)
213
+ expect(params[:bash].length).to eq(max_len)
214
+ end
215
+ end
216
+
217
+ context "of short strings" do
218
+ let(:params) do
219
+ { bingo: %w(foo bar baz), bango: 'bongo', bish: 'bash' }
220
+ end
221
+
222
+ it "truncates long strings in the array, but not short ones" do
223
+ @truncator.truncate_object(params)
224
+ expect(params).
225
+ to eq(bingo: %w(foo bar baz), bango: 'bongo', bish: 'bash')
226
+ end
227
+ end
228
+
229
+ context "of hashes" do
230
+ context "with long strings" do
231
+ let(:params) do
232
+ { bingo: [{}, { bango: 'bongo', hoge: { fuga: 'piyo' * 2000 } }],
233
+ bish: 'bash',
234
+ bosh: 'foo' }
235
+ end
236
+
237
+ it "truncates the long string" do
238
+ expect(params[:bingo][1][:hoge][:fuga].length).to eq(8000)
239
+
240
+ @truncator.truncate_object(params)
241
+
242
+ expect(params[:bingo][0]).to eq({})
243
+ expect(params[:bingo][1][:bango]).to eq('bongo')
244
+ expect(params[:bingo][1][:hoge][:fuga].length).to eq(max_len)
245
+ expect(params[:bish]).to eq('bash')
246
+ expect(params[:bosh]).to eq('foo')
247
+ end
248
+ end
249
+
250
+ context "with short strings" do
251
+ let(:params) do
252
+ { bingo: [{}, { bango: 'bongo', hoge: { fuga: 'piyo' } }],
253
+ bish: 'bash',
254
+ bosh: 'foo' }
255
+ end
256
+
257
+ it "doesn't truncate the short string" do
258
+ expect(params[:bingo][1][:hoge][:fuga].length).to eq(4)
259
+
260
+ @truncator.truncate_object(params)
261
+
262
+ expect(params[:bingo][0]).to eq({})
263
+ expect(params[:bingo][1][:bango]).to eq('bongo')
264
+ expect(params[:bingo][1][:hoge][:fuga].length).to eq(4)
265
+ expect(params[:bish]).to eq('bash')
266
+ expect(params[:bosh]).to eq('foo')
267
+ end
268
+ end
269
+
270
+ context "with strings that equal to max_size" do
271
+ before do
272
+ @truncator = described_class.new(max_size, Logger.new('/dev/null'))
273
+ end
274
+
275
+ let(:params) { { unicode: '1111' } }
276
+ let(:max_size) { params[:unicode].size }
277
+
278
+ it "is doesn't truncate the string" do
279
+ @truncator.truncate_object(params)
280
+
281
+ expect(params[:unicode].length).to eq(max_size)
282
+ expect(params[:unicode]).to match(/\A1{#{max_size}}\z/)
283
+ end
284
+ end
285
+ end
286
+
287
+ context "of recursive hashes" do
288
+ let(:params) do
289
+ a = { bingo: {} }
290
+ a[:bingo][:bango] = a
291
+ end
292
+
293
+ it "prevents recursion" do
294
+ @truncator.truncate_object(params)
295
+
296
+ expect(params).to eq(bingo: { bango: '[Circular]' })
297
+ end
298
+ end
299
+
300
+ context "of arrays" do
301
+ context "with long strings" do
302
+ let(:params) do
303
+ { bingo: ['bango', ['bongo', ['bish' * 2000]]],
304
+ bish: 'bash',
305
+ bosh: 'foo' }
306
+ end
307
+
308
+ it "truncates only the long string" do
309
+ expect(params[:bingo][1][1][0].length).to eq(8000)
310
+
311
+ @truncator.truncate_object(params)
312
+
313
+ expect(params[:bingo][1][1][0].length).to eq(max_len)
314
+ end
315
+ end
316
+ end
317
+
318
+ context "of recursive arrays" do
319
+ let(:params) do
320
+ a = []
321
+ a << a << :bango
322
+ { bingo: a }
323
+ end
324
+
325
+ it "prevents recursion" do
326
+ @truncator.truncate_object(params)
327
+
328
+ expect(params).to eq(bingo: ['[Circular]', :bango])
329
+ end
330
+ end
331
+ end
332
+
333
+ context "which are arbitrary objects" do
334
+ context "with default #to_s" do
335
+ let(:params) { { bingo: Object.new } }
336
+
337
+ it "converts the object to a safe string" do
338
+ @truncator.truncate_object(params)
339
+
340
+ expect(params[:bingo]).to include('Object')
341
+ end
342
+ end
343
+
344
+ context "with redefined #to_s" do
345
+ let(:params) do
346
+ obj = Object.new
347
+
348
+ def obj.to_s
349
+ 'bango' * 2000
350
+ end
351
+
352
+ { bingo: obj }
353
+ end
354
+
355
+ it "truncates the string if it's too long" do
356
+ @truncator.truncate_object(params)
357
+
358
+ expect(params[:bingo].length).to eq(max_len)
359
+ end
360
+ end
361
+
362
+ context "with other owner than Kernel" do
363
+ let(:params) do
364
+ mod = Module.new do
365
+ def to_s
366
+ "I am a fancy object" * 2000
367
+ end
368
+ end
369
+
370
+ klass = Class.new { include mod }
371
+
372
+ { bingo: klass.new }
373
+ end
374
+
375
+ it "truncates the string it if it's long" do
376
+ @truncator.truncate_object(params)
377
+
378
+ expect(params[:bingo].length).to eq(max_len)
379
+ end
380
+ end
381
+ end
382
+
383
+ context "multiple copies of the same object" do
384
+ let(:params) do
385
+ bingo = []
386
+ bango = ['bongo']
387
+ bingo << bango << bango
388
+ { bish: bingo }
389
+ end
390
+
391
+ it "are not being truncated" do
392
+ @truncator.truncate_object(params)
393
+
394
+ expect(params).to eq(bish: [['bongo'], ['bongo']])
395
+ end
396
+ end
397
+ end
398
+
399
+ describe "unicode payload" do
400
+ before do
401
+ @truncator = described_class.new(max_size - 1, Logger.new('/dev/null'))
402
+ end
403
+
404
+ describe "truncation" do
405
+ let(:params) { { unicode: "€€€€" } }
406
+ let(:max_size) { params[:unicode].length }
407
+
408
+ it "is performed correctly" do
409
+ @truncator.truncate_object(params)
410
+
411
+ expect(params[:unicode].length).to eq(max_len - 1)
412
+
413
+ if RUBY_VERSION == '1.9.2'
414
+ expect(params[:unicode]).to match(/\A?{#{max_size - 1}}\[Truncated\]\z/)
415
+ else
416
+ expect(params[:unicode]).to match(/\A€{#{max_size - 1}}\[Truncated\]\z/)
417
+ end
418
+ end
419
+ end
420
+
421
+ describe "string encoding conversion" do
422
+ let(:params) { { unicode: "bad string€\xAE" } }
423
+ let(:max_size) { 100 }
424
+
425
+ it "converts strings to valid UTF-8" do
426
+ @truncator.truncate_object(params)
427
+
428
+ if RUBY_VERSION == '1.9.2'
429
+ expect(params[:unicode]).to eq('bad string??')
430
+ else
431
+ expect(params[:unicode]).to match(/\Abad string€[�\?]\z/)
432
+ end
433
+
434
+ expect { params.to_json }.not_to raise_error
435
+ end
436
+
437
+ it "converts ASCII-8BIT strings with invalid characters to UTF-8 correctly" do
438
+ # Shenanigans to get a bad ASCII-8BIT string. Direct conversion raises error.
439
+ encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
440
+ bad_string = Base64.decode64(encoded)
441
+
442
+ params = { unicode: bad_string }
443
+
444
+ @truncator.truncate_object(params)
445
+
446
+ expect(params[:unicode]).to match(/[�\?]{4}/)
447
+ end
448
+ end
449
+ end
450
+
451
+ describe "given a non-recursible object" do
452
+ it "raises error" do
453
+ expect { @truncator.truncate_object(:bingo) }.
454
+ to raise_error(Airbrake::Error, /cannot truncate object/)
455
+ end
456
+ end
457
+ end
458
+ end