airbrake-ruby 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +513 -0
  3. data/lib/airbrake-ruby/async_sender.rb +142 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +48 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +45 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +125 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +53 -0
  38. data/lib/airbrake-ruby/request.rb +45 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  45. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  46. data/lib/airbrake-ruby/truncator.rb +115 -0
  47. data/lib/airbrake-ruby/version.rb +6 -0
  48. data/spec/airbrake_spec.rb +324 -0
  49. data/spec/async_sender_spec.rb +155 -0
  50. data/spec/backtrace_spec.rb +427 -0
  51. data/spec/benchmark_spec.rb +33 -0
  52. data/spec/code_hunk_spec.rb +115 -0
  53. data/spec/config/validator_spec.rb +184 -0
  54. data/spec/config_spec.rb +154 -0
  55. data/spec/deploy_notifier_spec.rb +48 -0
  56. data/spec/file_cache.rb +36 -0
  57. data/spec/filter_chain_spec.rb +92 -0
  58. data/spec/filters/context_filter_spec.rb +23 -0
  59. data/spec/filters/dependency_filter_spec.rb +12 -0
  60. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  61. data/spec/filters/gem_root_filter_spec.rb +41 -0
  62. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  63. data/spec/filters/git_repository_filter.rb +61 -0
  64. data/spec/filters/git_revision_filter_spec.rb +126 -0
  65. data/spec/filters/keys_blacklist_spec.rb +225 -0
  66. data/spec/filters/keys_whitelist_spec.rb +194 -0
  67. data/spec/filters/root_directory_filter_spec.rb +39 -0
  68. data/spec/filters/sql_filter_spec.rb +219 -0
  69. data/spec/filters/system_exit_filter_spec.rb +14 -0
  70. data/spec/filters/thread_filter_spec.rb +277 -0
  71. data/spec/fixtures/notroot.txt +7 -0
  72. data/spec/fixtures/project_root/code.rb +221 -0
  73. data/spec/fixtures/project_root/empty_file.rb +0 -0
  74. data/spec/fixtures/project_root/long_line.txt +1 -0
  75. data/spec/fixtures/project_root/short_file.rb +3 -0
  76. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  77. data/spec/helpers.rb +9 -0
  78. data/spec/ignorable_spec.rb +14 -0
  79. data/spec/inspectable_spec.rb +45 -0
  80. data/spec/monotonic_time_spec.rb +12 -0
  81. data/spec/nested_exception_spec.rb +73 -0
  82. data/spec/notice_notifier_spec.rb +356 -0
  83. data/spec/notice_notifier_spec/options_spec.rb +259 -0
  84. data/spec/notice_spec.rb +296 -0
  85. data/spec/performance_breakdown_spec.rb +12 -0
  86. data/spec/performance_notifier_spec.rb +435 -0
  87. data/spec/promise_spec.rb +197 -0
  88. data/spec/query_spec.rb +11 -0
  89. data/spec/request_spec.rb +11 -0
  90. data/spec/response_spec.rb +88 -0
  91. data/spec/spec_helper.rb +100 -0
  92. data/spec/stashable_spec.rb +23 -0
  93. data/spec/stat_spec.rb +47 -0
  94. data/spec/sync_sender_spec.rb +133 -0
  95. data/spec/tdigest_spec.rb +230 -0
  96. data/spec/time_truncate_spec.rb +13 -0
  97. data/spec/timed_trace_spec.rb +125 -0
  98. data/spec/truncator_spec.rb +238 -0
  99. metadata +213 -0
@@ -0,0 +1,12 @@
1
+ RSpec.describe Airbrake::PerformanceBreakdown do
2
+ describe "#stash" do
3
+ subject do
4
+ described_class.new(
5
+ method: 'GET', route: '/', response_type: '', groups: {},
6
+ start_time: Time.now
7
+ )
8
+ end
9
+
10
+ it { is_expected.to respond_to(:stash) }
11
+ end
12
+ end
@@ -0,0 +1,435 @@
1
+ RSpec.describe Airbrake::PerformanceNotifier do
2
+ let(:routes) { 'https://api.airbrake.io/api/v5/projects/1/routes-stats' }
3
+ let(:queries) { 'https://api.airbrake.io/api/v5/projects/1/queries-stats' }
4
+ let(:breakdowns) { 'https://api.airbrake.io/api/v5/projects/1/routes-breakdowns' }
5
+
6
+ before do
7
+ stub_request(:put, routes).to_return(status: 200, body: '')
8
+ stub_request(:put, queries).to_return(status: 200, body: '')
9
+ stub_request(:put, breakdowns).to_return(status: 200, body: '')
10
+
11
+ Airbrake::Config.instance = Airbrake::Config.new(
12
+ project_id: 1,
13
+ project_key: 'banana',
14
+ performance_stats: true,
15
+ performance_stats_flush_period: 0,
16
+ query_stats: true
17
+ )
18
+ end
19
+
20
+ describe "#notify" do
21
+ it "sends full query" do
22
+ subject.notify(
23
+ Airbrake::Query.new(
24
+ method: 'POST',
25
+ route: '/foo',
26
+ query: 'SELECT * FROM things',
27
+ func: 'foo',
28
+ file: 'foo.rb',
29
+ line: 123,
30
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
31
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
32
+ )
33
+ )
34
+
35
+ expect(
36
+ a_request(:put, queries).with(body: %r|
37
+ \A{"queries":\[{
38
+ "method":"POST",
39
+ "route":"/foo",
40
+ "query":"SELECT\s\*\sFROM\sthings",
41
+ "time":"2018-01-01T00:49:00\+00:00",
42
+ "function":"foo",
43
+ "file":"foo.rb",
44
+ "line":123,
45
+ "count":1,
46
+ "sum":60000.0,
47
+ "sumsq":3600000000.0,
48
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"
49
+ }\]}\z|x)
50
+ ).to have_been_made
51
+ end
52
+
53
+ it "sends full request" do
54
+ subject.notify(
55
+ Airbrake::Request.new(
56
+ method: 'POST',
57
+ route: '/foo',
58
+ status_code: 200,
59
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
60
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
61
+ )
62
+ )
63
+
64
+ expect(
65
+ a_request(:put, routes).with(body: %r|
66
+ \A{"routes":\[{
67
+ "method":"POST",
68
+ "route":"/foo",
69
+ "statusCode":200,
70
+ "time":"2018-01-01T00:49:00\+00:00",
71
+ "count":1,
72
+ "sum":60000.0,
73
+ "sumsq":3600000000.0,
74
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"
75
+ }\]}\z|x)
76
+ ).to have_been_made
77
+ end
78
+
79
+ it "sends full performance breakdown" do
80
+ subject.notify(
81
+ Airbrake::PerformanceBreakdown.new(
82
+ method: 'DELETE',
83
+ route: '/routes-breakdowns',
84
+ response_type: 'json',
85
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
86
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0),
87
+ groups: { db: 131, view: 421 }
88
+ )
89
+ )
90
+
91
+ expect(
92
+ a_request(:put, breakdowns).with(body: %r|
93
+ \A{"routes":\[{
94
+ "method":"DELETE",
95
+ "route":"/routes-breakdowns",
96
+ "responseType":"json",
97
+ "time":"2018-01-01T00:49:00\+00:00",
98
+ "count":1,
99
+ "sum":60000.0,
100
+ "sumsq":3600000000.0,
101
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB",
102
+ "groups":{
103
+ "db":{
104
+ "count":1,
105
+ "sum":131.0,
106
+ "sumsq":17161.0,
107
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUMDAAAB"
108
+ },
109
+ "view":{
110
+ "count":1,
111
+ "sum":421.0,
112
+ "sumsq":177241.0,
113
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUPSgAAB"
114
+ }
115
+ }
116
+ }\]}\z|x)
117
+ ).to have_been_made
118
+ end
119
+
120
+ it "rounds time to the floor minute" do
121
+ subject.notify(
122
+ Airbrake::Request.new(
123
+ method: 'GET',
124
+ route: '/foo',
125
+ status_code: 200,
126
+ start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
127
+ )
128
+ )
129
+ expect(
130
+ a_request(:put, routes).with(body: /"time":"2018-01-01T00:00:00\+00:00"/)
131
+ ).to have_been_made
132
+ end
133
+
134
+ it "increments routes with the same key" do
135
+ subject.notify(
136
+ Airbrake::Request.new(
137
+ method: 'GET',
138
+ route: '/foo',
139
+ status_code: 200,
140
+ start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
141
+ )
142
+ )
143
+ subject.notify(
144
+ Airbrake::Request.new(
145
+ method: 'GET',
146
+ route: '/foo',
147
+ status_code: 200,
148
+ start_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
149
+ )
150
+ )
151
+ expect(
152
+ a_request(:put, routes).with(body: /"count":2/)
153
+ ).to have_been_made
154
+ end
155
+
156
+ it "groups routes by time" do
157
+ subject.notify(
158
+ Airbrake::Request.new(
159
+ method: 'GET',
160
+ route: '/foo',
161
+ status_code: 200,
162
+ start_time: Time.new(2018, 1, 1, 0, 0, 49, 0),
163
+ end_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
164
+ )
165
+ )
166
+ subject.notify(
167
+ Airbrake::Request.new(
168
+ method: 'GET',
169
+ route: '/foo',
170
+ status_code: 200,
171
+ start_time: Time.new(2018, 1, 1, 0, 1, 49, 0),
172
+ end_time: Time.new(2018, 1, 1, 0, 1, 55, 0)
173
+ )
174
+ )
175
+ expect(
176
+ a_request(:put, routes).with(
177
+ body: %r|\A
178
+ {"routes":\[
179
+ {"method":"GET","route":"/foo","statusCode":200,
180
+ "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":1000.0,
181
+ "sumsq":1000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUR6AAAB"},
182
+ {"method":"GET","route":"/foo","statusCode":200,
183
+ "time":"2018-01-01T00:01:00\+00:00","count":1,"sum":6000.0,
184
+ "sumsq":36000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUW7gAAB"}\]}
185
+ \z|x
186
+ )
187
+ ).to have_been_made
188
+ end
189
+
190
+ it "groups routes by route key" do
191
+ subject.notify(
192
+ Airbrake::Request.new(
193
+ method: 'GET',
194
+ route: '/foo',
195
+ status_code: 200,
196
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
197
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
198
+ )
199
+ )
200
+ subject.notify(
201
+ Airbrake::Request.new(
202
+ method: 'POST',
203
+ route: '/foo',
204
+ status_code: 200,
205
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
206
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
207
+ )
208
+ )
209
+ expect(
210
+ a_request(:put, routes).with(
211
+ body: %r|\A
212
+ {"routes":\[
213
+ {"method":"GET","route":"/foo","statusCode":200,
214
+ "time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
215
+ "sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"},
216
+ {"method":"POST","route":"/foo","statusCode":200,
217
+ "time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
218
+ "sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"}\]}
219
+ \z|x
220
+ )
221
+ ).to have_been_made
222
+ end
223
+
224
+ it "groups performance breakdowns by route key" do
225
+ subject.notify(
226
+ Airbrake::PerformanceBreakdown.new(
227
+ method: 'DELETE',
228
+ route: '/routes-breakdowns',
229
+ response_type: 'json',
230
+ start_time: Time.new(2018, 1, 1, 0, 0, 20, 0),
231
+ end_time: Time.new(2018, 1, 1, 0, 0, 22, 0),
232
+ groups: { db: 131, view: 421 }
233
+ )
234
+ )
235
+ subject.notify(
236
+ Airbrake::PerformanceBreakdown.new(
237
+ method: 'DELETE',
238
+ route: '/routes-breakdowns',
239
+ response_type: 'json',
240
+ start_time: Time.new(2018, 1, 1, 0, 0, 30, 0),
241
+ end_time: Time.new(2018, 1, 1, 0, 0, 32, 0),
242
+ groups: { db: 55, view: 11 }
243
+ )
244
+ )
245
+
246
+ expect(
247
+ a_request(:put, breakdowns).with(body: %r|
248
+ \A{"routes":\[{
249
+ "method":"DELETE",
250
+ "route":"/routes-breakdowns",
251
+ "responseType":"json",
252
+ "time":"2018-01-01T00:00:00\+00:00",
253
+ "count":2,
254
+ "sum":4000.0,
255
+ "sumsq":8000000.0,
256
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUT6AAAC",
257
+ "groups":{
258
+ "db":{
259
+ "count":2,
260
+ "sum":186.0,
261
+ "sumsq":20186.0,
262
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAkJcAABCmAAAAQE="
263
+ },
264
+ "view":{
265
+ "count":2,
266
+ "sum":432.0,
267
+ "sumsq":177362.0,
268
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAkEwAABDzQAAAQE="
269
+ }
270
+ }
271
+ }\]}\z|x)
272
+ ).to have_been_made
273
+ end
274
+
275
+ it "returns a promise" do
276
+ promise = subject.notify(
277
+ Airbrake::Request.new(
278
+ method: 'GET',
279
+ route: '/foo',
280
+ status_code: 200,
281
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
282
+ )
283
+ )
284
+ expect(promise).to be_an(Airbrake::Promise)
285
+ expect(promise.value).to eq('' => nil)
286
+ end
287
+
288
+ it "checks performance stat configuration" do
289
+ request = Airbrake::Request.new(
290
+ method: 'GET', route: '/foo', status_code: 200, start_time: Time.new
291
+ )
292
+ expect(Airbrake::Config.instance).to receive(:check_performance_options)
293
+ .with(request).and_return(Airbrake::Promise.new)
294
+ subject.notify(request)
295
+ end
296
+
297
+ it "sends environment when it's specified" do
298
+ Airbrake::Config.instance.merge(performance_stats: true, environment: 'test')
299
+
300
+ subject.notify(
301
+ Airbrake::Request.new(
302
+ method: 'POST',
303
+ route: '/foo',
304
+ status_code: 200,
305
+ start_time: Time.new
306
+ )
307
+ )
308
+ expect(
309
+ a_request(:put, routes).with(
310
+ body: /\A{"routes":\[.+\],"environment":"test"}\z/x
311
+ )
312
+ ).to have_been_made
313
+ end
314
+
315
+ context "when config is invalid" do
316
+ before { Airbrake::Config.instance.merge(project_id: nil) }
317
+
318
+ it "returns a rejected promise" do
319
+ promise = subject.notify({})
320
+ expect(promise).to be_rejected
321
+ end
322
+ end
323
+
324
+ describe "payload grouping" do
325
+ let(:flush_period) { 0.5 }
326
+
327
+ it "groups payload by performance name and sends it separately" do
328
+ Airbrake::Config.instance.merge(
329
+ project_id: 1,
330
+ project_key: 'banana',
331
+ performance_stats: true,
332
+ performance_stats_flush_period: flush_period
333
+ )
334
+
335
+ subject.notify(
336
+ Airbrake::Request.new(
337
+ method: 'GET',
338
+ route: '/foo',
339
+ status_code: 200,
340
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
341
+ )
342
+ )
343
+
344
+ subject.notify(
345
+ Airbrake::Query.new(
346
+ method: 'POST',
347
+ route: '/foo',
348
+ query: 'SELECT * FROM things',
349
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
350
+ )
351
+ )
352
+
353
+ sleep(flush_period + 0.5)
354
+
355
+ expect(a_request(:put, routes)).to have_been_made
356
+ expect(a_request(:put, queries)).to have_been_made
357
+ end
358
+ end
359
+
360
+ context "when an ignore filter was defined" do
361
+ before { subject.add_filter(&:ignore!) }
362
+
363
+ it "doesn't notify airbrake of requests" do
364
+ subject.notify(
365
+ Airbrake::Request.new(
366
+ method: 'GET',
367
+ route: '/foo',
368
+ status_code: 200,
369
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
370
+ )
371
+ )
372
+ expect(a_request(:put, routes)).not_to have_been_made
373
+ end
374
+
375
+ it "doesn't notify airbrake of queries" do
376
+ subject.notify(
377
+ Airbrake::Query.new(
378
+ method: 'POST',
379
+ route: '/foo',
380
+ query: 'SELECT * FROM things',
381
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
382
+ )
383
+ )
384
+ expect(a_request(:put, queries)).not_to have_been_made
385
+ end
386
+ end
387
+
388
+ context "when a filter that modifies payload was defined" do
389
+ before do
390
+ subject.add_filter do |resource|
391
+ resource.route = '[Filtered]'
392
+ end
393
+ end
394
+
395
+ it "notifies airbrake with modified payload" do
396
+ subject.notify(
397
+ Airbrake::Query.new(
398
+ method: 'POST',
399
+ route: '/foo',
400
+ query: 'SELECT * FROM things',
401
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
402
+ )
403
+ )
404
+ expect(
405
+ a_request(:put, queries).with(
406
+ body: /\A{"queries":\[{"method":"POST","route":"\[Filtered\]"/
407
+ )
408
+ ).to have_been_made
409
+ end
410
+ end
411
+ end
412
+
413
+ describe "#delete_filter" do
414
+ let(:filter) do
415
+ Class.new do
416
+ def call(resource); end
417
+ end
418
+ end
419
+
420
+ before { subject.add_filter(filter.new) }
421
+
422
+ it "deletes a filter" do
423
+ subject.delete_filter(filter)
424
+ subject.notify(
425
+ Airbrake::Request.new(
426
+ method: 'POST',
427
+ route: '/foo',
428
+ status_code: 200,
429
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
430
+ )
431
+ )
432
+ expect(a_request(:put, routes)).to have_been_made
433
+ end
434
+ end
435
+ end
@@ -0,0 +1,197 @@
1
+ RSpec.describe Airbrake::Promise do
2
+ describe ".then" do
3
+ let(:resolved_with) { [] }
4
+ let(:rejected_with) { [] }
5
+
6
+ context "when it is not resolved" do
7
+ it "returns self" do
8
+ expect(subject.then {}).to eq(subject)
9
+ end
10
+
11
+ it "doesn't call the resolve callbacks yet" do
12
+ subject.then { resolved_with << 1 }.then { resolved_with << 2 }
13
+ expect(resolved_with).to be_empty
14
+ end
15
+ end
16
+
17
+ context "when it is resolved" do
18
+ shared_examples "then specs" do
19
+ it "returns self" do
20
+ expect(subject.then {}).to eq(subject)
21
+ end
22
+
23
+ it "yields the resolved value" do
24
+ yielded = nil
25
+ subject.then { |value| yielded = value }
26
+ expect(yielded).to eq('id' => '123')
27
+ end
28
+
29
+ it "calls the resolve callbacks" do
30
+ expect(resolved_with).to match_array([1, 2])
31
+ end
32
+
33
+ it "doesn't call the reject callbacks" do
34
+ expect(rejected_with).to be_empty
35
+ end
36
+ end
37
+
38
+ context "and there are some resolve and reject callbacks in place" do
39
+ before do
40
+ subject.then { resolved_with << 1 }.then { resolved_with << 2 }
41
+ subject.rescue { rejected_with << 1 }.rescue { rejected_with << 2 }
42
+ subject.resolve('id' => '123')
43
+ end
44
+
45
+ include_examples "then specs"
46
+
47
+ it "registers the resolve callbacks" do
48
+ subject.resolve('id' => '456')
49
+ expect(resolved_with).to match_array([1, 2, 1, 2])
50
+ end
51
+ end
52
+
53
+ context "and additional then callbacks are added" do
54
+ before do
55
+ subject.resolve('id' => '123')
56
+ subject.then { resolved_with << 1 }.then { resolved_with << 2 }
57
+ end
58
+
59
+ include_examples "then specs"
60
+
61
+ it "doesn't register new resolve callbacks" do
62
+ subject.resolve('id' => '456')
63
+ expect(resolved_with).to match_array([1, 2])
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ describe ".rescue" do
70
+ let(:resolved_with) { [] }
71
+ let(:rejected_with) { [] }
72
+
73
+ context "when it is not rejected" do
74
+ it "returns self" do
75
+ expect(subject.then {}).to eq(subject)
76
+ end
77
+
78
+ it "doesn't call the reject callbacks yet" do
79
+ subject.rescue { rejected_with << 1 }.rescue { rejected_with << 2 }
80
+ expect(rejected_with).to be_empty
81
+ end
82
+ end
83
+
84
+ context "when it is rejected" do
85
+ shared_examples "rescue specs" do
86
+ it "returns self" do
87
+ expect(subject.rescue {}).to eq(subject)
88
+ end
89
+
90
+ it "yields the rejected value" do
91
+ yielded = nil
92
+ subject.rescue { |value| yielded = value }
93
+ expect(yielded).to eq('bingo')
94
+ end
95
+
96
+ it "doesn't call the resolve callbacks" do
97
+ expect(resolved_with).to be_empty
98
+ end
99
+
100
+ it "calls the reject callbacks" do
101
+ expect(rejected_with).to match_array([1, 2])
102
+ end
103
+ end
104
+
105
+ context "and there are some resolve and reject callbacks in place" do
106
+ before do
107
+ subject.then { resolved_with << 1 }.then { resolved_with << 2 }
108
+ subject.rescue { rejected_with << 1 }.rescue { rejected_with << 2 }
109
+ subject.reject('bingo')
110
+ end
111
+
112
+ include_examples "rescue specs"
113
+
114
+ it "registers the reject callbacks" do
115
+ subject.reject('bingo again')
116
+ expect(rejected_with).to match_array([1, 2, 1, 2])
117
+ end
118
+ end
119
+
120
+ context "and additional reject callbacks are added" do
121
+ before do
122
+ subject.reject('bingo')
123
+ subject.rescue { rejected_with << 1 }.rescue { rejected_with << 2 }
124
+ end
125
+
126
+ include_examples "rescue specs"
127
+
128
+ it "doesn't register new reject callbacks" do
129
+ subject.reject('bingo again')
130
+ expect(rejected_with).to match_array([1, 2])
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ describe ".resolve" do
137
+ it "returns self" do
138
+ expect(subject.resolve(1)).to eq(subject)
139
+ end
140
+
141
+ it "executes callbacks attached with .then" do
142
+ array = []
143
+ subject.then { |notice_id| array << notice_id }.rescue { array << 999 }
144
+
145
+ expect(array.size).to be_zero
146
+ subject.resolve(1)
147
+ expect(array).to match_array([1])
148
+ end
149
+ end
150
+
151
+ describe ".reject" do
152
+ it "returns self" do
153
+ expect(subject.reject(1)).to eq(subject)
154
+ end
155
+
156
+ it "executes callbacks attached with .rescue" do
157
+ array = []
158
+ subject.then { array << 1 }.rescue { |error| array << error }
159
+
160
+ expect(array.size).to be_zero
161
+ subject.reject(999)
162
+ expect(array).to match_array([999])
163
+ end
164
+ end
165
+
166
+ describe "#rejected?" do
167
+ context "when it was rejected" do
168
+ before { subject.reject(1) }
169
+ it { is_expected.to be_rejected }
170
+ end
171
+
172
+ context "when it wasn't rejected" do
173
+ it { is_expected.not_to be_rejected }
174
+ end
175
+
176
+ context "when it was resolved" do
177
+ before { subject.resolve }
178
+ it { is_expected.not_to be_rejected }
179
+ end
180
+ end
181
+
182
+ describe "#resolved?" do
183
+ context "when it was resolved" do
184
+ before { subject.resolve }
185
+ it { is_expected.to be_resolved }
186
+ end
187
+
188
+ context "when it wasn't resolved" do
189
+ it { is_expected.not_to be_resolved }
190
+ end
191
+
192
+ context "when it was rejected" do
193
+ before { subject.reject(1) }
194
+ it { is_expected.not_to be_resolved }
195
+ end
196
+ end
197
+ end