airbrake-ruby 4.6.0

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