airbrake-ruby 4.15.0 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +22 -33
  3. data/lib/airbrake-ruby/async_sender.rb +1 -1
  4. data/lib/airbrake-ruby/backtrace.rb +6 -5
  5. data/lib/airbrake-ruby/config.rb +30 -30
  6. data/lib/airbrake-ruby/config/processor.rb +69 -0
  7. data/lib/airbrake-ruby/config/validator.rb +6 -0
  8. data/lib/airbrake-ruby/file_cache.rb +1 -1
  9. data/lib/airbrake-ruby/filter_chain.rb +1 -0
  10. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  11. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  12. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +1 -2
  13. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  14. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  15. data/lib/airbrake-ruby/filters/keys_filter.rb +21 -13
  16. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  17. data/lib/airbrake-ruby/filters/sql_filter.rb +4 -4
  18. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  19. data/lib/airbrake-ruby/filters/thread_filter.rb +2 -0
  20. data/lib/airbrake-ruby/ignorable.rb +1 -0
  21. data/lib/airbrake-ruby/notice.rb +1 -8
  22. data/lib/airbrake-ruby/notice_notifier.rb +1 -0
  23. data/lib/airbrake-ruby/performance_breakdown.rb +1 -6
  24. data/lib/airbrake-ruby/performance_notifier.rb +2 -15
  25. data/lib/airbrake-ruby/promise.rb +1 -0
  26. data/lib/airbrake-ruby/query.rb +1 -6
  27. data/lib/airbrake-ruby/queue.rb +1 -8
  28. data/lib/airbrake-ruby/remote_settings.rb +145 -0
  29. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  30. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  31. data/lib/airbrake-ruby/request.rb +1 -8
  32. data/lib/airbrake-ruby/stat.rb +1 -12
  33. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  34. data/lib/airbrake-ruby/tdigest.rb +2 -0
  35. data/lib/airbrake-ruby/thread_pool.rb +1 -0
  36. data/lib/airbrake-ruby/truncator.rb +8 -2
  37. data/lib/airbrake-ruby/version.rb +11 -1
  38. data/spec/airbrake_spec.rb +45 -22
  39. data/spec/backtrace_spec.rb +26 -26
  40. data/spec/code_hunk_spec.rb +2 -2
  41. data/spec/config/processor_spec.rb +125 -0
  42. data/spec/config/validator_spec.rb +18 -1
  43. data/spec/config_spec.rb +11 -32
  44. data/spec/filters/gem_root_filter_spec.rb +4 -4
  45. data/spec/filters/keys_allowlist_spec.rb +1 -0
  46. data/spec/filters/keys_blocklist_spec.rb +10 -0
  47. data/spec/filters/root_directory_filter_spec.rb +4 -4
  48. data/spec/filters/sql_filter_spec.rb +2 -2
  49. data/spec/notice_notifier/options_spec.rb +2 -2
  50. data/spec/notice_notifier_spec.rb +2 -2
  51. data/spec/notice_spec.rb +1 -1
  52. data/spec/performance_breakdown_spec.rb +0 -12
  53. data/spec/performance_notifier_spec.rb +0 -25
  54. data/spec/query_spec.rb +1 -11
  55. data/spec/queue_spec.rb +1 -13
  56. data/spec/remote_settings/callback_spec.rb +141 -0
  57. data/spec/remote_settings/settings_data_spec.rb +348 -0
  58. data/spec/remote_settings_spec.rb +230 -0
  59. data/spec/request_spec.rb +1 -13
  60. data/spec/spec_helper.rb +4 -4
  61. data/spec/stat_spec.rb +0 -9
  62. data/spec/sync_sender_spec.rb +3 -1
  63. metadata +18 -6
@@ -151,6 +151,7 @@ RSpec.describe Airbrake::Filters::KeysAllowlist do
151
151
  # thing. One is a Java exception, the other is a Ruby exception.
152
152
  # Likely a bug: https://github.com/jruby/jruby/issues/1903
153
153
  raise ex unless RUBY_ENGINE == 'jruby'
154
+
154
155
  expect { subject.call(notice) }.to raise_error(java.lang.StackOverflowError)
155
156
  end
156
157
  end
@@ -150,6 +150,16 @@ RSpec.describe Airbrake::Filters::KeysBlocklist do
150
150
  { bongo: { bish: '[Filtered]' } },
151
151
  ],
152
152
  )
153
+
154
+ it "doesn't mutate the original hash" do
155
+ params = { bongo: { bish: 'bash' } }
156
+ notice[:params] = params
157
+
158
+ blocklist = described_class.new([:bish])
159
+ blocklist.call(notice)
160
+
161
+ expect(params[:bongo][:bish]).to eq('bash')
162
+ end
153
163
  end
154
164
 
155
165
  context "and it is recursive" do
@@ -5,18 +5,18 @@ RSpec.describe Airbrake::Filters::RootDirectoryFilter do
5
5
  let(:notice) { Airbrake::Notice.new(AirbrakeTestError.new) }
6
6
 
7
7
  it "replaces root directory in the backtrace with a label" do
8
- # rubocop:disable Metrics/LineLength
8
+ # rubocop:disable Layout/LineLength
9
9
  notice[:errors].first[:backtrace] = [
10
10
  { file: "/home/kyrylo/code/airbrake/ruby/spec/spec_helper.rb" },
11
11
  { file: "#{root_directory}/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb " },
12
12
  { file: "/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb" },
13
13
  { file: "#{root_directory}/gems/rspec-core-3.3.2/exe/rspec" },
14
14
  ]
15
- # rubocop:enable Metrics/LineLength
15
+ # rubocop:enable Layout/LineLength
16
16
 
17
17
  subject.call(notice)
18
18
 
19
- # rubocop:disable Metrics/LineLength
19
+ # rubocop:disable Layout/LineLength
20
20
  expect(notice[:errors].first[:backtrace]).to(
21
21
  eq(
22
22
  [
@@ -27,7 +27,7 @@ RSpec.describe Airbrake::Filters::RootDirectoryFilter do
27
27
  ],
28
28
  ),
29
29
  )
30
- # rubocop:enable Metrics/LineLength
30
+ # rubocop:enable Layout/LineLength
31
31
  end
32
32
 
33
33
  it "does not filter file when it is nil" do
@@ -22,7 +22,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
22
22
 
23
23
  ALL_DIALECTS = %i[mysql postgres sqlite cassandra oracle].freeze
24
24
 
25
- # rubocop:disable Metrics/LineLength
25
+ # rubocop:disable Layout/LineLength
26
26
  [
27
27
  {
28
28
  input: 'SELECT * FROM things;',
@@ -229,7 +229,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
229
229
  ].each do |test|
230
230
  include_examples 'query filtering', test
231
231
  end
232
- # rubocop:enable Metrics/LineLength
232
+ # rubocop:enable Layout/LineLength
233
233
 
234
234
  [
235
235
  'COMMIT',
@@ -149,10 +149,10 @@ RSpec.describe Airbrake::NoticeNotifier do
149
149
  expect(proxied_request.header['proxy-authorization'].first)
150
150
  .to eq('Basic dXNlcjpwYXNzd29yZA==')
151
151
 
152
- # rubocop:disable Metrics/LineLength
152
+ # rubocop:disable Layout/LineLength
153
153
  expect(proxied_request.request_line)
154
154
  .to eq("POST http://localhost:#{proxy.config[:Port]}/api/v3/projects/105138/notices HTTP/1.1\r\n")
155
- # rubocop:enable Metrics/LineLength
155
+ # rubocop:enable Layout/LineLength
156
156
  end
157
157
  end
158
158
 
@@ -309,7 +309,7 @@ RSpec.describe Airbrake::NoticeNotifier do
309
309
 
310
310
  notice = subject.build_notice(Exception.new)
311
311
 
312
- # rubocop:disable Metrics/LineLength
312
+ # rubocop:disable Layout/LineLength
313
313
  expect(notice[:errors].first[:backtrace]).to eq(
314
314
  [
315
315
  { file: 'org/jruby/RubyKernel.java', line: 998, function: 'eval' },
@@ -317,7 +317,7 @@ RSpec.describe Airbrake::NoticeNotifier do
317
317
  { file: '/ruby/stdlib/irb.rb:489', line: 489, function: 'block in eval_input' },
318
318
  ],
319
319
  )
320
- # rubocop:enable Metrics/LineLength
320
+ # rubocop:enable Layout/LineLength
321
321
  end
322
322
  end
323
323
 
@@ -268,7 +268,7 @@ RSpec.describe Airbrake::Notice do
268
268
  it "sets a payload value" do
269
269
  hash = { bingo: 'bango' }
270
270
  notice[:params] = hash
271
- expect(notice[:params]).to equal(hash)
271
+ expect(notice[:params]).to eq(hash)
272
272
  end
273
273
 
274
274
  it "raises error if notice is ignored" do
@@ -3,21 +3,9 @@ RSpec.describe Airbrake::PerformanceBreakdown do
3
3
  subject do
4
4
  described_class.new(
5
5
  method: 'GET', route: '/', response_type: '', groups: {},
6
- start_time: Time.now
7
6
  )
8
7
  end
9
8
 
10
9
  it { is_expected.to respond_to(:stash) }
11
10
  end
12
-
13
- describe "#end_time" do
14
- it "is always equal to start_time + 1 second by default" do
15
- time = Time.now
16
- performance_breakdown = described_class.new(
17
- method: 'GET', route: '/', response_type: '', groups: {},
18
- start_time: time
19
- )
20
- expect(performance_breakdown.end_time).to eq(time + 1)
21
- end
22
- end
23
11
  end
@@ -541,31 +541,6 @@ RSpec.describe Airbrake::PerformanceNotifier do
541
541
  end
542
542
  end
543
543
 
544
- context "when :start_time is specified (deprecated)" do
545
- before do
546
- allow(Kernel).to receive(:warn)
547
- end
548
-
549
- it "uses the value of :start_time to update stat" do
550
- subject.notify(
551
- Airbrake::Query.new(
552
- method: 'POST',
553
- route: '/foo',
554
- query: 'SELECT * FROM things',
555
- start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
556
- end_time: Time.new(2018, 1, 1, 0, 50, 0, 0),
557
- ),
558
- )
559
- subject.close
560
-
561
- expect(
562
- a_request(:put, queries).with(
563
- body: /"count":1,"sum":60000.0,"sumsq":3600000000.0/,
564
- ),
565
- ).to have_been_made
566
- end
567
- end
568
-
569
544
  context "when provided :timing is zero" do
570
545
  it "doesn't notify" do
571
546
  queue = Airbrake::Queue.new(queue: 'bananas', error_count: 0, timing: 0)
@@ -2,20 +2,10 @@ RSpec.describe Airbrake::Query do
2
2
  describe "#stash" do
3
3
  subject do
4
4
  described_class.new(
5
- method: 'GET', route: '/', query: '', start_time: Time.now,
5
+ method: 'GET', route: '/', query: '',
6
6
  )
7
7
  end
8
8
 
9
9
  it { is_expected.to respond_to(:stash) }
10
10
  end
11
-
12
- describe "#end_time" do
13
- it "is always equal to start_time + 1 second by default" do
14
- time = Time.now
15
- query = described_class.new(
16
- method: 'GET', route: '/', query: '', start_time: time,
17
- )
18
- expect(query.end_time).to eq(time + 1)
19
- end
20
- end
21
11
  end
@@ -9,21 +9,9 @@ RSpec.describe Airbrake::Queue do
9
9
  it { is_expected.to respond_to(:stash) }
10
10
  end
11
11
 
12
- describe "#end_time" do
13
- it "is always equal to start_time + 1 second by default" do
14
- time = Time.now
15
- queue = described_class.new(
16
- queue: 'bananas', error_count: 0, start_time: time,
17
- )
18
- expect(queue.end_time).to eq(time + 1)
19
- end
20
- end
21
-
22
12
  describe "#route" do
23
13
  it "always returns an empty route" do
24
- queue = described_class.new(
25
- queue: 'a', error_count: 0, start_time: Time.now,
26
- )
14
+ queue = described_class.new(queue: 'a', error_count: 0)
27
15
  expect(queue.route).to be_empty
28
16
  end
29
17
  end
@@ -0,0 +1,141 @@
1
+ RSpec.describe Airbrake::RemoteSettings::Callback do
2
+ describe "#call" do
3
+ let(:logger) { Logger.new(File::NULL) }
4
+
5
+ let(:config) do
6
+ Airbrake::Config.new(
7
+ project_id: 123,
8
+ logger: logger,
9
+ )
10
+ end
11
+
12
+ let(:data) do
13
+ instance_double(Airbrake::RemoteSettings::SettingsData)
14
+ end
15
+
16
+ before do
17
+ allow(data).to receive(:to_h)
18
+ allow(data).to receive(:error_host)
19
+ allow(data).to receive(:apm_host)
20
+ allow(data).to receive(:error_notifications?)
21
+ allow(data).to receive(:performance_stats?)
22
+ end
23
+
24
+ it "logs given data" do
25
+ expect(logger).to receive(:debug).with(/applying remote settings/)
26
+ described_class.new(config).call(data)
27
+ end
28
+
29
+ context "when the config disables error notifications" do
30
+ before do
31
+ config.error_notifications = false
32
+ allow(data).to receive(:error_notifications?).and_return(true)
33
+ end
34
+
35
+ it "keeps the option disabled forever" do
36
+ callback = described_class.new(config)
37
+
38
+ callback.call(data)
39
+ expect(config.error_notifications).to eq(false)
40
+
41
+ callback.call(data)
42
+ expect(config.error_notifications).to eq(false)
43
+
44
+ callback.call(data)
45
+ expect(config.error_notifications).to eq(false)
46
+ end
47
+ end
48
+
49
+ context "when the config enables error notifications" do
50
+ before { config.error_notifications = true }
51
+
52
+ it "can disable and enable error notifications" do
53
+ expect(data).to receive(:error_notifications?).and_return(false)
54
+
55
+ callback = described_class.new(config)
56
+ callback.call(data)
57
+ expect(config.error_notifications).to eq(false)
58
+
59
+ expect(data).to receive(:error_notifications?).and_return(true)
60
+ callback.call(data)
61
+ expect(config.error_notifications).to eq(true)
62
+ end
63
+ end
64
+
65
+ context "when the config disables performance_stats" do
66
+ before do
67
+ config.performance_stats = false
68
+ allow(data).to receive(:performance_stats?).and_return(true)
69
+ end
70
+
71
+ it "keeps the option disabled forever" do
72
+ callback = described_class.new(config)
73
+
74
+ callback.call(data)
75
+ expect(config.performance_stats).to eq(false)
76
+
77
+ callback.call(data)
78
+ expect(config.performance_stats).to eq(false)
79
+
80
+ callback.call(data)
81
+ expect(config.performance_stats).to eq(false)
82
+ end
83
+ end
84
+
85
+ context "when the config enables performance stats" do
86
+ before { config.performance_stats = true }
87
+
88
+ it "can disable and enable performance_stats" do
89
+ expect(data).to receive(:performance_stats?).and_return(false)
90
+
91
+ callback = described_class.new(config)
92
+ callback.call(data)
93
+ expect(config.performance_stats).to eq(false)
94
+
95
+ expect(data).to receive(:performance_stats?).and_return(true)
96
+ callback.call(data)
97
+ expect(config.performance_stats).to eq(true)
98
+ end
99
+ end
100
+
101
+ context "when error_host returns a value" do
102
+ it "sets the error_host option" do
103
+ config.error_host = 'http://api.airbrake.io'
104
+ allow(data).to receive(:error_host).and_return('https://api.example.com')
105
+
106
+ described_class.new(config).call(data)
107
+ expect(config.error_host).to eq('https://api.example.com')
108
+ end
109
+ end
110
+
111
+ context "when error_host returns nil" do
112
+ it "doesn't modify the error_host option" do
113
+ config.error_host = 'http://api.airbrake.io'
114
+ allow(data).to receive(:error_host).and_return(nil)
115
+
116
+ described_class.new(config).call(data)
117
+ expect(config.error_host).to eq('http://api.airbrake.io')
118
+ end
119
+ end
120
+
121
+ context "when apm_host returns a value" do
122
+ it "sets the apm_host option" do
123
+ config.apm_host = 'http://api.airbrake.io'
124
+ allow(data).to receive(:apm_host).and_return('https://api.example.com')
125
+
126
+ described_class.new(config).call(data)
127
+ expect(config.apm_host).to eq('https://api.example.com')
128
+ end
129
+ end
130
+
131
+ context "when apm_host returns nil" do
132
+ it "doesn't modify the apm_host option" do
133
+ config.apm_host = 'http://api.airbrake.io'
134
+ allow(data).to receive(:apm_host).and_return(nil)
135
+
136
+ described_class.new(config).call(data)
137
+ expect(config.apm_host).to eq('http://api.airbrake.io')
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,348 @@
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')
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) { 'http://example.com/' }
64
+
65
+ context "when remote config specifies a config route" do
66
+ let(:data) do
67
+ { 'config_route' => '123/cfg/321/cfg.json' }
68
+ end
69
+
70
+ it "returns the config route with the provided location" do
71
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
72
+ 'http://example.com/123/cfg/321/cfg.json',
73
+ )
74
+ end
75
+ end
76
+
77
+ context "when remote config DOES NOT specify a config route" do
78
+ it "returns the config route with the default location" do
79
+ expect(described_class.new(project_id, {}).config_route(host)).to eq(
80
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
81
+ )
82
+ end
83
+ end
84
+
85
+ context "when a config route is specified but is set to nil" do
86
+ let(:data) do
87
+ { 'config_route' => nil }
88
+ end
89
+
90
+ it "returns the config route with the default location" do
91
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
92
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
93
+ )
94
+ end
95
+ end
96
+
97
+ context "when a config route is specified but is set to an empty string" do
98
+ let(:data) do
99
+ { 'config_route' => '' }
100
+ end
101
+
102
+ it "returns the route with the default instead" do
103
+ expect(described_class.new(project_id, data).config_route(host)).to eq(
104
+ "http://example.com/2020-06-18/config/#{project_id}/config.json",
105
+ )
106
+ end
107
+ end
108
+ end
109
+
110
+ describe "#error_notifications?" do
111
+ context "when the 'errors' setting is present" do
112
+ context "and when it is enabled" do
113
+ let(:data) do
114
+ {
115
+ 'settings' => [
116
+ {
117
+ 'name' => 'errors',
118
+ 'enabled' => true,
119
+ },
120
+ ],
121
+ }
122
+ end
123
+
124
+ it "returns true" do
125
+ expect(described_class.new(project_id, data).error_notifications?)
126
+ .to eq(true)
127
+ end
128
+ end
129
+
130
+ context "and when it is disabled" do
131
+ let(:data) do
132
+ {
133
+ 'settings' => [
134
+ {
135
+ 'name' => 'errors',
136
+ 'enabled' => false,
137
+ },
138
+ ],
139
+ }
140
+ end
141
+
142
+ it "returns false" do
143
+ expect(described_class.new(project_id, data).error_notifications?)
144
+ .to eq(false)
145
+ end
146
+ end
147
+ end
148
+
149
+ context "when the 'errors' setting is missing" do
150
+ let(:data) do
151
+ { 'settings' => [] }
152
+ end
153
+
154
+ it "returns true" do
155
+ expect(described_class.new(project_id, data).error_notifications?)
156
+ .to eq(true)
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "#performance_stats?" do
162
+ context "when the 'apm' setting is present" do
163
+ context "and when it is enabled" do
164
+ let(:data) do
165
+ {
166
+ 'settings' => [
167
+ {
168
+ 'name' => 'apm',
169
+ 'enabled' => true,
170
+ },
171
+ ],
172
+ }
173
+ end
174
+
175
+ it "returns true" do
176
+ expect(described_class.new(project_id, data).performance_stats?)
177
+ .to eq(true)
178
+ end
179
+ end
180
+
181
+ context "and when it is disabled" do
182
+ let(:data) do
183
+ {
184
+ 'settings' => [
185
+ {
186
+ 'name' => 'apm',
187
+ 'enabled' => false,
188
+ },
189
+ ],
190
+ }
191
+ end
192
+
193
+ it "returns false" do
194
+ expect(described_class.new(project_id, data).performance_stats?)
195
+ .to eq(false)
196
+ end
197
+ end
198
+ end
199
+
200
+ context "when the 'apm' setting is missing" do
201
+ let(:data) do
202
+ { 'settings' => [] }
203
+ end
204
+
205
+ it "returns true" do
206
+ expect(described_class.new(project_id, data).performance_stats?)
207
+ .to eq(true)
208
+ end
209
+ end
210
+ end
211
+
212
+ describe "#error_host" do
213
+ context "when the 'errors' setting is present" do
214
+ context "and when 'endpoint' is specified" do
215
+ let(:endpoint) { 'https://api.example.com/' }
216
+
217
+ let(:data) do
218
+ {
219
+ 'settings' => [
220
+ {
221
+ 'name' => 'errors',
222
+ 'enabled' => true,
223
+ 'endpoint' => endpoint,
224
+ },
225
+ ],
226
+ }
227
+ end
228
+
229
+ it "returns the endpoint" do
230
+ expect(described_class.new(project_id, data).error_host).to eq(endpoint)
231
+ end
232
+ end
233
+
234
+ context "and when an endpoint is NOT specified" do
235
+ let(:data) do
236
+ {
237
+ 'settings' => [
238
+ {
239
+ 'name' => 'errors',
240
+ 'enabled' => true,
241
+ },
242
+ ],
243
+ }
244
+ end
245
+
246
+ it "returns nil" do
247
+ expect(described_class.new(project_id, data).error_host).to be_nil
248
+ end
249
+ end
250
+ end
251
+
252
+ context "when the 'errors' setting is missing" do
253
+ let(:data) do
254
+ { 'settings' => [] }
255
+ end
256
+
257
+ it "returns nil" do
258
+ expect(described_class.new(project_id, data).error_host).to be_nil
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "#apm_host" do
264
+ context "when the 'apm' setting is present" do
265
+ context "and when 'endpoint' is specified" do
266
+ let(:endpoint) { 'https://api.example.com/' }
267
+
268
+ let(:data) do
269
+ {
270
+ 'settings' => [
271
+ {
272
+ 'name' => 'apm',
273
+ 'enabled' => true,
274
+ 'endpoint' => endpoint,
275
+ },
276
+ ],
277
+ }
278
+ end
279
+
280
+ it "returns the endpoint" do
281
+ expect(described_class.new(project_id, data).apm_host).to eq(endpoint)
282
+ end
283
+ end
284
+
285
+ context "and when an endpoint is NOT specified" do
286
+ let(:data) do
287
+ {
288
+ 'settings' => [
289
+ {
290
+ 'name' => 'apm',
291
+ 'enabled' => true,
292
+ },
293
+ ],
294
+ }
295
+ end
296
+
297
+ it "returns nil" do
298
+ expect(described_class.new(project_id, data).apm_host).to be_nil
299
+ end
300
+ end
301
+ end
302
+
303
+ context "when the 'apm' setting is missing" do
304
+ let(:data) do
305
+ { 'settings' => [] }
306
+ end
307
+
308
+ it "returns nil" do
309
+ expect(described_class.new(project_id, data).apm_host).to be_nil
310
+ end
311
+ end
312
+ end
313
+
314
+ describe "#to_h" do
315
+ let(:data) do
316
+ {
317
+ 'poll_sec' => 123,
318
+ 'settings' => [
319
+ {
320
+ 'name' => 'apm',
321
+ 'enabled' => false,
322
+ },
323
+ ],
324
+ }
325
+ end
326
+
327
+ subject { described_class.new(project_id, data) }
328
+
329
+ it "returns a hash representation of settings" do
330
+ expect(described_class.new(project_id, data).to_h).to eq(data)
331
+ end
332
+
333
+ it "doesn't allow mutation of the original data object" do
334
+ hash = subject.to_h
335
+ hash['poll_sec'] = 0
336
+
337
+ expect(subject.to_h).to eq(
338
+ 'poll_sec' => 123,
339
+ 'settings' => [
340
+ {
341
+ 'name' => 'apm',
342
+ 'enabled' => false,
343
+ },
344
+ ],
345
+ )
346
+ end
347
+ end
348
+ end