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,259 @@
1
+ RSpec.describe Airbrake::NoticeNotifier do
2
+ let(:project_id) { 105138 }
3
+ let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
4
+ let(:localhost) { 'http://localhost:8080' }
5
+
6
+ let(:endpoint) do
7
+ "https://api.airbrake.io/api/v3/projects/#{project_id}/notices"
8
+ end
9
+
10
+ let(:params) { {} }
11
+ let(:ex) { AirbrakeTestError.new }
12
+
13
+ before do
14
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
15
+
16
+ Airbrake::Config.instance = Airbrake::Config.new(
17
+ project_id: project_id,
18
+ project_key: project_key
19
+ )
20
+ end
21
+
22
+ describe "options" do
23
+ describe ":host" do
24
+ context "when custom" do
25
+ shared_examples 'endpoint' do |host, endpoint, title|
26
+ before { Airbrake::Config.instance.merge(host: host) }
27
+
28
+ example(title) do
29
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
30
+ subject.notify_sync(ex)
31
+
32
+ expect(a_request(:post, endpoint)).to have_been_made.once
33
+ end
34
+ end
35
+
36
+ path = '/api/v3/projects/105138/notices'
37
+
38
+ context "given a full host" do
39
+ include_examples('endpoint', localhost = 'http://localhost:8080',
40
+ URI.join(localhost, path),
41
+ "sends notices to the specified host's endpoint")
42
+ end
43
+
44
+ context "given a full host" do
45
+ include_examples('endpoint', localhost = 'http://localhost',
46
+ URI.join(localhost, path),
47
+ "assumes port 80 by default")
48
+ end
49
+
50
+ context "given a host without scheme" do
51
+ include_examples 'endpoint', localhost = 'localhost:8080',
52
+ URI.join("https://#{localhost}", path),
53
+ "assumes https by default"
54
+ end
55
+
56
+ context "given only hostname" do
57
+ include_examples 'endpoint', localhost = 'localhost',
58
+ URI.join("https://#{localhost}", path),
59
+ "assumes https and port 80 by default"
60
+ end
61
+ end
62
+ end
63
+
64
+ describe ":root_directory" do
65
+ before do
66
+ subject.add_filter(
67
+ Airbrake::Filters::RootDirectoryFilter.new('/home/kyrylo/code')
68
+ )
69
+ end
70
+
71
+ it "filters out frames" do
72
+ subject.notify_sync(ex)
73
+
74
+ expect(
75
+ a_request(:post, endpoint)
76
+ .with(body: %r|{"file":"/PROJECT_ROOT/airbrake/ruby/spec/airbrake_spec.+|)
77
+ ).to have_been_made.once
78
+ end
79
+
80
+ context "when present and is a" do
81
+ shared_examples 'root directory' do |dir|
82
+ before { Airbrake::Config.instance.merge(root_directory: dir) }
83
+
84
+ it "being included into the notice's payload" do
85
+ subject.notify_sync(ex)
86
+ expect(
87
+ a_request(:post, endpoint)
88
+ .with(body: %r{"rootDirectory":"/bingo/bango"})
89
+ ).to have_been_made.once
90
+ end
91
+ end
92
+
93
+ context "String" do
94
+ include_examples 'root directory', '/bingo/bango'
95
+ end
96
+
97
+ context "Pathname" do
98
+ include_examples 'root directory', Pathname.new('/bingo/bango')
99
+ end
100
+ end
101
+ end
102
+
103
+ describe ":proxy" do
104
+ let(:proxy) do
105
+ WEBrick::HTTPServer.new(
106
+ Port: 0,
107
+ Logger: WEBrick::Log.new('/dev/null'),
108
+ AccessLog: []
109
+ )
110
+ end
111
+
112
+ let(:requests) { Queue.new }
113
+
114
+ let(:proxy_params) do
115
+ { host: 'localhost',
116
+ port: proxy.config[:Port],
117
+ user: 'user',
118
+ password: 'password' }
119
+ end
120
+
121
+ before do
122
+ Airbrake::Config.instance.merge(
123
+ proxy: proxy_params,
124
+ host: "http://localhost:#{proxy.config[:Port]}"
125
+ )
126
+
127
+ proxy.mount_proc '/' do |req, res|
128
+ requests << req
129
+ res.status = 201
130
+ res.body = "OK\n"
131
+ end
132
+
133
+ Thread.new { proxy.start }
134
+ end
135
+
136
+ after { proxy.stop }
137
+
138
+ it "is being used if configured" do
139
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
140
+ skip(
141
+ "We use Webmock 2, which doesn't support Ruby 2.6+. It's " \
142
+ "safe to run this test on 2.6+ once we upgrade to Webmock 3.5+"
143
+ )
144
+ end
145
+ subject.notify_sync(ex)
146
+
147
+ proxied_request = requests.pop(true)
148
+
149
+ expect(proxied_request.header['proxy-authorization'].first)
150
+ .to eq('Basic dXNlcjpwYXNzd29yZA==')
151
+
152
+ # rubocop:disable Metrics/LineLength
153
+ expect(proxied_request.request_line)
154
+ .to eq("POST http://localhost:#{proxy.config[:Port]}/api/v3/projects/105138/notices HTTP/1.1\r\n")
155
+ # rubocop:enable Metrics/LineLength
156
+ end
157
+ end
158
+
159
+ describe ":environment" do
160
+ context "when present" do
161
+ before { Airbrake::Config.instance.merge(environment: :production) }
162
+
163
+ it "being included into the notice's payload" do
164
+ subject.notify_sync(ex)
165
+ expect(
166
+ a_request(:post, endpoint)
167
+ .with(body: /"context":{.*"environment":"production".*}/)
168
+ ).to have_been_made.once
169
+ end
170
+ end
171
+ end
172
+
173
+ describe ":ignore_environments" do
174
+ shared_examples 'sent notice' do |params|
175
+ before { Airbrake::Config.instance.merge(params) }
176
+
177
+ it "sends a notice" do
178
+ subject.notify_sync(ex)
179
+ expect(a_request(:post, endpoint)).to have_been_made
180
+ end
181
+ end
182
+
183
+ shared_examples 'ignored notice' do |params|
184
+ before { Airbrake::Config.instance.merge(params) }
185
+
186
+ it "ignores exceptions occurring in envs that were not configured" do
187
+ subject.notify_sync(ex)
188
+ expect(a_request(:post, endpoint)).not_to have_been_made
189
+ end
190
+ end
191
+
192
+ context "when env is set and ignore_environments doesn't mention it" do
193
+ params = {
194
+ environment: :development,
195
+ ignore_environments: [:production]
196
+ }
197
+
198
+ include_examples 'sent notice', params
199
+ end
200
+
201
+ context "when the current env and notify envs are the same" do
202
+ params = {
203
+ environment: :development,
204
+ ignore_environments: %i[production development]
205
+ }
206
+
207
+ include_examples 'ignored notice', params
208
+
209
+ it "returns early and doesn't try to parse the given exception" do
210
+ expect(Airbrake::Notice).not_to receive(:new)
211
+ expect(subject.notify_sync(ex))
212
+ .to eq('error' => "current environment 'development' is ignored")
213
+ expect(a_request(:post, endpoint)).not_to have_been_made
214
+ end
215
+ end
216
+
217
+ context "when the current env is not set and notify envs are present" do
218
+ params = { ignore_environments: %i[production development] }
219
+
220
+ include_examples 'sent notice', params
221
+ end
222
+
223
+ context "when the current env is set and notify envs aren't" do
224
+ include_examples 'sent notice', environment: :development
225
+ end
226
+
227
+ context "when ignore_environments specifies a Regexp pattern" do
228
+ params = {
229
+ environment: :testing,
230
+ ignore_environments: ['staging', /test.+/]
231
+ }
232
+
233
+ include_examples 'ignored notice', params
234
+ end
235
+ end
236
+
237
+ describe ":blacklist_keys" do
238
+ # Fixes https://github.com/airbrake/airbrake-ruby/issues/276
239
+ context "when specified along with :whitelist_keys" do
240
+ context "and when context payload is present" do
241
+ before do
242
+ Airbrake::Config.instance.merge(
243
+ blacklist_keys: %i[password password_confirmation],
244
+ whitelist_keys: [:email, /user/i, 'account_id']
245
+ )
246
+ end
247
+
248
+ it "sends a notice" do
249
+ notice = subject.build_notice(ex)
250
+ notice[:context][:headers] = 'banana'
251
+ subject.notify_sync(notice)
252
+
253
+ expect(a_request(:post, endpoint)).to have_been_made
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,296 @@
1
+ RSpec.describe Airbrake::Notice do
2
+ let(:notice) { described_class.new(AirbrakeTestError.new, bingo: '1') }
3
+
4
+ describe "#to_json" do
5
+ context "app_version" do
6
+ context "when missing" do
7
+ before { Airbrake::Config.instance.merge(app_version: nil) }
8
+
9
+ it "doesn't include app_version" do
10
+ expect(notice.to_json).not_to match(/"context":{"version":"1.2.3"/)
11
+ end
12
+ end
13
+
14
+ context "when present" do
15
+ let(:notice) { described_class.new(AirbrakeTestError.new) }
16
+
17
+ before do
18
+ Airbrake::Config.instance.merge(
19
+ app_version: "1.2.3",
20
+ root_directory: "/one/two"
21
+ )
22
+ end
23
+
24
+ it "includes app_version" do
25
+ expect(notice.to_json).to match(/"context":{"version":"1.2.3"/)
26
+ end
27
+
28
+ it "includes root_directory" do
29
+ expect(notice.to_json).to match(%r{"rootDirectory":"/one/two"})
30
+ end
31
+ end
32
+ end
33
+
34
+ context "when versions is empty" do
35
+ it "doesn't set the 'versions' payload" do
36
+ expect(notice.to_json).not_to match(
37
+ /"context":{"versions":{"dep":"1.2.3"}}/
38
+ )
39
+ end
40
+ end
41
+
42
+ context "when versions is not empty" do
43
+ it "sets the 'versions' payload" do
44
+ notice[:context][:versions] = { 'dep' => '1.2.3' }
45
+ expect(notice.to_json).to match(
46
+ /"context":{.*"versions":{"dep":"1.2.3"}.*}/
47
+ )
48
+ end
49
+ end
50
+
51
+ context "truncation" do
52
+ shared_examples 'payloads' do |size, msg|
53
+ it msg do
54
+ ex = AirbrakeTestError.new
55
+
56
+ backtrace = []
57
+ size.times { backtrace << "bin/rails:3:in `<main>'" }
58
+ ex.set_backtrace(backtrace)
59
+
60
+ notice = described_class.new(ex)
61
+
62
+ expect(notice.to_json.bytesize).to be < 64000
63
+ end
64
+ end
65
+
66
+ max_msg = 'truncates to the max allowed size'
67
+
68
+ context "with an extremely huge payload" do
69
+ include_examples 'payloads', 200_000, max_msg
70
+ end
71
+
72
+ context "with a big payload" do
73
+ include_examples 'payloads', 50_000, max_msg
74
+ end
75
+
76
+ small_msg = "doesn't truncate it"
77
+
78
+ context "with a small payload" do
79
+ include_examples 'payloads', 1000, small_msg
80
+ end
81
+
82
+ context "with a tiny payload" do
83
+ include_examples 'payloads', 300, small_msg
84
+ end
85
+
86
+ context "when truncation failed" do
87
+ it "returns nil" do
88
+ expect_any_instance_of(Airbrake::Truncator)
89
+ .to receive(:reduce_max_size).and_return(0)
90
+
91
+ encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
92
+ bad_string = Base64.decode64(encoded)
93
+
94
+ ex = AirbrakeTestError.new
95
+
96
+ backtrace = []
97
+ 10.times { backtrace << "bin/rails:3:in `<#{bad_string}>'" }
98
+ ex.set_backtrace(backtrace)
99
+
100
+ notice = described_class.new(ex)
101
+ expect(notice.to_json).to be_nil
102
+ end
103
+ end
104
+
105
+ describe "object replacement with its string version" do
106
+ let(:klass) { Class.new {} }
107
+ let(:ex) { AirbrakeTestError.new }
108
+ let(:params) { { bingo: [Object.new, klass.new] } }
109
+ let(:notice) { described_class.new(ex, params) }
110
+
111
+ before do
112
+ backtrace = []
113
+ backtrace_size.times { backtrace << "bin/rails:3:in `<main>'" }
114
+ ex.set_backtrace(backtrace)
115
+ end
116
+
117
+ context "with payload within the limits" do
118
+ let(:backtrace_size) { 1000 }
119
+
120
+ it "doesn't happen" do
121
+ expect(notice.to_json)
122
+ .to match(/bingo":\["#<Object:.+>","#<#<Class:.+>:.+>"/)
123
+ end
124
+ end
125
+
126
+ context "with payload bigger than the limit" do
127
+ context "with payload within the limits" do
128
+ let(:backtrace_size) { 50_000 }
129
+
130
+ it "happens" do
131
+ expect(notice.to_json)
132
+ .to match(/bingo":\[".+Object.+",".+Class.+"/)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ context "given a closed IO object" do
140
+ context "and when it is not monkey-patched by ActiveSupport" do
141
+ it "is not getting truncated" do
142
+ notice[:params] = { obj: IO.new(0).tap(&:close) }
143
+ expect(notice.to_json).to match(/"obj":"#<IO:0x.+>"/)
144
+ end
145
+ end
146
+
147
+ context "and when it is monkey-patched by ActiveSupport" do
148
+ # Instances of this class contain a closed IO object assigned to an
149
+ # instance variable. Normally, the JSON gem, which we depend on can
150
+ # parse closed IO objects. However, because ActiveSupport monkey-patches
151
+ # #to_json and calls #to_a on them, they raise IOError when we try to
152
+ # serialize them.
153
+ #
154
+ # @see https://goo.gl/0A3xNC
155
+ class ObjectWithIoIvars
156
+ def initialize
157
+ @bongo = Tempfile.new('bongo').tap(&:close)
158
+ end
159
+
160
+ # @raise [NotImplementedError] when inside a Rails environment
161
+ def to_json(*)
162
+ raise NotImplementedError
163
+ end
164
+ end
165
+
166
+ # @see ObjectWithIoIvars
167
+ class ObjectWithNestedIoIvars
168
+ def initialize
169
+ @bish = ObjectWithIoIvars.new
170
+ end
171
+
172
+ # @see ObjectWithIoIvars#to_json
173
+ def to_json(*)
174
+ raise NotImplementedError
175
+ end
176
+ end
177
+
178
+ context "and also when it's a closed Tempfile" do
179
+ it "doesn't fail" do
180
+ notice[:params] = { obj: Tempfile.new('bongo').tap(&:close) }
181
+ expect(notice.to_json).to match(/"obj":"#<(Temp)?file:0x.+>"/i)
182
+ end
183
+ end
184
+
185
+ context "and also when it's an IO ivar" do
186
+ it "doesn't fail" do
187
+ notice[:params] = { obj: ObjectWithIoIvars.new }
188
+ expect(notice.to_json).to match(/"obj":".+ObjectWithIoIvars.+"/)
189
+ end
190
+
191
+ context "and when it's deeply nested inside a hash" do
192
+ it "doesn't fail" do
193
+ notice[:params] = { a: { b: { c: ObjectWithIoIvars.new } } }
194
+ expect(notice.to_json).to match(
195
+ /"params":{"a":{"b":{"c":".+ObjectWithIoIvars.+"}}.*}/
196
+ )
197
+ end
198
+ end
199
+
200
+ context "and when it's deeply nested inside an array" do
201
+ it "doesn't fail" do
202
+ notice[:params] = { a: [[ObjectWithIoIvars.new]] }
203
+ expect(notice.to_json).to match(
204
+ /"params":{"a":\[\[".+ObjectWithIoIvars.+"\]\].*}/
205
+ )
206
+ end
207
+ end
208
+ end
209
+
210
+ context "and also when it's a non-IO ivar, which contains an IO ivar itself" do
211
+ it "doesn't fail" do
212
+ notice[:params] = { obj: ObjectWithNestedIoIvars.new }
213
+ expect(notice.to_json).to match(/"obj":".+ObjectWithNested.+"/)
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ it "overwrites the 'notifier' payload with the default values" do
220
+ notice[:notifier] = { name: 'bingo', bango: 'bongo' }
221
+
222
+ expect(notice.to_json)
223
+ .to match(/"notifier":{"name":"airbrake-ruby","version":".+","url":".+"}/)
224
+ end
225
+
226
+ it "always contains context/hostname" do
227
+ expect(notice.to_json)
228
+ .to match(/"context":{.*"hostname":".+".*}/)
229
+ end
230
+
231
+ it "defaults to the error severity" do
232
+ expect(notice.to_json).to match(/"context":{.*"severity":"error".*}/)
233
+ end
234
+
235
+ it "always contains environment/program_name" do
236
+ expect(notice.to_json)
237
+ .to match(%r|"environment":{"program_name":.+/rspec.*|)
238
+ end
239
+
240
+ it "contains errors" do
241
+ expect(notice.to_json)
242
+ .to match(/"errors":\[{"type":"AirbrakeTestError","message":"App crash/)
243
+ end
244
+
245
+ it "contains a backtrace" do
246
+ expect(notice.to_json)
247
+ .to match(%r|"backtrace":\[{"file":"/home/.+/spec/spec_helper.rb"|)
248
+ end
249
+
250
+ it "contains params" do
251
+ expect(notice.to_json).to match(/"params":{"bingo":"1"}/)
252
+ end
253
+ end
254
+
255
+ describe "#[]" do
256
+ it "accesses payload" do
257
+ expect(notice[:params]).to eq(bingo: '1')
258
+ end
259
+
260
+ it "raises error if notice is ignored" do
261
+ notice.ignore!
262
+ expect { notice[:params] }
263
+ .to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
264
+ end
265
+ end
266
+
267
+ describe "#[]=" do
268
+ it "sets a payload value" do
269
+ hash = { bingo: 'bango' }
270
+ notice[:params] = hash
271
+ expect(notice[:params]).to equal(hash)
272
+ end
273
+
274
+ it "raises error if notice is ignored" do
275
+ notice.ignore!
276
+ expect { notice[:params] = {} }
277
+ .to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
278
+ end
279
+
280
+ it "raises error when trying to assign unrecognized key" do
281
+ expect { notice[:bingo] = 1 }
282
+ .to raise_error(Airbrake::Error, /:bingo is not recognized among/)
283
+ end
284
+
285
+ it "raises when setting non-hash objects as the value" do
286
+ expect { notice[:params] = Object.new }
287
+ .to raise_error(Airbrake::Error, 'Got Object value, wanted a Hash')
288
+ end
289
+ end
290
+
291
+ describe "#stash" do
292
+ subject { described_class.new(AirbrakeTestError.new) }
293
+
294
+ it { is_expected.to respond_to(:stash) }
295
+ end
296
+ end