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