vcr_stripe_webhook 0.3.0
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +12 -0
- data/lib/vcr_stripe_webhook/configuration.rb +22 -0
- data/lib/vcr_stripe_webhook/error.rb +12 -0
- data/lib/vcr_stripe_webhook/event.rb +33 -0
- data/lib/vcr_stripe_webhook/event_cassette.rb +156 -0
- data/lib/vcr_stripe_webhook/event_receiver.rb +117 -0
- data/lib/vcr_stripe_webhook/server.rb +135 -0
- data/lib/vcr_stripe_webhook/stripe_cli.rb +97 -0
- data/lib/vcr_stripe_webhook/version.rb +5 -0
- data/lib/vcr_stripe_webhook/waiter.rb +39 -0
- data/lib/vcr_stripe_webhook.rb +56 -0
- data/sig/vcr_stripe_webhook.rbs +4 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3a8235b3c64f4d6c484b72c9338943983f32edbef036d4490099792ebb5613ac
|
|
4
|
+
data.tar.gz: 6c772cedb526c96fd06a4eab52be8892d5a1884a33bddf9455537e998592eca4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ea08bd8d64a7c372365dc7a8b42903f82acda35a195b8e1df31025d68dedb4a7929883c723b0e38e26ccaf5cd32b25efe6fff450b4934a7344bb249387074ec2
|
|
7
|
+
data.tar.gz: 8cce67fa17d558e942a744c29f9201af74c6d3584688229205ecd6967284f9267d540fd22ef078036b5f3f6d9d57f3987b96e136e8e7b2b26c8ef94a655268d2
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.0
|
|
3
|
+
|
|
4
|
+
Style/StringLiterals:
|
|
5
|
+
Enabled: true
|
|
6
|
+
EnforcedStyle: double_quotes
|
|
7
|
+
|
|
8
|
+
Style/StringLiteralsInInterpolation:
|
|
9
|
+
Enabled: true
|
|
10
|
+
EnforcedStyle: double_quotes
|
|
11
|
+
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Layout/LineLength:
|
|
16
|
+
Max: 120
|
|
17
|
+
|
|
18
|
+
Metrics/AbcSize:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
21
|
+
Metrics/MethodLength:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
Metrics/CyclomaticComplexity:
|
|
25
|
+
Enabled: false
|
|
26
|
+
|
|
27
|
+
Metrics/BlockLength:
|
|
28
|
+
Enabled: false
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.0.6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.3.0] - 2024-12-01
|
|
4
|
+
|
|
5
|
+
- Update gem dependencies
|
|
6
|
+
- Fix typo
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2023-10-16
|
|
9
|
+
|
|
10
|
+
- Add new methods
|
|
11
|
+
- `VcrStripeWebhook.receive_webhook_event`
|
|
12
|
+
- `VcrStripeWebhook.receive_webhook_events`
|
|
13
|
+
|
|
14
|
+
### Breaking changes
|
|
15
|
+
|
|
16
|
+
- Remove `VcrStripeWebhook.receive_webhook`
|
|
17
|
+
|
|
18
|
+
## [0.1.0] - 2023-10-05
|
|
19
|
+
|
|
20
|
+
- Initial release
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
vcr_stripe_webhook (0.3.0)
|
|
5
|
+
stripe (>= 9.0)
|
|
6
|
+
vcr (~> 6.0)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
addressable (2.8.7)
|
|
12
|
+
public_suffix (>= 2.0.2, < 7.0)
|
|
13
|
+
ast (2.4.2)
|
|
14
|
+
base64 (0.2.0)
|
|
15
|
+
bigdecimal (3.1.8)
|
|
16
|
+
crack (1.0.0)
|
|
17
|
+
bigdecimal
|
|
18
|
+
rexml
|
|
19
|
+
diff-lcs (1.5.1)
|
|
20
|
+
dotenv (3.1.4)
|
|
21
|
+
hashdiff (1.1.2)
|
|
22
|
+
json (2.8.2)
|
|
23
|
+
language_server-protocol (3.17.0.3)
|
|
24
|
+
parallel (1.26.3)
|
|
25
|
+
parser (3.3.6.0)
|
|
26
|
+
ast (~> 2.4.1)
|
|
27
|
+
racc
|
|
28
|
+
public_suffix (6.0.1)
|
|
29
|
+
racc (1.8.1)
|
|
30
|
+
rainbow (3.1.1)
|
|
31
|
+
rake (13.2.1)
|
|
32
|
+
regexp_parser (2.9.3)
|
|
33
|
+
rexml (3.3.9)
|
|
34
|
+
rspec (3.13.0)
|
|
35
|
+
rspec-core (~> 3.13.0)
|
|
36
|
+
rspec-expectations (~> 3.13.0)
|
|
37
|
+
rspec-mocks (~> 3.13.0)
|
|
38
|
+
rspec-core (3.13.2)
|
|
39
|
+
rspec-support (~> 3.13.0)
|
|
40
|
+
rspec-expectations (3.13.3)
|
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
+
rspec-support (~> 3.13.0)
|
|
43
|
+
rspec-mocks (3.13.2)
|
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
45
|
+
rspec-support (~> 3.13.0)
|
|
46
|
+
rspec-support (3.13.1)
|
|
47
|
+
rubocop (1.69.0)
|
|
48
|
+
json (~> 2.3)
|
|
49
|
+
language_server-protocol (>= 3.17.0)
|
|
50
|
+
parallel (~> 1.10)
|
|
51
|
+
parser (>= 3.3.0.2)
|
|
52
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
53
|
+
regexp_parser (>= 2.4, < 3.0)
|
|
54
|
+
rubocop-ast (>= 1.36.1, < 2.0)
|
|
55
|
+
ruby-progressbar (~> 1.7)
|
|
56
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
57
|
+
rubocop-ast (1.36.2)
|
|
58
|
+
parser (>= 3.3.1.0)
|
|
59
|
+
ruby-progressbar (1.13.0)
|
|
60
|
+
stripe (13.2.0)
|
|
61
|
+
unicode-display_width (3.1.2)
|
|
62
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
|
63
|
+
unicode-emoji (4.0.4)
|
|
64
|
+
vcr (6.3.1)
|
|
65
|
+
base64
|
|
66
|
+
webmock (3.24.0)
|
|
67
|
+
addressable (>= 2.8.0)
|
|
68
|
+
crack (>= 0.3.2)
|
|
69
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
|
70
|
+
|
|
71
|
+
PLATFORMS
|
|
72
|
+
x86_64-linux
|
|
73
|
+
|
|
74
|
+
DEPENDENCIES
|
|
75
|
+
dotenv (~> 3.0)
|
|
76
|
+
rake
|
|
77
|
+
rspec
|
|
78
|
+
rubocop
|
|
79
|
+
vcr_stripe_webhook!
|
|
80
|
+
webmock (~> 3.0)
|
|
81
|
+
|
|
82
|
+
BUNDLED WITH
|
|
83
|
+
2.4.20
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Shunichi Ikegami
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# VcrStripeWebhook
|
|
2
|
+
|
|
3
|
+
Record and replay Stripe webhooks during integration test.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [vcr gem](https://github.com/vcr/vcr)
|
|
8
|
+
- [Stripe CLI](https://stripe.com/docs/stripe-cli)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add this gem to Gemfile.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
gem 'vcr_stripe_webhook', github: 'shunichi/vcr_stripe_webhook'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Set Stripe API key for test mode
|
|
22
|
+
Stripe.api_key = 'sk_test_...'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use `VcrStripeWebhook.use_cassette` instead of `VCR.use_cassette`.
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# This method wraps VCR.use_cassette
|
|
29
|
+
VcrStripeWebhook.use_cassette('create_customer') do |vcr_cassette|
|
|
30
|
+
stripe_customer = nil
|
|
31
|
+
|
|
32
|
+
# Get webhook payload from Stripe server or current cassette
|
|
33
|
+
webhook_event = VcrStripeWebhook.receive_webhook_event('customer.created') do
|
|
34
|
+
customer_params = {
|
|
35
|
+
email: 'test-user@example.com',
|
|
36
|
+
name: 'test-user',
|
|
37
|
+
}
|
|
38
|
+
stripe_customer = Stripe::Customer.create(customer_params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Call your webhook manually
|
|
42
|
+
post your_stripe_webhook_path, params: webhook_event.as_json,
|
|
43
|
+
headers: { 'Content-Type: application/json' }
|
|
44
|
+
|
|
45
|
+
# Insert assertion here
|
|
46
|
+
ensure
|
|
47
|
+
stripe_customer&.delete
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
On the first run, this code records HTTP requests/responses with VCR gem and additionally records received webhooks with vcr_stripe_webhook gem.
|
|
52
|
+
On the second run or later, it replays the recorded requests, responses and webhooks.
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
57
|
+
|
|
58
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
59
|
+
|
|
60
|
+
## Contributing
|
|
61
|
+
|
|
62
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shunichi/vcr_stripe_webhook.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VcrStripeWebhook
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_TIMEOUT_SEC = 5
|
|
6
|
+
|
|
7
|
+
attr_writer :timeout, :cassette_library_dir, :stripe_api_key
|
|
8
|
+
|
|
9
|
+
def timeout
|
|
10
|
+
@timeout ||= DEFAULT_TIMEOUT_SEC
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def cassette_library_dir
|
|
14
|
+
@cassette_library_dir ||=
|
|
15
|
+
File.join(VCR.configuration.cassette_library_dir, "stripe_webhooks")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stripe_api_key
|
|
19
|
+
@stripe_api_key ||= Stripe.api_key
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VcrStripeWebhook
|
|
4
|
+
class Event
|
|
5
|
+
attr_reader :type
|
|
6
|
+
|
|
7
|
+
def initialize(type, event_hash)
|
|
8
|
+
@type = type
|
|
9
|
+
@event_hash = event_hash
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_h
|
|
13
|
+
{
|
|
14
|
+
"type" => type,
|
|
15
|
+
"value" => as_hash
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def as_hash
|
|
20
|
+
@event_hash
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def as_json
|
|
24
|
+
@json = JSON.generate(@event_hash)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def from_hash(hash)
|
|
29
|
+
Event.new(hash["type"], hash["value"])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "event"
|
|
6
|
+
require_relative "error"
|
|
7
|
+
|
|
8
|
+
module VcrStripeWebhook
|
|
9
|
+
class EventCassette
|
|
10
|
+
attr_reader :name, :vcr_cassette
|
|
11
|
+
|
|
12
|
+
def initialize(name, vcr_cassette, recording)
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@name = name
|
|
15
|
+
@vcr_cassette = vcr_cassette
|
|
16
|
+
@recording = recording
|
|
17
|
+
if recording
|
|
18
|
+
@data = Data.new
|
|
19
|
+
else
|
|
20
|
+
@data = Data.deserialize(path)
|
|
21
|
+
@wait_counter = 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def path
|
|
26
|
+
dir = VcrStripeWebhook.configuration.cassette_library_dir
|
|
27
|
+
File.join(dir, "#{name}.stripe_webhooks.yml")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def recording?
|
|
31
|
+
@recording
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def wait_for_events(waiter, timeout:, &block)
|
|
35
|
+
if recording?
|
|
36
|
+
record_and_wait_for_events(waiter, timeout: timeout, &block)
|
|
37
|
+
else
|
|
38
|
+
replay_and_wait_for_events(waiter, &block)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def receive_event_payload(event)
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@data.events.push(event)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def serialize
|
|
49
|
+
@data.serialize(path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def record_and_wait_for_events(waiter, timeout:)
|
|
55
|
+
wait_start = @data.events.size
|
|
56
|
+
|
|
57
|
+
yield
|
|
58
|
+
|
|
59
|
+
start_time = Time.now
|
|
60
|
+
loop do
|
|
61
|
+
finished = false
|
|
62
|
+
target_events = nil
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
wait_end = @data.events.size
|
|
65
|
+
target_events = @data.events[wait_start...wait_end]
|
|
66
|
+
finished = waiter.call(target_events)
|
|
67
|
+
@data.waits.push(Wait.new(wait_start, wait_end)) if finished
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return target_events if finished
|
|
71
|
+
|
|
72
|
+
if Time.now - start_time > timeout
|
|
73
|
+
message = waiter.timeout_message(target_events, recording: true)
|
|
74
|
+
raise EventWaitTimeout, message
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sleep 0.5
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def replay_and_wait_for_events(waiter)
|
|
82
|
+
wait = @data.waits[@wait_counter]
|
|
83
|
+
if wait.nil?
|
|
84
|
+
raise CassetteDataError, <<~ERROR_MESSAGE
|
|
85
|
+
receive_webhook_event(s) calls is more than the calls in the recorded data.
|
|
86
|
+
If you modify test code after recording, record real webhooks again.
|
|
87
|
+
ERROR_MESSAGE
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
yield
|
|
91
|
+
|
|
92
|
+
target_events = @data.events[wait.start...wait.end]
|
|
93
|
+
finished = waiter.call(target_events)
|
|
94
|
+
unless finished
|
|
95
|
+
message = waiter.timeout_message(target_events, recording: false)
|
|
96
|
+
raise raise CassetteDataError, message
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@wait_counter += 1
|
|
100
|
+
target_events
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def logger
|
|
104
|
+
VcrStripeWebhook.logger
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Wait
|
|
108
|
+
attr_reader :start, :end
|
|
109
|
+
|
|
110
|
+
def initialize(wait_start, wait_end)
|
|
111
|
+
@start = wait_start
|
|
112
|
+
@end = wait_end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_h
|
|
116
|
+
{
|
|
117
|
+
"start" => start,
|
|
118
|
+
"end" => self.end
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class << self
|
|
123
|
+
def from_hash(hash)
|
|
124
|
+
Wait.new(hash["start"], hash["end"])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class Data
|
|
130
|
+
attr_reader :events, :waits
|
|
131
|
+
|
|
132
|
+
def initialize(events: [], waits: [])
|
|
133
|
+
@events = events
|
|
134
|
+
@waits = waits
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def serialize(path)
|
|
138
|
+
yaml = YAML.dump({
|
|
139
|
+
"events" => @events.map(&:to_h),
|
|
140
|
+
"waits" => @waits.map(&:to_h)
|
|
141
|
+
})
|
|
142
|
+
FileUtils.mkdir_p File.dirname(path)
|
|
143
|
+
File.write(path, yaml)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class << self
|
|
147
|
+
def deserialize(path)
|
|
148
|
+
yaml = YAML.safe_load_file(path, permitted_classes: [])
|
|
149
|
+
events = yaml["events"].map { |event_hash| Event.from_hash(event_hash) }
|
|
150
|
+
waits = yaml["waits"].map { |hash| Wait.from_hash(hash) }
|
|
151
|
+
Data.new(events: events, waits: waits)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "server"
|
|
5
|
+
require_relative "stripe_cli"
|
|
6
|
+
|
|
7
|
+
module VcrStripeWebhook
|
|
8
|
+
class EventReceiver
|
|
9
|
+
class << self
|
|
10
|
+
def instance
|
|
11
|
+
@instance ||= EventReceiver.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def terminate
|
|
15
|
+
return unless @instance
|
|
16
|
+
|
|
17
|
+
@instance.stop
|
|
18
|
+
@instance = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@started = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
return if @started
|
|
28
|
+
|
|
29
|
+
api_key = VcrStripeWebhook.configuration.stripe_api_key
|
|
30
|
+
|
|
31
|
+
@fence_mutex = Mutex.new
|
|
32
|
+
@fence_cond_var = ConditionVariable.new
|
|
33
|
+
@fence_test_clock_id = nil
|
|
34
|
+
@fence_signaled = false
|
|
35
|
+
@last_fence_event_created = nil
|
|
36
|
+
|
|
37
|
+
@cassette = nil
|
|
38
|
+
@server = VcrStripeWebhook::Server.new do |payload|
|
|
39
|
+
receive_webhook(payload)
|
|
40
|
+
end
|
|
41
|
+
@cli = VcrStripeWebhook::StripeCLI.new(@server.port, api_key)
|
|
42
|
+
@started = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
@cli.terminate
|
|
47
|
+
@server.close
|
|
48
|
+
@started = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def use_cassette(cassette)
|
|
52
|
+
if cassette.recording?
|
|
53
|
+
start
|
|
54
|
+
logger.info "Using cassette: #{cassette.name}"
|
|
55
|
+
@cassette = cassette
|
|
56
|
+
yield cassette.vcr_cassette
|
|
57
|
+
logger.info "Eject cassette: #{cassette.name}"
|
|
58
|
+
@cassette = nil
|
|
59
|
+
wait_for_rest_webhooks
|
|
60
|
+
else
|
|
61
|
+
yield cassette.vcr_cassette
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Create and delete dummy test clock to wait for the rest webhooks to be received.
|
|
66
|
+
# Stripe doesn’t guarantee delivery of events in the order in which they’re generated.
|
|
67
|
+
# But waiting dummy test clock event lower the probability of webhook ordering problem.
|
|
68
|
+
def wait_for_rest_webhooks
|
|
69
|
+
time = Time.now
|
|
70
|
+
name = "vcr_sw_fence_#{time.to_i}_#{SecureRandom.alphanumeric}"
|
|
71
|
+
test_clock = nil
|
|
72
|
+
@fence_mutex.synchronize do
|
|
73
|
+
raise "Fence already used!" if @fence_test_clock_id
|
|
74
|
+
|
|
75
|
+
@fence_signaled = false
|
|
76
|
+
test_clock = Stripe::TestHelpers::TestClock.create(frozen_time: time.to_i, name: name)
|
|
77
|
+
@fence_test_clock_id = test_clock.id
|
|
78
|
+
end
|
|
79
|
+
test_clock.delete
|
|
80
|
+
@fence_mutex.synchronize do
|
|
81
|
+
loop do
|
|
82
|
+
break if @fence_signaled
|
|
83
|
+
|
|
84
|
+
@fence_cond_var.wait(@fence_mutex, VcrStripeWebhook.configuration.timeout)
|
|
85
|
+
end
|
|
86
|
+
@fence_test_clock_id = nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def receive_webhook(payload)
|
|
93
|
+
event_hash = JSON.parse(payload)
|
|
94
|
+
if @last_fence_event_created && event_hash["created"] < @last_fence_event_created
|
|
95
|
+
logger.info "Ignore old event: #{event_hash["type"]}"
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
logger.info "Server received event: #{event_hash["type"]} (cassette=#{@cassette&.name})"
|
|
100
|
+
@cassette&.receive_event_payload(Event.new(event_hash["type"], event_hash))
|
|
101
|
+
|
|
102
|
+
@fence_mutex.synchronize do
|
|
103
|
+
if @fence_test_clock_id && event_hash["type"] == "test_helpers.test_clock.deleted" && event_hash.dig(
|
|
104
|
+
"data", "object", "id"
|
|
105
|
+
) == @fence_test_clock_id
|
|
106
|
+
@last_fence_event_created = event_hash["created"]
|
|
107
|
+
@fence_signaled = true
|
|
108
|
+
@fence_cond_var.signal
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def logger
|
|
114
|
+
VcrStripeWebhook.logger
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module VcrStripeWebhook
|
|
6
|
+
# Tiny web server for stripe webhook
|
|
7
|
+
class Server
|
|
8
|
+
attr_reader :tcp_server, :port
|
|
9
|
+
|
|
10
|
+
def initialize(port = 0, &block)
|
|
11
|
+
run(port, &block)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close
|
|
15
|
+
logger.info "Closing Server"
|
|
16
|
+
tcp_server.close
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wait_event(event_type)
|
|
20
|
+
@event_waiter.wait(event_type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def join
|
|
24
|
+
@accept_thread.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def run(port, &block)
|
|
30
|
+
@tcp_server = TCPServer.open(port)
|
|
31
|
+
_family, port, host, _ip = tcp_server.addr
|
|
32
|
+
@port = port
|
|
33
|
+
logger.info "Server is on #{[host, port].join(":")}"
|
|
34
|
+
|
|
35
|
+
@block = block
|
|
36
|
+
@accept_thread = Thread.start do
|
|
37
|
+
run_thread
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_thread
|
|
42
|
+
loop do
|
|
43
|
+
socket = tcp_server.accept
|
|
44
|
+
Thread.start(socket) do |s|
|
|
45
|
+
request = RequestParser.new(s)
|
|
46
|
+
|
|
47
|
+
if request.json?
|
|
48
|
+
@block&.call(request.body)
|
|
49
|
+
s.write("HTTP/1.1 204\r\n\r\n")
|
|
50
|
+
else
|
|
51
|
+
s.write("HTTP/1.1 400\r\n\r\n")
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
s.close
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
rescue IOError
|
|
58
|
+
# Closed in another thread
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def logger
|
|
62
|
+
VcrStripeWebhook.logger
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class RequestParser
|
|
66
|
+
MAX_HEADER_LENGTH = 16 * 1024
|
|
67
|
+
|
|
68
|
+
attr_reader :method, :path, :headers, :body
|
|
69
|
+
|
|
70
|
+
def initialize(socket)
|
|
71
|
+
@method, @path = read_start_line(socket)
|
|
72
|
+
@headers = read_headers(socket)
|
|
73
|
+
return unless content_length
|
|
74
|
+
|
|
75
|
+
@body = socket.read(content_length)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def content_length
|
|
79
|
+
@content_length ||= headers["content-length"]&.to_i
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def content_type
|
|
83
|
+
return @content_type if defined?(@content_type)
|
|
84
|
+
|
|
85
|
+
if headers["content-type"]
|
|
86
|
+
@content_type, _attributes = headers["content-type"].split(/;\s+/, 2)
|
|
87
|
+
else
|
|
88
|
+
@content_type = nil
|
|
89
|
+
end
|
|
90
|
+
@content_type
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def json?
|
|
94
|
+
content_type == "application/json"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def json_value
|
|
98
|
+
return @json_value if defined?(@json_value)
|
|
99
|
+
|
|
100
|
+
@json_value =
|
|
101
|
+
if json?
|
|
102
|
+
begin
|
|
103
|
+
JSON.parse(@body)
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def read_start_line(socket)
|
|
113
|
+
line = socket.gets("\n")
|
|
114
|
+
method, path, _http_version = line.split(" ", 3)
|
|
115
|
+
[method, path]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read_headers(socket)
|
|
119
|
+
headers = {}
|
|
120
|
+
loop do
|
|
121
|
+
line = socket.gets("\n", MAX_HEADER_LENGTH, chomp: true)
|
|
122
|
+
break if line.nil? || line.empty?
|
|
123
|
+
|
|
124
|
+
key, value = line.split(/:\s+/, 2).map(&:strip)
|
|
125
|
+
headers[key.downcase] = value
|
|
126
|
+
end
|
|
127
|
+
headers
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def logger
|
|
131
|
+
VcrStripeWebhook.logger
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VcrStripeWebhook
|
|
4
|
+
class StripeCLI
|
|
5
|
+
def initialize(server_port, api_key)
|
|
6
|
+
@server_port = server_port
|
|
7
|
+
@api_key = api_key
|
|
8
|
+
run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def terminate
|
|
12
|
+
@stdout.close
|
|
13
|
+
@read_out_thread.join
|
|
14
|
+
@stderr.close
|
|
15
|
+
@read_err_thread.join
|
|
16
|
+
Process.kill(:TERM, @pid)
|
|
17
|
+
Process.waitpid(@pid)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
spawn_process
|
|
24
|
+
|
|
25
|
+
@ready_waiter = ReadyWaiter.new
|
|
26
|
+
|
|
27
|
+
@read_out_thread = Thread.new do
|
|
28
|
+
read_thread("OUT", @stdout)
|
|
29
|
+
end
|
|
30
|
+
@read_err_thread = Thread.new do
|
|
31
|
+
read_thread("ERR", @stderr, ready_check: true)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
logger.info "Waiting for Stripe CLI to be ready..."
|
|
35
|
+
@ready_waiter.wait_ready
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def spawn_process
|
|
40
|
+
env = { "STRIPE_API_KEY" => @api_key }
|
|
41
|
+
r_out, w_out = IO.pipe
|
|
42
|
+
r_err, w_err = IO.pipe
|
|
43
|
+
command_and_args = ["stripe", "listen", "--forward-to", "localhost:#{@server_port}/"]
|
|
44
|
+
logger.info "Spawning Stripe CLI: #{command_and_args.join(" ")}"
|
|
45
|
+
@pid = spawn(env, *command_and_args, { out: w_out, err: w_err })
|
|
46
|
+
w_out.close
|
|
47
|
+
w_err.close
|
|
48
|
+
@stdout = r_out
|
|
49
|
+
@stderr = r_err
|
|
50
|
+
logger.info "Stripe CLI PID=#{@pid}"
|
|
51
|
+
rescue Errno::ENOENT
|
|
52
|
+
raise "Could not spawn Stripe CLI"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_thread(prefix, io, ready_check: false)
|
|
56
|
+
loop do
|
|
57
|
+
line = io.gets(chomp: true)
|
|
58
|
+
return if line.nil?
|
|
59
|
+
|
|
60
|
+
@ready_waiter.signal_ready if ready_check && line.start_with?("Ready!")
|
|
61
|
+
logger.info "Stripe CLI: #{prefix}: #{line}"
|
|
62
|
+
rescue IOError
|
|
63
|
+
# Closed in another thread
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def logger
|
|
69
|
+
VcrStripeWebhook.logger
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class ReadyWaiter
|
|
73
|
+
def initialize
|
|
74
|
+
@ready = false
|
|
75
|
+
@ready_mutex = Mutex.new
|
|
76
|
+
@ready_cond_var = ConditionVariable.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def wait_ready
|
|
80
|
+
@ready_mutex.synchronize do
|
|
81
|
+
loop do
|
|
82
|
+
break if @ready
|
|
83
|
+
|
|
84
|
+
@ready_cond_var.wait(@ready_mutex)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def signal_ready
|
|
90
|
+
@ready_mutex.synchronize do
|
|
91
|
+
@ready = true
|
|
92
|
+
@ready_cond_var.signal
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VcrStripeWebhook
|
|
4
|
+
class ProcWaiter
|
|
5
|
+
def initialize(_proc)
|
|
6
|
+
@proc
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(events)
|
|
10
|
+
@proc.call(events)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def timeout_message(_events, recording:)
|
|
14
|
+
events_words = recording ? "received events" : "the recorded events"
|
|
15
|
+
"wait_until proc does not return true with #{events_words}."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class EventTypeWaiter
|
|
20
|
+
attr_reader :event_types
|
|
21
|
+
|
|
22
|
+
def initialize(event_types)
|
|
23
|
+
@event_types = event_types
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(events)
|
|
27
|
+
type_set = events.map(&:type).to_set
|
|
28
|
+
event_types.all? { |type| type_set.member?(type) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def timeout_message(events, recording:)
|
|
32
|
+
type_set = events.map(&:type).to_set
|
|
33
|
+
missing_event_types = event_types.reject { |type| type_set.member?(type) }
|
|
34
|
+
events_words = recording ? "Received events" : "The recorded events"
|
|
35
|
+
types_words = missing_event_types.map { |type| "'#{type}'" }.join(", ")
|
|
36
|
+
"#{events_words} don't contain #{types_words}."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "set"
|
|
5
|
+
require_relative "vcr_stripe_webhook/version"
|
|
6
|
+
require_relative "vcr_stripe_webhook/configuration"
|
|
7
|
+
require_relative "vcr_stripe_webhook/event_receiver"
|
|
8
|
+
require_relative "vcr_stripe_webhook/event_cassette"
|
|
9
|
+
require_relative "vcr_stripe_webhook/waiter"
|
|
10
|
+
|
|
11
|
+
module VcrStripeWebhook
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :server, :current_event_cassette
|
|
14
|
+
attr_writer :logger
|
|
15
|
+
|
|
16
|
+
def stop
|
|
17
|
+
EventReceiver.terminate
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configuration
|
|
21
|
+
@configuration ||= VcrStripeWebhook::Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def logger
|
|
25
|
+
@logger ||= Logger.new("log/vcr_stripe_webhook.log")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def use_cassette(name, options = {}, &block)
|
|
29
|
+
VCR.use_cassette(name, options) do |vcr_cassette|
|
|
30
|
+
@current_event_cassette = EventCassette.new(name, vcr_cassette, vcr_cassette.recording?)
|
|
31
|
+
EventReceiver.instance.use_cassette(@current_event_cassette, &block)
|
|
32
|
+
@current_event_cassette.serialize if @current_event_cassette.recording?
|
|
33
|
+
ensure
|
|
34
|
+
@current_event_cassette = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def receive_webhook_events(event_types: nil, wait_until: nil, timeout: configuration.timeout, &block)
|
|
39
|
+
raise "No event cassette inserted." if current_event_cassette.nil?
|
|
40
|
+
raise "event_types or wait_until must be passed as an argument." if event_types.nil? && wait_until.nil?
|
|
41
|
+
|
|
42
|
+
waiter =
|
|
43
|
+
if event_types
|
|
44
|
+
EventTypeWaiter.new(event_types)
|
|
45
|
+
else
|
|
46
|
+
ProcWaiter.new(wait_until)
|
|
47
|
+
end
|
|
48
|
+
current_event_cassette.wait_for_events(waiter, timeout: timeout, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def receive_webhook_event(event_type, timeout: configuration.timeout, &block)
|
|
52
|
+
events = receive_webhook_events(event_types: [event_type], timeout: timeout, &block)
|
|
53
|
+
events.find { |event| event.type == event_type }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: vcr_stripe_webhook
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shunichi Ikegami
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-12-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: stripe
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '9.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '9.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: vcr
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: dotenv
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: webmock
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
description: Record and replay Stripe webhooks during test
|
|
70
|
+
email:
|
|
71
|
+
- sike.tm@gmail.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- ".rspec"
|
|
77
|
+
- ".rubocop.yml"
|
|
78
|
+
- ".ruby-version"
|
|
79
|
+
- CHANGELOG.md
|
|
80
|
+
- Gemfile
|
|
81
|
+
- Gemfile.lock
|
|
82
|
+
- LICENSE.txt
|
|
83
|
+
- README.md
|
|
84
|
+
- Rakefile
|
|
85
|
+
- lib/vcr_stripe_webhook.rb
|
|
86
|
+
- lib/vcr_stripe_webhook/configuration.rb
|
|
87
|
+
- lib/vcr_stripe_webhook/error.rb
|
|
88
|
+
- lib/vcr_stripe_webhook/event.rb
|
|
89
|
+
- lib/vcr_stripe_webhook/event_cassette.rb
|
|
90
|
+
- lib/vcr_stripe_webhook/event_receiver.rb
|
|
91
|
+
- lib/vcr_stripe_webhook/server.rb
|
|
92
|
+
- lib/vcr_stripe_webhook/stripe_cli.rb
|
|
93
|
+
- lib/vcr_stripe_webhook/version.rb
|
|
94
|
+
- lib/vcr_stripe_webhook/waiter.rb
|
|
95
|
+
- sig/vcr_stripe_webhook.rbs
|
|
96
|
+
homepage: https://github.com/shunichi/vcr_stripe_webhook
|
|
97
|
+
licenses:
|
|
98
|
+
- MIT
|
|
99
|
+
metadata:
|
|
100
|
+
homepage_uri: https://github.com/shunichi/vcr_stripe_webhook
|
|
101
|
+
source_code_uri: https://github.com/shunichi/vcr_stripe_webhook
|
|
102
|
+
changelog_uri: https://github.com/shunichi/vcr_stripe_webhook/blob/master/CHANGELOG.md
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: 3.0.0
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.2.33
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Record and replay Stripe webhooks during test
|
|
122
|
+
test_files: []
|