airbrake-ruby 4.15.0 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,7 +10,9 @@ RSpec.describe Airbrake::Config do
10
10
  its(:app_version) { is_expected.to be_nil }
11
11
  its(:versions) { is_expected.to be_empty }
12
12
  its(:host) { is_expected.to eq('https://api.airbrake.io') }
13
- its(:endpoint) { is_expected.not_to be_nil }
13
+ its(:error_host) { is_expected.to eq('https://api.airbrake.io') }
14
+ its(:apm_host) { is_expected.to eq('https://api.airbrake.io') }
15
+ its(:error_endpoint) { is_expected.not_to be_nil }
14
16
  its(:workers) { is_expected.to eq(1) }
15
17
  its(:queue_size) { is_expected.to eq(100) }
16
18
  its(:root_directory) { is_expected.to eq(Bundler.root.realpath.to_s) }
@@ -23,6 +25,8 @@ RSpec.describe Airbrake::Config do
23
25
  its(:performance_stats_flush_period) { is_expected.to eq(15) }
24
26
  its(:query_stats) { is_expected.to eq(true) }
25
27
  its(:job_stats) { is_expected.to eq(true) }
28
+ its(:error_notifications) { is_expected.to eq(true) }
29
+ its(:__remote_configuration) { is_expected.to eq(false) }
26
30
 
27
31
  describe "#new" do
28
32
  context "when user config is passed" do
@@ -63,13 +67,13 @@ RSpec.describe Airbrake::Config do
63
67
  end
64
68
  end
65
69
 
66
- describe "#endpoint" do
70
+ describe "#error_endpoint" do
67
71
  subject { described_class.new(valid_params.merge(user_config)) }
68
72
 
69
73
  context "when host ends with a URL with a slug with a trailing slash" do
70
74
  let(:user_config) { { host: 'https://localhost/bingo/' } }
71
75
 
72
- its(:endpoint) do
76
+ its(:error_endpoint) do
73
77
  is_expected.to eq(URI('https://localhost/bingo/api/v3/projects/1/notices'))
74
78
  end
75
79
  end
@@ -77,7 +81,7 @@ RSpec.describe Airbrake::Config do
77
81
  context "when host ends with a URL with a slug without a trailing slash" do
78
82
  let(:user_config) { { host: 'https://localhost/bingo' } }
79
83
 
80
- its(:endpoint) do
84
+ its(:error_endpoint) do
81
85
  is_expected.to eq(URI('https://localhost/api/v3/projects/1/notices'))
82
86
  end
83
87
  end
@@ -0,0 +1,327 @@
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
+ # rubocop:disable Performance/RedundantMerge
13
+ settings_data.merge!('poll_sec' => 123, 'config_route' => 'abc')
14
+ # rubocop:enable Performance/RedundantMerge
15
+
16
+ expect(settings_data.interval).to eq(123)
17
+ expect(settings_data.config_route).to eq('abc')
18
+ end
19
+ end
20
+
21
+ describe "#interval" do
22
+ context "when given data has zero interval" do
23
+ let(:data) do
24
+ { 'poll_sec' => 0 }
25
+ end
26
+
27
+ it "returns the default interval" do
28
+ expect(described_class.new(project_id, data).interval).to eq(600)
29
+ end
30
+ end
31
+
32
+ context "when given data has negative interval" do
33
+ let(:data) do
34
+ { 'poll_sec' => -1 }
35
+ end
36
+
37
+ it "returns the default interval" do
38
+ expect(described_class.new(project_id, data).interval).to eq(600)
39
+ end
40
+ end
41
+
42
+ context "when given data has nil interval" do
43
+ let(:data) do
44
+ { 'poll_sec' => nil }
45
+ end
46
+
47
+ it "returns the default interval" do
48
+ expect(described_class.new(project_id, data).interval).to eq(600)
49
+ end
50
+ end
51
+
52
+ context "when given data has a positive interval" do
53
+ let(:data) do
54
+ { 'poll_sec' => 123 }
55
+ end
56
+
57
+ it "returns the interval from data" do
58
+ expect(described_class.new(project_id, data).interval).to eq(123)
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#config_route" do
64
+ context "when given a pathname" do
65
+ let(:data) do
66
+ { 'config_route' => 'http://example.com' }
67
+ end
68
+
69
+ it "returns the given pathname" do
70
+ expect(described_class.new(project_id, data).config_route)
71
+ .to eq('http://example.com')
72
+ end
73
+ end
74
+
75
+ context "when the pathname is nil" do
76
+ let(:data) do
77
+ { 'config_route' => nil }
78
+ end
79
+
80
+ it "returns the default pathname" do
81
+ expect(described_class.new(project_id, data).config_route).to eq(
82
+ 'https://staging-notifier-configs.s3.amazonaws.com/' \
83
+ "2020-06-18/config/#{project_id}/config.json",
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#error_notifications?" do
90
+ context "when the 'errors' setting is present" do
91
+ context "and when it is enabled" do
92
+ let(:data) do
93
+ {
94
+ 'settings' => [
95
+ {
96
+ 'name' => 'errors',
97
+ 'enabled' => true,
98
+ },
99
+ ],
100
+ }
101
+ end
102
+
103
+ it "returns true" do
104
+ expect(described_class.new(project_id, data).error_notifications?)
105
+ .to eq(true)
106
+ end
107
+ end
108
+
109
+ context "and when it is disabled" do
110
+ let(:data) do
111
+ {
112
+ 'settings' => [
113
+ {
114
+ 'name' => 'errors',
115
+ 'enabled' => false,
116
+ },
117
+ ],
118
+ }
119
+ end
120
+
121
+ it "returns false" do
122
+ expect(described_class.new(project_id, data).error_notifications?)
123
+ .to eq(false)
124
+ end
125
+ end
126
+ end
127
+
128
+ context "when the 'errors' setting is missing" do
129
+ let(:data) do
130
+ { 'settings' => [] }
131
+ end
132
+
133
+ it "returns true" do
134
+ expect(described_class.new(project_id, data).error_notifications?)
135
+ .to eq(true)
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "#performance_stats?" do
141
+ context "when the 'apm' setting is present" do
142
+ context "and when it is enabled" do
143
+ let(:data) do
144
+ {
145
+ 'settings' => [
146
+ {
147
+ 'name' => 'apm',
148
+ 'enabled' => true,
149
+ },
150
+ ],
151
+ }
152
+ end
153
+
154
+ it "returns true" do
155
+ expect(described_class.new(project_id, data).performance_stats?)
156
+ .to eq(true)
157
+ end
158
+ end
159
+
160
+ context "and when it is disabled" do
161
+ let(:data) do
162
+ {
163
+ 'settings' => [
164
+ {
165
+ 'name' => 'apm',
166
+ 'enabled' => false,
167
+ },
168
+ ],
169
+ }
170
+ end
171
+
172
+ it "returns false" do
173
+ expect(described_class.new(project_id, data).performance_stats?)
174
+ .to eq(false)
175
+ end
176
+ end
177
+ end
178
+
179
+ context "when the 'apm' setting is missing" do
180
+ let(:data) do
181
+ { 'settings' => [] }
182
+ end
183
+
184
+ it "returns true" do
185
+ expect(described_class.new(project_id, data).performance_stats?)
186
+ .to eq(true)
187
+ end
188
+ end
189
+ end
190
+
191
+ describe "#error_host" do
192
+ context "when the 'errors' setting is present" do
193
+ context "and when 'endpoint' is specified" do
194
+ let(:endpoint) { 'https://api.example.com/' }
195
+
196
+ let(:data) do
197
+ {
198
+ 'settings' => [
199
+ {
200
+ 'name' => 'errors',
201
+ 'enabled' => true,
202
+ 'endpoint' => endpoint,
203
+ },
204
+ ],
205
+ }
206
+ end
207
+
208
+ it "returns the endpoint" do
209
+ expect(described_class.new(project_id, data).error_host).to eq(endpoint)
210
+ end
211
+ end
212
+
213
+ context "and when an endpoint is NOT specified" do
214
+ let(:data) do
215
+ {
216
+ 'settings' => [
217
+ {
218
+ 'name' => 'errors',
219
+ 'enabled' => true,
220
+ },
221
+ ],
222
+ }
223
+ end
224
+
225
+ it "returns nil" do
226
+ expect(described_class.new(project_id, data).error_host).to be_nil
227
+ end
228
+ end
229
+ end
230
+
231
+ context "when the 'errors' setting is missing" do
232
+ let(:data) do
233
+ { 'settings' => [] }
234
+ end
235
+
236
+ it "returns nil" do
237
+ expect(described_class.new(project_id, data).error_host).to be_nil
238
+ end
239
+ end
240
+ end
241
+
242
+ describe "#apm_host" do
243
+ context "when the 'apm' setting is present" do
244
+ context "and when 'endpoint' is specified" do
245
+ let(:endpoint) { 'https://api.example.com/' }
246
+
247
+ let(:data) do
248
+ {
249
+ 'settings' => [
250
+ {
251
+ 'name' => 'apm',
252
+ 'enabled' => true,
253
+ 'endpoint' => endpoint,
254
+ },
255
+ ],
256
+ }
257
+ end
258
+
259
+ it "returns the endpoint" do
260
+ expect(described_class.new(project_id, data).apm_host).to eq(endpoint)
261
+ end
262
+ end
263
+
264
+ context "and when an endpoint is NOT specified" do
265
+ let(:data) do
266
+ {
267
+ 'settings' => [
268
+ {
269
+ 'name' => 'apm',
270
+ 'enabled' => true,
271
+ },
272
+ ],
273
+ }
274
+ end
275
+
276
+ it "returns nil" do
277
+ expect(described_class.new(project_id, data).apm_host).to be_nil
278
+ end
279
+ end
280
+ end
281
+
282
+ context "when the 'apm' setting is missing" do
283
+ let(:data) do
284
+ { 'settings' => [] }
285
+ end
286
+
287
+ it "returns nil" do
288
+ expect(described_class.new(project_id, data).apm_host).to be_nil
289
+ end
290
+ end
291
+ end
292
+
293
+ describe "#to_h" do
294
+ let(:data) do
295
+ {
296
+ 'poll_sec' => 123,
297
+ 'settings' => [
298
+ {
299
+ 'name' => 'apm',
300
+ 'enabled' => false,
301
+ },
302
+ ],
303
+ }
304
+ end
305
+
306
+ subject { described_class.new(project_id, data) }
307
+
308
+ it "returns a hash representation of settings" do
309
+ expect(described_class.new(project_id, data).to_h).to eq(data)
310
+ end
311
+
312
+ it "doesn't allow mutation of the original data object" do
313
+ hash = subject.to_h
314
+ hash['poll_sec'] = 0
315
+
316
+ expect(subject.to_h).to eq(
317
+ 'poll_sec' => 123,
318
+ 'settings' => [
319
+ {
320
+ 'name' => 'apm',
321
+ 'enabled' => false,
322
+ },
323
+ ],
324
+ )
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,212 @@
1
+ RSpec.describe Airbrake::RemoteSettings do
2
+ let(:project_id) { 123 }
3
+
4
+ let(:endpoint) do
5
+ "https://staging-notifier-configs.s3.amazonaws.com/2020-06-18/config/" \
6
+ "#{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
+ before do
29
+ stub_request(:get, endpoint).to_return(status: 200, body: body.to_json)
30
+
31
+ # Do not create config dumps on disk.
32
+ allow(Dir).to receive(:mkdir).with(config_dir)
33
+ allow(File).to receive(:write).with(config_path, anything)
34
+ end
35
+
36
+ describe ".poll" do
37
+ describe "config loading" do
38
+ let(:settings_data) { described_class::SettingsData.new(project_id, body) }
39
+
40
+ before do
41
+ allow(File).to receive(:exist?).with(config_path).and_return(true)
42
+ allow(File).to receive(:read).with(config_path).and_return(body.to_json)
43
+
44
+ allow(described_class::SettingsData).to receive(:new).and_return(settings_data)
45
+ end
46
+
47
+ it "loads the config from disk" do
48
+ expect(File).to receive(:read).with(config_path)
49
+ expect(settings_data).to receive(:merge!).with(body).twice
50
+
51
+ remote_settings = described_class.poll(project_id) {}
52
+ sleep(0.2)
53
+ remote_settings.stop_polling
54
+
55
+ expect(a_request(:get, endpoint)).to have_been_made.once
56
+ end
57
+
58
+ it "yields the config to the block twice" do
59
+ block = proc {}
60
+ expect(block).to receive(:call).twice
61
+
62
+ remote_settings = described_class.poll(project_id, &block)
63
+ sleep(0.2)
64
+ remote_settings.stop_polling
65
+
66
+ expect(a_request(:get, endpoint)).to have_been_made.once
67
+ end
68
+
69
+ context "when config loading fails" do
70
+ it "logs an error" do
71
+ expect(File).to receive(:read).and_raise(StandardError)
72
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
73
+ '**Airbrake: config loading failed: StandardError',
74
+ )
75
+
76
+ remote_settings = described_class.poll(project_id) {}
77
+ sleep(0.2)
78
+ remote_settings.stop_polling
79
+
80
+ expect(a_request(:get, endpoint)).to have_been_made.once
81
+ end
82
+ end
83
+ end
84
+
85
+ context "when no errors are raised" do
86
+ it "makes a request to AWS S3" do
87
+ remote_settings = described_class.poll(project_id) {}
88
+ sleep(0.1)
89
+ remote_settings.stop_polling
90
+
91
+ expect(a_request(:get, endpoint)).to have_been_made.at_least_once
92
+ end
93
+
94
+ it "fetches remote settings" do
95
+ settings = nil
96
+ remote_settings = described_class.poll(project_id) do |data|
97
+ settings = data
98
+ end
99
+ sleep(0.1)
100
+ remote_settings.stop_polling
101
+
102
+ expect(settings.error_notifications?).to eq(true)
103
+ expect(settings.performance_stats?).to eq(false)
104
+ expect(settings.interval).to eq(1)
105
+ end
106
+ end
107
+
108
+ context "when an error is raised while making a HTTP request" do
109
+ before do
110
+ allow(Net::HTTP).to receive(:get).and_raise(StandardError)
111
+ end
112
+
113
+ it "doesn't fetch remote settings" do
114
+ settings = nil
115
+ remote_settings = described_class.poll(project_id) do |data|
116
+ settings = data
117
+ end
118
+ sleep(0.1)
119
+ remote_settings.stop_polling
120
+
121
+ expect(a_request(:get, endpoint)).not_to have_been_made
122
+ expect(settings.interval).to eq(600)
123
+ end
124
+ end
125
+
126
+ context "when an error is raised while parsing returned JSON" do
127
+ before do
128
+ allow(JSON).to receive(:parse).and_raise(JSON::ParserError)
129
+ end
130
+
131
+ it "doesn't update settings data" do
132
+ settings = nil
133
+ remote_settings = described_class.poll(project_id) do |data|
134
+ settings = data
135
+ end
136
+ sleep(0.1)
137
+ remote_settings.stop_polling
138
+
139
+ expect(a_request(:get, endpoint)).to have_been_made.once
140
+ expect(settings.interval).to eq(600)
141
+ end
142
+ end
143
+
144
+ context "when API returns an XML response" do
145
+ before do
146
+ stub_request(:get, endpoint).to_return(status: 200, body: '<?xml ...')
147
+ end
148
+
149
+ it "doesn't update settings data" do
150
+ settings = nil
151
+ remote_settings = described_class.poll(project_id) do |data|
152
+ settings = data
153
+ end
154
+ sleep(0.1)
155
+ remote_settings.stop_polling
156
+
157
+ expect(a_request(:get, endpoint)).to have_been_made.once
158
+ expect(settings.interval).to eq(600)
159
+ end
160
+ end
161
+
162
+ context "when a config route is specified in the returned data" do
163
+ let(:new_endpoint) { 'http://example.com' }
164
+
165
+ let(:body) do
166
+ { 'config_route' => new_endpoint, 'poll_sec' => 0.1 }
167
+ end
168
+
169
+ before do
170
+ stub_request(:get, new_endpoint).to_return(status: 200, body: body.to_json)
171
+ end
172
+
173
+ it "makes the next request to the specified config route" do
174
+ remote_settings = described_class.poll(project_id) {}
175
+ sleep(0.2)
176
+
177
+ remote_settings.stop_polling
178
+
179
+ expect(a_request(:get, endpoint)).to have_been_made.once
180
+ expect(a_request(:get, new_endpoint)).to have_been_made.once
181
+ end
182
+ end
183
+ end
184
+
185
+ describe "#stop_polling" do
186
+ it "dumps config data to disk" do
187
+ expect(Dir).to receive(:mkdir).with(config_dir)
188
+ expect(File).to receive(:write).with(config_path, body.to_json)
189
+
190
+ remote_settings = described_class.poll(project_id) {}
191
+ sleep(0.2)
192
+ remote_settings.stop_polling
193
+
194
+ expect(a_request(:get, endpoint)).to have_been_made.once
195
+ end
196
+
197
+ context "when config dumping fails" do
198
+ it "logs an error" do
199
+ expect(File).to receive(:write).and_raise(StandardError)
200
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
201
+ '**Airbrake: config dumping failed: StandardError',
202
+ )
203
+
204
+ remote_settings = described_class.poll(project_id) {}
205
+ sleep(0.2)
206
+ remote_settings.stop_polling
207
+
208
+ expect(a_request(:get, endpoint)).to have_been_made.once
209
+ end
210
+ end
211
+ end
212
+ end