slangerq 0.6.1

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.
@@ -0,0 +1,43 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'active_support/json'
4
+ require 'active_support/core_ext/hash'
5
+ require 'eventmachine'
6
+ require 'em-http-request'
7
+ require 'pusher'
8
+ require 'thin'
9
+ require 'slanger_helper_methods'
10
+ require 'have_attributes'
11
+ require 'openssl'
12
+ require 'socket'
13
+ require 'timecop'
14
+ require 'pry'
15
+ require 'webmock/rspec'
16
+ require 'slanger'
17
+
18
+ WebMock.disable!
19
+
20
+ module Slanger; end
21
+
22
+ def errback
23
+ @errback ||= Proc.new { |e| fail 'cannot connect to slanger. your box might be too slow. try increasing sleep value in the before block' }
24
+ end
25
+
26
+ RSpec.configure do |config|
27
+ config.formatter = 'documentation'
28
+ config.color = true
29
+ config.mock_framework = :mocha
30
+ config.order = 'random'
31
+ config.include SlangerHelperMethods
32
+ config.fail_fast = true
33
+ config.after(:each) { stop_slanger if @server_pid }
34
+ config.before :all do
35
+ Pusher.tap do |p|
36
+ p.host = '0.0.0.0'
37
+ p.port = 4567
38
+ p.app_id = 'your-pusher-app-id'
39
+ p.secret = 'your-pusher-secret'
40
+ p.key = '765ec374ae0a69f4ce44'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'slanger'
3
+ require 'oj'
4
+
5
+ def clear_redis_connections
6
+ Slanger::Redis.instance_variables.each do |ivar|
7
+ Slanger::Redis.send :remove_instance_variable, ivar
8
+ end
9
+ end
10
+
11
+ describe 'Slanger::Channel' do
12
+ let(:channel) { Slanger::Channel.create channel_id: 'test' }
13
+
14
+ before(:each) do
15
+ EM::Hiredis.stubs(:connect).returns stub_everything('redis', :pubsub => stub_everything('redis'))
16
+ clear_redis_connections
17
+ end
18
+
19
+ after(:each) do
20
+ clear_redis_connections
21
+ EM::Hiredis.unstub(:connect)
22
+ end
23
+
24
+ describe '#unsubscribe' do
25
+ it 'decrements channel subscribers on Redis' do
26
+ Slanger::Redis.expects(:hincrby).
27
+ with('channel_subscriber_count', channel.channel_id, -1).
28
+ once.returns mock { expects(:callback).once.yields(2) }
29
+
30
+ channel.unsubscribe 1
31
+ end
32
+
33
+ it 'activates a webhook when the last subscriber of a channel unsubscribes' do
34
+ Slanger::Webhook.expects(:post).
35
+ with(name: 'channel_vacated', channel: channel.channel_id).
36
+ once
37
+
38
+ Slanger::Redis.expects(:hincrby).
39
+ with('channel_subscriber_count', channel.channel_id, -1).
40
+ times(3).returns mock {
41
+ expects(:callback).times(3).yields(2).then.yields(1).then.yields(0)
42
+ }
43
+
44
+ 3.times { |i| channel.unsubscribe i + 1 }
45
+ end
46
+ end
47
+
48
+ describe '#subscribe' do
49
+ it 'increments channel subscribers on Redis' do
50
+ Slanger::Redis.expects(:hincrby).
51
+ with('channel_subscriber_count', channel.channel_id, 1).
52
+ once.returns mock { expects(:callback).once.yields(2) }
53
+ channel.subscribe { |m| nil }
54
+ end
55
+
56
+ it 'activates a webhook when the first subscriber of a channel joins' do
57
+ Slanger::Webhook.expects(:post).
58
+ with(name: 'channel_occupied', channel: channel.channel_id).
59
+ once
60
+
61
+ Slanger::Redis.expects(:hincrby).
62
+ with('channel_subscriber_count', channel.channel_id, 1).
63
+ times(3).returns mock {
64
+ expects(:callback).times(3).yields(1).then.yields(2).then.yields(3)
65
+ }
66
+
67
+ 3.times { channel.subscribe { |m| nil } }
68
+ end
69
+ end
70
+
71
+ describe '#dispatch' do
72
+ it 'activates a webhook when client events are received' do
73
+ message = {
74
+ 'event' => 'client-test_event',
75
+ 'channel' => 'private-test_channel',
76
+ 'socket_id' => '8.422225',
77
+ 'data' => { 'key' => 'value' }
78
+ }
79
+
80
+ expected_params = message.merge({
81
+ 'name' => 'client_event',
82
+ 'data' => Oj.dump(message['data'])
83
+ })
84
+
85
+ Slanger::Webhook.expects(:post).
86
+ with(expected_params).
87
+ once
88
+
89
+ channel.dispatch(message, 'private-test_channel')
90
+ end
91
+
92
+ it 'does not activate a webhook when non-client event messages are received' do
93
+ message = {
94
+ 'event' => 'non_client_event',
95
+ 'channel' => 'private-test_channel',
96
+ 'socket_id' => '8.422225',
97
+ 'data' => { 'key' => 'value' }
98
+ }
99
+
100
+ Slanger::Webhook.expects(:post).never
101
+
102
+ channel.dispatch(message, 'private-test_channel')
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,72 @@
1
+ #encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Slanger::Api::RequestValidation do
5
+ describe '#socket_id' do
6
+ it 'validation' do
7
+ socket_id = "POST\n/apps/99759/events\nauth_key=840543d97de9803651b1&auth_timestamp=123&auth_version=1.0&body_md5=some_md5&dummy="
8
+
9
+ expect {validate(nil) }.not_to raise_error
10
+ expect {validate("1234.123455") }.not_to raise_error
11
+
12
+ expect {validate(socket_id) }.to raise_error Slanger::Api::InvalidRequest
13
+ expect {validate("something 123")}.to raise_error Slanger::Api::InvalidRequest
14
+
15
+ expect {validate("1234.12345 ") }.to raise_error Slanger::Api::InvalidRequest
16
+ expect {validate(" 1234.12345") }.to raise_error Slanger::Api::InvalidRequest
17
+ expect {validate("hello\n1234.123456\nhomakov") }.to raise_error Slanger::Api::InvalidRequest
18
+ end
19
+ end
20
+
21
+ before do
22
+ request = mock("request")
23
+ request.expects(:authenticate).times(0..2)
24
+ Signature::Request.expects(:new).times(0..2).returns request
25
+ end
26
+
27
+ describe "#channels" do
28
+ let(:body) { {socket_id: "1234.5678", channels: channels}.to_json }
29
+
30
+ context "with valid channels" do
31
+ let(:channels) { ["MY_CHANNEL", "presence-abcd", "foo-bar_1234@=,.;", "a"*164] }
32
+
33
+ it "returns an array of valid channel_id values" do
34
+ rv = Slanger::Api::RequestValidation.new(body, {}, "")
35
+
36
+ expect(rv.channels).to eq ["MY_CHANNEL", "presence-abcd", "foo-bar_1234@=,.;", "a"*164]
37
+ end
38
+ end
39
+
40
+ context "with invalid characters" do
41
+ let(:channels) { ["MY_CHANNEL:presence-abcd", "presence-abcd"] }
42
+
43
+ it "rejects invalid channels" do
44
+ expect{ Slanger::Api::RequestValidation.new(body, {}, "")}.to raise_error Slanger::Api::InvalidRequest
45
+ end
46
+ end
47
+
48
+ context "with invalid channel length" do
49
+ let(:channels) { ["a"*165] }
50
+
51
+ it "rejects names longer than 164 characters" do
52
+ expect{ Slanger::Api::RequestValidation.new(body, {}, "")}.to raise_error Slanger::Api::InvalidRequest
53
+ end
54
+ end
55
+ end
56
+
57
+ describe "#socket_id" do
58
+ it do
59
+ rv = Slanger::Api::RequestValidation.new(body("1234.5678"), {}, "")
60
+ expect(rv.socket_id).to eq "1234.5678"
61
+ end
62
+ end
63
+
64
+ def validate(socket_id)
65
+ Slanger::Api::RequestValidation.new(body(socket_id), {}, "").socket_id
66
+ end
67
+
68
+ def body(socket_id)
69
+ {socket_id: socket_id}.to_json
70
+ end
71
+ end
72
+
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'lib/slanger/webhook'
3
+
4
+ describe 'Slanger::Webhook' do
5
+
6
+ around do |example|
7
+ Slanger::Config.load webhook_url: 'https://example.com/pusher',
8
+ app_key: 'PUSHER_APP_KEY', secret: 'secret'
9
+ WebMock.enable!
10
+ Timecop.freeze(Time.now) { example.run }
11
+ WebMock.disable!
12
+ Slanger::Config.load webhook_url: nil
13
+ end
14
+
15
+ describe '.post' do
16
+ it 'make a POST request to the endpoint' do
17
+ payload = {
18
+ time_ms: Time.now.strftime('%s%L'),
19
+ events: [{ name: 'channel_occupied', channel: 'test channel' }]
20
+ }.to_json
21
+
22
+ digest = OpenSSL::Digest::SHA256.new
23
+ hmac = OpenSSL::HMAC.hexdigest(digest, Slanger::Config.secret, payload)
24
+
25
+ stub_request(:post, Slanger::Config.webhook_url).
26
+ with(body: payload, headers: {
27
+ "X-Pusher-Key" => Slanger::Config.app_key,
28
+ "X-Pusher-Signature" => hmac,
29
+ "Content-Type" => 'application/json'
30
+ }).
31
+ to_return(:status => 200, :body => {}.to_json, :headers => {})
32
+
33
+ Slanger::Webhook.post name: 'channel_occupied', channel: 'test channel'
34
+
35
+ expect(WebMock).to have_requested(:post, Slanger::Config.webhook_url).
36
+ with(body: payload, headers: {
37
+ "X-Pusher-Key" => Slanger::Config.app_key,
38
+ "X-Pusher-Signature" => hmac,
39
+ "Content-Type" => 'application/json'
40
+ })
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,392 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slangerq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
+ platform: ruby
6
+ authors:
7
+ - Stevie Graham
8
+ - Mark Burns
9
+ - KP
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2018-06-05 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: 1.0.0
29
+ - !ruby/object:Gem::Dependency
30
+ name: em-hiredis
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: 0.2.0
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: 0.2.0
43
+ - !ruby/object:Gem::Dependency
44
+ name: em-websocket
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: 0.5.1
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: 0.5.1
57
+ - !ruby/object:Gem::Dependency
58
+ name: rack
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 1.4.5
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 1.4.5
71
+ - !ruby/object:Gem::Dependency
72
+ name: rack-fiber_pool
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: 0.9.2
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 0.9.2
85
+ - !ruby/object:Gem::Dependency
86
+ name: signature
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: 0.1.6
92
+ type: :runtime
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: 0.1.6
99
+ - !ruby/object:Gem::Dependency
100
+ name: activesupport
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: 4.2.1
106
+ type: :runtime
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: 4.2.1
113
+ - !ruby/object:Gem::Dependency
114
+ name: sinatra
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: 1.4.4
120
+ type: :runtime
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: 1.4.4
127
+ - !ruby/object:Gem::Dependency
128
+ name: thin
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: 1.6.0
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: 1.6.0
141
+ - !ruby/object:Gem::Dependency
142
+ name: em-http-request
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: 0.3.0
148
+ type: :runtime
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: 0.3.0
155
+ - !ruby/object:Gem::Dependency
156
+ name: oj
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: 3.6.2
162
+ type: :runtime
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: 3.6.2
169
+ - !ruby/object:Gem::Dependency
170
+ name: rspec
171
+ requirement: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: 3.6.0
176
+ type: :development
177
+ prerelease: false
178
+ version_requirements: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - "~>"
181
+ - !ruby/object:Gem::Version
182
+ version: 3.6.0
183
+ - !ruby/object:Gem::Dependency
184
+ name: pusher
185
+ requirement: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - "~>"
188
+ - !ruby/object:Gem::Version
189
+ version: 0.14.2
190
+ type: :development
191
+ prerelease: false
192
+ version_requirements: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - "~>"
195
+ - !ruby/object:Gem::Version
196
+ version: 0.14.2
197
+ - !ruby/object:Gem::Dependency
198
+ name: haml
199
+ requirement: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - "~>"
202
+ - !ruby/object:Gem::Version
203
+ version: 3.1.2
204
+ type: :development
205
+ prerelease: false
206
+ version_requirements: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - "~>"
209
+ - !ruby/object:Gem::Version
210
+ version: 3.1.2
211
+ - !ruby/object:Gem::Dependency
212
+ name: timecop
213
+ requirement: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - "~>"
216
+ - !ruby/object:Gem::Version
217
+ version: 0.3.5
218
+ type: :development
219
+ prerelease: false
220
+ version_requirements: !ruby/object:Gem::Requirement
221
+ requirements:
222
+ - - "~>"
223
+ - !ruby/object:Gem::Version
224
+ version: 0.3.5
225
+ - !ruby/object:Gem::Dependency
226
+ name: webmock
227
+ requirement: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - "~>"
230
+ - !ruby/object:Gem::Version
231
+ version: 1.8.7
232
+ type: :development
233
+ prerelease: false
234
+ version_requirements: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - "~>"
237
+ - !ruby/object:Gem::Version
238
+ version: 1.8.7
239
+ - !ruby/object:Gem::Dependency
240
+ name: mocha
241
+ requirement: !ruby/object:Gem::Requirement
242
+ requirements:
243
+ - - "~>"
244
+ - !ruby/object:Gem::Version
245
+ version: 0.13.2
246
+ type: :development
247
+ prerelease: false
248
+ version_requirements: !ruby/object:Gem::Requirement
249
+ requirements:
250
+ - - "~>"
251
+ - !ruby/object:Gem::Version
252
+ version: 0.13.2
253
+ - !ruby/object:Gem::Dependency
254
+ name: pry
255
+ requirement: !ruby/object:Gem::Requirement
256
+ requirements:
257
+ - - "~>"
258
+ - !ruby/object:Gem::Version
259
+ version: 0.10.1
260
+ type: :development
261
+ prerelease: false
262
+ version_requirements: !ruby/object:Gem::Requirement
263
+ requirements:
264
+ - - "~>"
265
+ - !ruby/object:Gem::Version
266
+ version: 0.10.1
267
+ - !ruby/object:Gem::Dependency
268
+ name: pry-byebug
269
+ requirement: !ruby/object:Gem::Requirement
270
+ requirements:
271
+ - - "~>"
272
+ - !ruby/object:Gem::Version
273
+ version: 2.0.0
274
+ type: :development
275
+ prerelease: false
276
+ version_requirements: !ruby/object:Gem::Requirement
277
+ requirements:
278
+ - - "~>"
279
+ - !ruby/object:Gem::Version
280
+ version: 2.0.0
281
+ - !ruby/object:Gem::Dependency
282
+ name: bundler
283
+ requirement: !ruby/object:Gem::Requirement
284
+ requirements:
285
+ - - "~>"
286
+ - !ruby/object:Gem::Version
287
+ version: '1.5'
288
+ type: :development
289
+ prerelease: false
290
+ version_requirements: !ruby/object:Gem::Requirement
291
+ requirements:
292
+ - - "~>"
293
+ - !ruby/object:Gem::Version
294
+ version: '1.5'
295
+ - !ruby/object:Gem::Dependency
296
+ name: rake
297
+ requirement: !ruby/object:Gem::Requirement
298
+ requirements:
299
+ - - "~>"
300
+ - !ruby/object:Gem::Version
301
+ version: 10.4.2
302
+ type: :development
303
+ prerelease: false
304
+ version_requirements: !ruby/object:Gem::Requirement
305
+ requirements:
306
+ - - "~>"
307
+ - !ruby/object:Gem::Version
308
+ version: 10.4.2
309
+ description: A websocket service compatible with Pusher libraries
310
+ email:
311
+ - sjtgraham@mac.com
312
+ - markthedeveloper@gmail.com
313
+ executables:
314
+ - slanger
315
+ extensions: []
316
+ extra_rdoc_files: []
317
+ files:
318
+ - README.md
319
+ - bin/slanger
320
+ - lib/slanger/api.rb
321
+ - lib/slanger/api/event.rb
322
+ - lib/slanger/api/event_publisher.rb
323
+ - lib/slanger/api/request_validation.rb
324
+ - lib/slanger/api/server.rb
325
+ - lib/slanger/channel.rb
326
+ - lib/slanger/config.rb
327
+ - lib/slanger/connection.rb
328
+ - lib/slanger/handler.rb
329
+ - lib/slanger/logger.rb
330
+ - lib/slanger/presence_channel.rb
331
+ - lib/slanger/presence_subscription.rb
332
+ - lib/slanger/private_subscription.rb
333
+ - lib/slanger/redis.rb
334
+ - lib/slanger/service.rb
335
+ - lib/slanger/subscription.rb
336
+ - lib/slanger/version.rb
337
+ - lib/slanger/web_socket_server.rb
338
+ - lib/slanger/webhook.rb
339
+ - slanger.rb
340
+ - spec/have_attributes.rb
341
+ - spec/integration/channel_spec.rb
342
+ - spec/integration/integration_spec.rb
343
+ - spec/integration/presence_channel_spec.rb
344
+ - spec/integration/private_channel_spec.rb
345
+ - spec/integration/replaced_handler_spec.rb
346
+ - spec/integration/ssl_spec.rb
347
+ - spec/server.crt
348
+ - spec/server.key
349
+ - spec/slanger_helper_methods.rb
350
+ - spec/spec_helper.rb
351
+ - spec/unit/channel_spec.rb
352
+ - spec/unit/request_validation_spec.rb
353
+ - spec/unit/webhook_spec.rb
354
+ homepage: https://github.com/stevegraham/slanger
355
+ licenses:
356
+ - MIT
357
+ metadata: {}
358
+ post_install_message:
359
+ rdoc_options: []
360
+ require_paths:
361
+ - "."
362
+ required_ruby_version: !ruby/object:Gem::Requirement
363
+ requirements:
364
+ - - ">="
365
+ - !ruby/object:Gem::Version
366
+ version: 2.0.0
367
+ required_rubygems_version: !ruby/object:Gem::Requirement
368
+ requirements:
369
+ - - ">="
370
+ - !ruby/object:Gem::Version
371
+ version: '0'
372
+ requirements: []
373
+ rubyforge_project:
374
+ rubygems_version: 2.7.6
375
+ signing_key:
376
+ specification_version: 4
377
+ summary: A websocket service compatible with Pusher libraries
378
+ test_files:
379
+ - spec/have_attributes.rb
380
+ - spec/integration/channel_spec.rb
381
+ - spec/integration/integration_spec.rb
382
+ - spec/integration/presence_channel_spec.rb
383
+ - spec/integration/private_channel_spec.rb
384
+ - spec/integration/replaced_handler_spec.rb
385
+ - spec/integration/ssl_spec.rb
386
+ - spec/server.crt
387
+ - spec/server.key
388
+ - spec/slanger_helper_methods.rb
389
+ - spec/spec_helper.rb
390
+ - spec/unit/channel_spec.rb
391
+ - spec/unit/request_validation_spec.rb
392
+ - spec/unit/webhook_spec.rb