airbrake-ruby 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/lib/airbrake-ruby/async_sender.rb +22 -96
  3. data/lib/airbrake-ruby/backtrace.rb +8 -7
  4. data/lib/airbrake-ruby/benchmark.rb +39 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  6. data/lib/airbrake-ruby/config/processor.rb +84 -0
  7. data/lib/airbrake-ruby/config/validator.rb +9 -3
  8. data/lib/airbrake-ruby/config.rb +76 -20
  9. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  10. data/lib/airbrake-ruby/file_cache.rb +6 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  18. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  19. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +30 -6
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +4 -2
  25. data/lib/airbrake-ruby/grouppable.rb +12 -0
  26. data/lib/airbrake-ruby/ignorable.rb +1 -0
  27. data/lib/airbrake-ruby/inspectable.rb +2 -2
  28. data/lib/airbrake-ruby/loggable.rb +2 -2
  29. data/lib/airbrake-ruby/mergeable.rb +12 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +48 -0
  31. data/lib/airbrake-ruby/notice.rb +10 -20
  32. data/lib/airbrake-ruby/notice_notifier.rb +23 -42
  33. data/lib/airbrake-ruby/performance_breakdown.rb +52 -0
  34. data/lib/airbrake-ruby/performance_notifier.rb +126 -49
  35. data/lib/airbrake-ruby/promise.rb +1 -0
  36. data/lib/airbrake-ruby/query.rb +26 -11
  37. data/lib/airbrake-ruby/queue.rb +65 -0
  38. data/lib/airbrake-ruby/remote_settings/settings_data.rb +120 -0
  39. data/lib/airbrake-ruby/remote_settings.rb +145 -0
  40. data/lib/airbrake-ruby/request.rb +20 -6
  41. data/lib/airbrake-ruby/stashable.rb +15 -0
  42. data/lib/airbrake-ruby/stat.rb +34 -24
  43. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  44. data/lib/airbrake-ruby/tdigest.rb +43 -58
  45. data/lib/airbrake-ruby/thread_pool.rb +138 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +10 -4
  48. data/lib/airbrake-ruby/version.rb +11 -1
  49. data/lib/airbrake-ruby.rb +219 -53
  50. data/spec/airbrake_spec.rb +428 -9
  51. data/spec/async_sender_spec.rb +26 -110
  52. data/spec/backtrace_spec.rb +44 -44
  53. data/spec/benchmark_spec.rb +33 -0
  54. data/spec/code_hunk_spec.rb +11 -11
  55. data/spec/config/processor_spec.rb +209 -0
  56. data/spec/config/validator_spec.rb +23 -6
  57. data/spec/config_spec.rb +77 -7
  58. data/spec/deploy_notifier_spec.rb +2 -2
  59. data/spec/{file_cache.rb → file_cache_spec.rb} +2 -4
  60. data/spec/filter_chain_spec.rb +28 -1
  61. data/spec/filters/dependency_filter_spec.rb +1 -1
  62. data/spec/filters/gem_root_filter_spec.rb +9 -9
  63. data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
  64. data/spec/filters/git_repository_filter.rb +1 -1
  65. data/spec/filters/git_revision_filter_spec.rb +13 -11
  66. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
  67. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
  68. data/spec/filters/root_directory_filter_spec.rb +9 -9
  69. data/spec/filters/sql_filter_spec.rb +110 -55
  70. data/spec/filters/system_exit_filter_spec.rb +1 -1
  71. data/spec/filters/thread_filter_spec.rb +33 -31
  72. data/spec/fixtures/project_root/code.rb +9 -9
  73. data/spec/loggable_spec.rb +17 -0
  74. data/spec/monotonic_time_spec.rb +23 -0
  75. data/spec/{notice_notifier_spec → notice_notifier}/options_spec.rb +19 -21
  76. data/spec/notice_notifier_spec.rb +20 -80
  77. data/spec/notice_spec.rb +9 -11
  78. data/spec/performance_breakdown_spec.rb +11 -0
  79. data/spec/performance_notifier_spec.rb +360 -85
  80. data/spec/query_spec.rb +11 -0
  81. data/spec/queue_spec.rb +18 -0
  82. data/spec/remote_settings/settings_data_spec.rb +365 -0
  83. data/spec/remote_settings_spec.rb +230 -0
  84. data/spec/request_spec.rb +9 -0
  85. data/spec/response_spec.rb +8 -8
  86. data/spec/spec_helper.rb +9 -13
  87. data/spec/stashable_spec.rb +23 -0
  88. data/spec/stat_spec.rb +17 -15
  89. data/spec/sync_sender_spec.rb +14 -12
  90. data/spec/tdigest_spec.rb +6 -6
  91. data/spec/thread_pool_spec.rb +187 -0
  92. data/spec/timed_trace_spec.rb +125 -0
  93. data/spec/truncator_spec.rb +12 -12
  94. metadata +55 -18
@@ -0,0 +1,365 @@
1
+ RSpec.describe Airbrake::RemoteSettings::SettingsData do
2
+ let(:project_id) { 123 }
3
+
4
+ describe "#merge!" do
5
+ it "returns self" do
6
+ settings_data = described_class.new(project_id, {})
7
+ expect(settings_data.merge!({})).to eql(settings_data)
8
+ end
9
+
10
+ it "merges the given hash with the data" do
11
+ settings_data = described_class.new(project_id, {})
12
+ settings_data.merge!('poll_sec' => 123, 'config_route' => 'abc')
13
+
14
+ expect(settings_data.interval).to eq(123)
15
+ expect(settings_data.config_route(''))
16
+ .to eq('abc/2020-06-18/config/123/config.json')
17
+ end
18
+ end
19
+
20
+ describe "#interval" do
21
+ context "when given data has zero interval" do
22
+ let(:data) do
23
+ { 'poll_sec' => 0 }
24
+ end
25
+
26
+ it "returns the default interval" do
27
+ expect(described_class.new(project_id, data).interval).to eq(600)
28
+ end
29
+ end
30
+
31
+ context "when given data has negative interval" do
32
+ let(:data) do
33
+ { 'poll_sec' => -1 }
34
+ end
35
+
36
+ it "returns the default interval" do
37
+ expect(described_class.new(project_id, data).interval).to eq(600)
38
+ end
39
+ end
40
+
41
+ context "when given data has nil interval" do
42
+ let(:data) do
43
+ { 'poll_sec' => nil }
44
+ end
45
+
46
+ it "returns the default interval" do
47
+ expect(described_class.new(project_id, data).interval).to eq(600)
48
+ end
49
+ end
50
+
51
+ context "when given data has a positive interval" do
52
+ let(:data) do
53
+ { 'poll_sec' => 123 }
54
+ end
55
+
56
+ it "returns the interval from data" do
57
+ expect(described_class.new(project_id, data).interval).to eq(123)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#config_route" do
63
+ let(:host) { 'https://v1-production-notifier-configs.s3.amazonaws.com' }
64
+
65
+ context "when given a remote host through the remote config" do
66
+ context "and when the remote host ends with a slash" do
67
+ let(:data) do
68
+ { 'config_route' => 'http://example.com/' }
69
+ end
70
+
71
+ it "returns the route with the host" do
72
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
73
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
74
+ )
75
+ end
76
+ end
77
+
78
+ context "and when the remote host doesn't with a slash" do
79
+ let(:data) do
80
+ { 'config_route' => 'http://example.com' }
81
+ end
82
+
83
+ it "returns the route with the host" do
84
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
85
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
86
+ )
87
+ end
88
+ end
89
+ end
90
+
91
+ context "when given a remote host through local configuration" do
92
+ context "and when the remote host ends with a slash" do
93
+ let(:host) { 'http://example.com/' }
94
+
95
+ it "returns the route with the host" do
96
+ expect(described_class.new(project_id, {}).config_route(host)).to eq(
97
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
98
+ )
99
+ end
100
+ end
101
+
102
+ context "and when the remote host doesn't with a slash" do
103
+ let(:host) { 'http://example.com' }
104
+
105
+ it "returns the route with the host" do
106
+ expect(described_class.new(project_id, {}).config_route(host)).to eq(
107
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
108
+ )
109
+ end
110
+ end
111
+ end
112
+
113
+ context "when the given remote host in the remote config is nil" do
114
+ let(:data) do
115
+ { 'config_route' => nil }
116
+ end
117
+
118
+ it "returns the route with the given host instead" do
119
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
120
+ 'https://v1-production-notifier-configs.s3.amazonaws.com/' \
121
+ "2020-06-18/config/#{project_id}/config.json",
122
+ )
123
+ end
124
+ end
125
+ end
126
+
127
+ describe "#error_notifications?" do
128
+ context "when the 'errors' setting is present" do
129
+ context "and when it is enabled" do
130
+ let(:data) do
131
+ {
132
+ 'settings' => [
133
+ {
134
+ 'name' => 'errors',
135
+ 'enabled' => true,
136
+ },
137
+ ],
138
+ }
139
+ end
140
+
141
+ it "returns true" do
142
+ expect(described_class.new(project_id, data).error_notifications?)
143
+ .to eq(true)
144
+ end
145
+ end
146
+
147
+ context "and when it is disabled" do
148
+ let(:data) do
149
+ {
150
+ 'settings' => [
151
+ {
152
+ 'name' => 'errors',
153
+ 'enabled' => false,
154
+ },
155
+ ],
156
+ }
157
+ end
158
+
159
+ it "returns false" do
160
+ expect(described_class.new(project_id, data).error_notifications?)
161
+ .to eq(false)
162
+ end
163
+ end
164
+ end
165
+
166
+ context "when the 'errors' setting is missing" do
167
+ let(:data) do
168
+ { 'settings' => [] }
169
+ end
170
+
171
+ it "returns true" do
172
+ expect(described_class.new(project_id, data).error_notifications?)
173
+ .to eq(true)
174
+ end
175
+ end
176
+ end
177
+
178
+ describe "#performance_stats?" do
179
+ context "when the 'apm' setting is present" do
180
+ context "and when it is enabled" do
181
+ let(:data) do
182
+ {
183
+ 'settings' => [
184
+ {
185
+ 'name' => 'apm',
186
+ 'enabled' => true,
187
+ },
188
+ ],
189
+ }
190
+ end
191
+
192
+ it "returns true" do
193
+ expect(described_class.new(project_id, data).performance_stats?)
194
+ .to eq(true)
195
+ end
196
+ end
197
+
198
+ context "and when it is disabled" do
199
+ let(:data) do
200
+ {
201
+ 'settings' => [
202
+ {
203
+ 'name' => 'apm',
204
+ 'enabled' => false,
205
+ },
206
+ ],
207
+ }
208
+ end
209
+
210
+ it "returns false" do
211
+ expect(described_class.new(project_id, data).performance_stats?)
212
+ .to eq(false)
213
+ end
214
+ end
215
+ end
216
+
217
+ context "when the 'apm' setting is missing" do
218
+ let(:data) do
219
+ { 'settings' => [] }
220
+ end
221
+
222
+ it "returns true" do
223
+ expect(described_class.new(project_id, data).performance_stats?)
224
+ .to eq(true)
225
+ end
226
+ end
227
+ end
228
+
229
+ describe "#error_host" do
230
+ context "when the 'errors' setting is present" do
231
+ context "and when 'endpoint' is specified" do
232
+ let(:endpoint) { 'https://api.example.com/' }
233
+
234
+ let(:data) do
235
+ {
236
+ 'settings' => [
237
+ {
238
+ 'name' => 'errors',
239
+ 'enabled' => true,
240
+ 'endpoint' => endpoint,
241
+ },
242
+ ],
243
+ }
244
+ end
245
+
246
+ it "returns the endpoint" do
247
+ expect(described_class.new(project_id, data).error_host).to eq(endpoint)
248
+ end
249
+ end
250
+
251
+ context "and when an endpoint is NOT specified" do
252
+ let(:data) do
253
+ {
254
+ 'settings' => [
255
+ {
256
+ 'name' => 'errors',
257
+ 'enabled' => true,
258
+ },
259
+ ],
260
+ }
261
+ end
262
+
263
+ it "returns nil" do
264
+ expect(described_class.new(project_id, data).error_host).to be_nil
265
+ end
266
+ end
267
+ end
268
+
269
+ context "when the 'errors' setting is missing" do
270
+ let(:data) do
271
+ { 'settings' => [] }
272
+ end
273
+
274
+ it "returns nil" do
275
+ expect(described_class.new(project_id, data).error_host).to be_nil
276
+ end
277
+ end
278
+ end
279
+
280
+ describe "#apm_host" do
281
+ context "when the 'apm' setting is present" do
282
+ context "and when 'endpoint' is specified" do
283
+ let(:endpoint) { 'https://api.example.com/' }
284
+
285
+ let(:data) do
286
+ {
287
+ 'settings' => [
288
+ {
289
+ 'name' => 'apm',
290
+ 'enabled' => true,
291
+ 'endpoint' => endpoint,
292
+ },
293
+ ],
294
+ }
295
+ end
296
+
297
+ it "returns the endpoint" do
298
+ expect(described_class.new(project_id, data).apm_host).to eq(endpoint)
299
+ end
300
+ end
301
+
302
+ context "and when an endpoint is NOT specified" do
303
+ let(:data) do
304
+ {
305
+ 'settings' => [
306
+ {
307
+ 'name' => 'apm',
308
+ 'enabled' => true,
309
+ },
310
+ ],
311
+ }
312
+ end
313
+
314
+ it "returns nil" do
315
+ expect(described_class.new(project_id, data).apm_host).to be_nil
316
+ end
317
+ end
318
+ end
319
+
320
+ context "when the 'apm' setting is missing" do
321
+ let(:data) do
322
+ { 'settings' => [] }
323
+ end
324
+
325
+ it "returns nil" do
326
+ expect(described_class.new(project_id, data).apm_host).to be_nil
327
+ end
328
+ end
329
+ end
330
+
331
+ describe "#to_h" do
332
+ let(:data) do
333
+ {
334
+ 'poll_sec' => 123,
335
+ 'settings' => [
336
+ {
337
+ 'name' => 'apm',
338
+ 'enabled' => false,
339
+ },
340
+ ],
341
+ }
342
+ end
343
+
344
+ subject { described_class.new(project_id, data) }
345
+
346
+ it "returns a hash representation of settings" do
347
+ expect(described_class.new(project_id, data).to_h).to eq(data)
348
+ end
349
+
350
+ it "doesn't allow mutation of the original data object" do
351
+ hash = subject.to_h
352
+ hash['poll_sec'] = 0
353
+
354
+ expect(subject.to_h).to eq(
355
+ 'poll_sec' => 123,
356
+ 'settings' => [
357
+ {
358
+ 'name' => 'apm',
359
+ 'enabled' => false,
360
+ },
361
+ ],
362
+ )
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,230 @@
1
+ RSpec.describe Airbrake::RemoteSettings do
2
+ let(:project_id) { 123 }
3
+ let(:host) { 'https://v1-production-notifier-configs.s3.amazonaws.com' }
4
+
5
+ let(:endpoint) do
6
+ "#{host}/2020-06-18/config/#{project_id}/config.json"
7
+ end
8
+
9
+ let(:body) do
10
+ {
11
+ 'poll_sec' => 1,
12
+ 'settings' => [
13
+ {
14
+ 'name' => 'apm',
15
+ 'enabled' => false,
16
+ },
17
+ {
18
+ 'name' => 'errors',
19
+ 'enabled' => true,
20
+ },
21
+ ],
22
+ }
23
+ end
24
+
25
+ let(:config_path) { described_class::CONFIG_DUMP_PATH }
26
+ let(:config_dir) { File.dirname(config_path) }
27
+
28
+ let!(:stub) do
29
+ stub_request(:get, Regexp.new(endpoint))
30
+ .to_return(status: 200, body: body.to_json)
31
+ end
32
+
33
+ before do
34
+ # Do not create config dumps on disk.
35
+ allow(Dir).to receive(:mkdir).with(config_dir)
36
+ allow(File).to receive(:write).with(config_path, anything)
37
+ end
38
+
39
+ describe ".poll" do
40
+ describe "config loading" do
41
+ let(:settings_data) { described_class::SettingsData.new(project_id, body) }
42
+
43
+ before do
44
+ allow(File).to receive(:exist?).with(config_path).and_return(true)
45
+ allow(File).to receive(:read).with(config_path).and_return(body.to_json)
46
+
47
+ allow(described_class::SettingsData).to receive(:new).and_return(settings_data)
48
+ end
49
+
50
+ it "loads the config from disk" do
51
+ expect(File).to receive(:read).with(config_path)
52
+ expect(settings_data).to receive(:merge!).with(body).twice
53
+
54
+ remote_settings = described_class.poll(project_id, host) {}
55
+ sleep(0.2)
56
+ remote_settings.stop_polling
57
+
58
+ expect(stub).to have_been_requested.once
59
+ end
60
+
61
+ it "yields the config to the block twice" do
62
+ block = proc {}
63
+ expect(block).to receive(:call).twice
64
+
65
+ remote_settings = described_class.poll(project_id, host, &block)
66
+ sleep(0.2)
67
+ remote_settings.stop_polling
68
+
69
+ expect(stub).to have_been_requested.once
70
+ end
71
+
72
+ context "when config loading fails" do
73
+ it "logs an error" do
74
+ expect(File).to receive(:read).and_raise(StandardError)
75
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
76
+ '**Airbrake: config loading failed: StandardError',
77
+ )
78
+
79
+ remote_settings = described_class.poll(project_id, host) {}
80
+ sleep(0.2)
81
+ remote_settings.stop_polling
82
+
83
+ expect(stub).to have_been_requested.once
84
+ end
85
+ end
86
+ end
87
+
88
+ context "when no errors are raised" do
89
+ it "makes a request to AWS S3" do
90
+ remote_settings = described_class.poll(project_id, host) {}
91
+ sleep(0.1)
92
+ remote_settings.stop_polling
93
+
94
+ expect(stub).to have_been_requested.at_least_once
95
+ end
96
+
97
+ it "sends params about the environment with the request" do
98
+ remote_settings = described_class.poll(project_id, host) {}
99
+ sleep(0.1)
100
+ remote_settings.stop_polling
101
+
102
+ stub_with_query_params = stub.with(
103
+ query: URI.decode_www_form(described_class::QUERY_PARAMS).to_h,
104
+ )
105
+ expect(stub_with_query_params).to have_been_requested.at_least_once
106
+ end
107
+
108
+ it "fetches remote settings" do
109
+ settings = nil
110
+ remote_settings = described_class.poll(project_id, host) do |data|
111
+ settings = data
112
+ end
113
+ sleep(0.1)
114
+ remote_settings.stop_polling
115
+
116
+ expect(settings.error_notifications?).to eq(true)
117
+ expect(settings.performance_stats?).to eq(false)
118
+ expect(settings.interval).to eq(1)
119
+ end
120
+ end
121
+
122
+ context "when an error is raised while making a HTTP request" do
123
+ before do
124
+ allow(Net::HTTP).to receive(:get).and_raise(StandardError)
125
+ end
126
+
127
+ it "doesn't fetch remote settings" do
128
+ settings = nil
129
+ remote_settings = described_class.poll(project_id, host) do |data|
130
+ settings = data
131
+ end
132
+ sleep(0.1)
133
+ remote_settings.stop_polling
134
+
135
+ expect(stub).not_to have_been_requested
136
+ expect(settings.interval).to eq(600)
137
+ end
138
+ end
139
+
140
+ context "when an error is raised while parsing returned JSON" do
141
+ before do
142
+ allow(JSON).to receive(:parse).and_raise(JSON::ParserError)
143
+ end
144
+
145
+ it "doesn't update settings data" do
146
+ settings = nil
147
+ remote_settings = described_class.poll(project_id, host) do |data|
148
+ settings = data
149
+ end
150
+ sleep(0.1)
151
+ remote_settings.stop_polling
152
+
153
+ expect(stub).to have_been_requested.once
154
+ expect(settings.interval).to eq(600)
155
+ end
156
+ end
157
+
158
+ context "when API returns an XML response" do
159
+ let!(:stub) do
160
+ stub_request(:get, Regexp.new(endpoint))
161
+ .to_return(status: 200, body: '<?xml ...')
162
+ end
163
+
164
+ it "doesn't update settings data" do
165
+ settings = nil
166
+ remote_settings = described_class.poll(project_id, host) do |data|
167
+ settings = data
168
+ end
169
+ sleep(0.1)
170
+ remote_settings.stop_polling
171
+
172
+ expect(stub).to have_been_requested.once
173
+ expect(settings.interval).to eq(600)
174
+ end
175
+ end
176
+
177
+ context "when a config route is specified in the returned data" do
178
+ let(:new_endpoint) do
179
+ "http://example.com"
180
+ end
181
+
182
+ let(:body) do
183
+ { 'config_route' => new_endpoint, 'poll_sec' => 0.1 }
184
+ end
185
+
186
+ let!(:new_stub) do
187
+ stub_request(:get, Regexp.new(new_endpoint))
188
+ .to_return(status: 200, body: body.to_json)
189
+ end
190
+
191
+ it "makes the next request to the specified config route" do
192
+ remote_settings = described_class.poll(project_id, host) {}
193
+ sleep(0.2)
194
+
195
+ remote_settings.stop_polling
196
+
197
+ expect(stub).to have_been_requested.once
198
+ expect(new_stub).to have_been_requested.once
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#stop_polling" do
204
+ it "dumps config data to disk" do
205
+ expect(Dir).to receive(:mkdir).with(config_dir)
206
+ expect(File).to receive(:write).with(config_path, body.to_json)
207
+
208
+ remote_settings = described_class.poll(project_id, host) {}
209
+ sleep(0.2)
210
+ remote_settings.stop_polling
211
+
212
+ expect(stub).to have_been_requested.once
213
+ end
214
+
215
+ context "when config dumping fails" do
216
+ it "logs an error" do
217
+ expect(File).to receive(:write).and_raise(StandardError)
218
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
219
+ '**Airbrake: config dumping failed: StandardError',
220
+ )
221
+
222
+ remote_settings = described_class.poll(project_id, host) {}
223
+ sleep(0.2)
224
+ remote_settings.stop_polling
225
+
226
+ expect(stub).to have_been_requested.once
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.describe Airbrake::Request do
2
+ describe "#stash" do
3
+ subject do
4
+ described_class.new(method: 'GET', route: '/', status_code: 200)
5
+ end
6
+
7
+ it { is_expected.to respond_to(:stash) }
8
+ end
9
+ end
@@ -4,7 +4,7 @@ RSpec.describe Airbrake::Response do
4
4
  context "when response code is #{code}" do
5
5
  it "logs response body" do
6
6
  expect(Airbrake::Loggable.instance).to receive(:debug).with(
7
- /Airbrake::Response \(#{code}\): {}/
7
+ /Airbrake::Response \(#{code}\): {}/,
8
8
  )
9
9
  described_class.parse(OpenStruct.new(code: code, body: '{}'))
10
10
  end
@@ -15,10 +15,10 @@ RSpec.describe Airbrake::Response do
15
15
  context "when response code is #{code}" do
16
16
  it "logs response message" do
17
17
  expect(Airbrake::Loggable.instance).to receive(:error).with(
18
- /Airbrake: foo/
18
+ /Airbrake: foo/,
19
19
  )
20
20
  described_class.parse(
21
- OpenStruct.new(code: code, body: '{"message":"foo"}')
21
+ OpenStruct.new(code: code, body: '{"message":"foo"}'),
22
22
  )
23
23
  end
24
24
  end
@@ -29,7 +29,7 @@ RSpec.describe Airbrake::Response do
29
29
 
30
30
  it "logs response message" do
31
31
  expect(Airbrake::Loggable.instance).to receive(:error).with(
32
- /Airbrake: rate limited/
32
+ /Airbrake: rate limited/,
33
33
  )
34
34
  described_class.parse(response)
35
35
  end
@@ -41,7 +41,7 @@ RSpec.describe Airbrake::Response do
41
41
  resp = described_class.parse(response)
42
42
  expect(resp).to include(
43
43
  'error' => '**Airbrake: rate limited',
44
- 'rate_limit_reset' => time
44
+ 'rate_limit_reset' => time,
45
45
  )
46
46
  end
47
47
  end
@@ -51,7 +51,7 @@ RSpec.describe Airbrake::Response do
51
51
 
52
52
  it "logs response body" do
53
53
  expect(Airbrake::Loggable.instance).to receive(:error).with(
54
- /Airbrake: unexpected code \(500\)\. Body: foo/
54
+ /Airbrake: unexpected code \(500\)\. Body: foo/,
55
55
  )
56
56
  described_class.parse(response)
57
57
  end
@@ -73,14 +73,14 @@ RSpec.describe Airbrake::Response do
73
73
 
74
74
  it "logs response body" do
75
75
  expect(Airbrake::Loggable.instance).to receive(:error).with(
76
- /Airbrake: error while parsing body \(.*unexpected token.*\)\. Body: foo/
76
+ /Airbrake: error while parsing body \(.*unexpected token.*\)\. Body: foo/,
77
77
  )
78
78
  described_class.parse(response)
79
79
  end
80
80
 
81
81
  it "returns an error message" do
82
82
  expect(described_class.parse(response)['error']).to match(
83
- /\A#<JSON::ParserError.+>/
83
+ /\A#<JSON::ParserError.+>/,
84
84
  )
85
85
  end
86
86
  end