webvalve 1.3.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94d2cb2a5a975fb3dc734272460423bb27c6909d49a80b807161da8b95a97da1
4
- data.tar.gz: cc3df67e4361e88d496937ec37dc65619b96371c8ae52c6f4c217e434a7edddc
3
+ metadata.gz: e03b34e13263947acd26c73387bc7f1dcf6f8ae5749463fc83187fd43d75dc98
4
+ data.tar.gz: 85994585f1c19f5f63ea966fdc7c919e5e870e650c980fbea10accb30fb568ef
5
5
  SHA512:
6
- metadata.gz: 6c9202365a83879f8df5123ef802d075c4f6dc8ae475a76258035bbecbc9031e8c9fef27c0e4eb343448530d174ef47f8507f1711683918fa4ad2b749f27bfda
7
- data.tar.gz: c0a15038e8064bcd8bcfae0b2f13560a05e86b4d3df96a29a0a77366f985a86640ccc3a65b0d7350bd991fbe46f7c578c565b180ecfccbc572c2a352c90ff262
6
+ metadata.gz: '02801b418f46db96e7d6ef9cfad07c39a29054cd2a7357cfbb39029838a2cbf733f1e1a183590e731a6ef734fa6061de2a265eca761cffdccd3882d1b24ecb7e'
7
+ data.tar.gz: a100512db4c9e940a7853d168b7d0269db9e688048df3be24b1ccd31e4b881f64a3a14ec4da83bb1c9192ccd4867fe660ff30ebe2a8659f5b3ed59cc5d325d2d
data/CHANGELOG.md CHANGED
@@ -9,6 +9,10 @@ and this project aims to adhere to [Semantic Versioning](http://semver.org/spec/
9
9
  ### Added
10
10
  ### Removed
11
11
 
12
+ ## [2.0.0] - 2023-07-20
13
+ ### Added
14
+ - Dynamic URL support via wildcards, Regexps, and Addressable::Templates
15
+
12
16
  ## [1.3.1] - 2023-07-20
13
17
  ### Changed
14
18
  - Replace usage of deprecated `File.exists?` in generator
data/README.md CHANGED
@@ -218,6 +218,43 @@ WebValve.register FakeBank, url: ENV.fetch("SOME_CUSTOM_API_URL")
218
218
  WebValve.register FakeBank, url: "https://some-service.com"
219
219
  ```
220
220
 
221
+ ## Dynamic URLs
222
+
223
+ If the service you are interacting with contains dynamic elements, e.g.
224
+ an instance-specific subdomain, you can specify a wildcard in your url
225
+ with the `*` character to match a series of zero or more characters
226
+ within the same URL segment. For example:
227
+
228
+ ```bash
229
+ export BANK_API_URL=https://*.mybank.com/
230
+ ```
231
+
232
+ or
233
+
234
+ ```ruby
235
+ WebValve.register FakeBank, url: "https://*.mybank.com"
236
+ ```
237
+
238
+ Note: unlike filesystem globbing, `?` isn't respected to mean "exactly
239
+ one character" because it's a URL delimiter character. Only `*` works
240
+ for WebValve URL wildcards.
241
+
242
+ Alternatively you can use `Addressable::Template`s or `Regexp`s to
243
+ specify dynamic URLs if they for some reason aren't a good fit for the
244
+ wildcard syntax. Note that there is no `ENV` var support for these
245
+ formats because there is no detection logic to determine a URL string is
246
+ actually meant to represent a URL template or regexp. For example:
247
+
248
+ ```ruby
249
+ WebValve.register FakeBank, url: Addressable::Template.new("http://mybank.com{/path*}{?query}")
250
+ ```
251
+
252
+ or
253
+
254
+ ```ruby
255
+ WebValve.register FakeBank, url: %r{\Ahttp://mybank.com(/.*)?\z}
256
+ ```
257
+
221
258
  ## What's in a `FakeService`?
222
259
 
223
260
  The definition of `FakeService` is really simple. It's just a
@@ -1,6 +1,7 @@
1
1
  require 'webmock'
2
2
  require 'singleton'
3
3
  require 'set'
4
+ require 'webvalve/service_url_converter'
4
5
 
5
6
  module WebValve
6
7
  ALWAYS_ENABLED_ENVS = %w(development test).freeze
@@ -148,7 +149,7 @@ module WebValve
148
149
  end
149
150
 
150
151
  def url_to_regexp(url)
151
- %r(\A#{Regexp.escape url})
152
+ ServiceUrlConverter.new(url: url).regexp
152
153
  end
153
154
 
154
155
  def ensure_non_duplicate_stub(config)
@@ -0,0 +1,24 @@
1
+ module WebValve
2
+ class ServiceUrlConverter
3
+ TOKEN_BOUNDARY_CHARS = Regexp.escape('.:/?#@&=').freeze
4
+ WILDCARD_SUBSTITUTION = ('[^' + TOKEN_BOUNDARY_CHARS + ']*').freeze
5
+ URL_PREFIX_BOUNDARY = ('[' + TOKEN_BOUNDARY_CHARS + ']').freeze
6
+ URL_SUFFIX_PATTERN = ('((' + URL_PREFIX_BOUNDARY + '|(?<=' + URL_PREFIX_BOUNDARY + ')).*)?\z').freeze
7
+
8
+ attr_reader :url
9
+
10
+ def initialize(url:)
11
+ @url = url
12
+ end
13
+
14
+ def regexp
15
+ if url.is_a?(String)
16
+ regexp_string = Regexp.escape(url)
17
+ substituted_regexp_string = regexp_string.gsub('\*', WILDCARD_SUBSTITUTION)
18
+ %r(\A#{substituted_regexp_string}#{URL_SUFFIX_PATTERN})
19
+ else
20
+ url
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module WebValve
2
- VERSION = "1.3.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -38,6 +38,9 @@ RSpec.describe WebValve::Manager do
38
38
  end
39
39
 
40
40
  describe '#setup' do
41
+ let(:wildcard_substitution) { WebValve::ServiceUrlConverter::WILDCARD_SUBSTITUTION }
42
+ let(:url_suffix_pattern) { WebValve::ServiceUrlConverter::URL_SUFFIX_PATTERN }
43
+
41
44
  context 'when WebValve is disabled' do
42
45
  around do |ex|
43
46
  with_rails_env 'production' do
@@ -94,10 +97,17 @@ RSpec.describe WebValve::Manager do
94
97
 
95
98
  it 'allowlists configured urls in webmock' do
96
99
  allow(WebMock).to receive(:disable_net_connect!)
97
- results = [%r{\Ahttp://foo\.dev}, %r{\Ahttp://bar\.dev}]
100
+ results = [
101
+ %r{\Ahttp://foo\.dev#{url_suffix_pattern}},
102
+ %r{\Ahttp://bar\.dev#{url_suffix_pattern}},
103
+ %r{\Ahttp://bar\.#{wildcard_substitution}\.dev#{url_suffix_pattern}},
104
+ %r{\Ahttp://bar\.dev/\?foo=bar#{url_suffix_pattern}}
105
+ ]
98
106
 
99
107
  subject.allow_url 'http://foo.dev'
100
108
  subject.allow_url 'http://bar.dev'
109
+ subject.allow_url 'http://bar.*.dev'
110
+ subject.allow_url 'http://bar.dev/?foo=bar'
101
111
 
102
112
  subject.setup
103
113
 
@@ -115,7 +125,7 @@ RSpec.describe WebValve::Manager do
115
125
  subject.setup
116
126
  end
117
127
 
118
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev})
128
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev#{url_suffix_pattern}})
119
129
  expect(web_mock_stubble).to have_received(:to_rack)
120
130
  end
121
131
 
@@ -153,7 +163,7 @@ RSpec.describe WebValve::Manager do
153
163
  subject.register other_disabled_service.name
154
164
 
155
165
  expect { subject.setup }.to_not raise_error
156
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev}).twice
166
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev#{url_suffix_pattern}}).twice
157
167
  end
158
168
  end
159
169
  end
@@ -208,8 +218,8 @@ RSpec.describe WebValve::Manager do
208
218
  subject.setup
209
219
  end
210
220
 
211
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev})
212
- expect(WebMock).not_to have_received(:stub_request).with(:any, %r{\Ahttp://other\.dev})
221
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev#{url_suffix_pattern}})
222
+ expect(WebMock).not_to have_received(:stub_request).with(:any, %r{\Ahttp://other\.dev#{url_suffix_pattern}})
213
223
  expect(web_mock_stubble).to have_received(:to_rack).once
214
224
  end
215
225
 
@@ -261,9 +271,9 @@ RSpec.describe WebValve::Manager do
261
271
  subject.setup
262
272
  end
263
273
 
264
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev})
265
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\-else\.dev})
266
- expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://other\.dev})
274
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\.dev#{url_suffix_pattern}})
275
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://something\-else\.dev#{url_suffix_pattern}})
276
+ expect(WebMock).to have_received(:stub_request).with(:any, %r{\Ahttp://other\.dev#{url_suffix_pattern}})
267
277
  expect(web_mock_stubble).to have_received(:to_rack).exactly(3).times
268
278
  end
269
279
  end
@@ -0,0 +1,194 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe WebValve::ServiceUrlConverter do
4
+ let(:url) { "http://bar.com" }
5
+
6
+ subject { described_class.new(url: url) }
7
+
8
+ describe '#regexp' do
9
+ it "returns a regexp" do
10
+ expect(subject.regexp).to be_a(Regexp)
11
+ end
12
+
13
+ context "with a regexp" do
14
+ let(:url) { %r{\Ahttp://foo\.com} }
15
+
16
+ it "returns the same object" do
17
+ expect(subject.regexp).to be_a(Regexp)
18
+ expect(subject.regexp).to equal(url)
19
+ end
20
+ end
21
+
22
+ context "with an empty url" do
23
+ let(:url) { "" }
24
+
25
+ it "matches empty string" do
26
+ expect("").to match(subject.regexp)
27
+ end
28
+
29
+ it "matches a string starting with a URL delimiter because the rest is just interpreted as suffix" do
30
+ expect(":do:do:dodo:do:do").to match(subject.regexp)
31
+ end
32
+
33
+ it "doesn't match a string that doesn't start with a delimiter" do
34
+ expect("jamietart:do:do:dodo:do:do").not_to match(subject.regexp)
35
+ end
36
+ end
37
+
38
+ context "with a boundary char on the end" do
39
+ let(:url) { "http://bar.com/" }
40
+
41
+ it "matches arbitrary suffixes" do
42
+ expect("http://bar.com/baz/bump/beep").to match(subject.regexp)
43
+ end
44
+ end
45
+
46
+ context "with multiple asterisks" do
47
+ let(:url) { "http://bar.com/**/bump" }
48
+
49
+ it "matches like a single asterisk" do
50
+ expect("http://bar.com/foo/bump").to match(subject.regexp)
51
+ end
52
+
53
+ it "doesn't match like a filesystem glob" do
54
+ expect("http://bar.com/foo/bar/bump").not_to match(subject.regexp)
55
+ end
56
+ end
57
+
58
+ context "with a trailing *" do
59
+ let(:url) { "http://bar.com/*" }
60
+
61
+ it "matches when empty" do
62
+ expect("http://bar.com/").to match(subject.regexp)
63
+ end
64
+
65
+ it "matches when existing" do
66
+ expect("http://bar.com/foobaloo").to match(subject.regexp)
67
+ end
68
+
69
+ it "matches with additional tokens" do
70
+ expect("http://bar.com/foobaloo/wink").to match(subject.regexp)
71
+ end
72
+
73
+ it "doesn't match when missing the trailing slash tho" do
74
+ expect("http://bar.com").not_to match(subject.regexp)
75
+ end
76
+ end
77
+
78
+ context "with a totally wildcarded protocol" do
79
+ let(:url) { "*://bar.com" }
80
+
81
+ it "matches http" do
82
+ expect("http://bar.com/").to match(subject.regexp)
83
+ end
84
+
85
+ it "matches anything else" do
86
+ expect("gopher://bar.com/").to match(subject.regexp)
87
+ end
88
+
89
+ it "matches empty" do
90
+ expect("://bar.com").to match(subject.regexp)
91
+ end
92
+ end
93
+
94
+ context "with a wildcarded partial protocol" do
95
+ let(:url) { "http*://bar.com" }
96
+
97
+ it "matches empty" do
98
+ expect("http://bar.com/").to match(subject.regexp)
99
+ end
100
+
101
+ it "matches full" do
102
+ expect("https://bar.com/").to match(subject.regexp)
103
+ end
104
+ end
105
+
106
+ context "with a TLD that is a substring of another TLD" do
107
+ let(:url) { "http://bar.co" }
108
+
109
+ it "doesn't match a different TLD when extending" do
110
+ expect("http://bar.com").not_to match(subject.regexp)
111
+ end
112
+ end
113
+
114
+ context "with a wildcard subdomain" do
115
+ let(:url) { "http://*.bar.com" }
116
+
117
+ it "matches" do
118
+ expect("http://foo.bar.com").to match(subject.regexp)
119
+ end
120
+
121
+ it "doesn't match when too many subdomains" do
122
+ expect("http://beep.foo.bar.com").not_to match(subject.regexp)
123
+ end
124
+ end
125
+
126
+ context "with a partial postfix wildcard subdomain" do
127
+ let(:url) { "http://foo*.bar.com" }
128
+
129
+ it "matches when present" do
130
+ expect("http://foobaz.bar.com").to match(subject.regexp)
131
+ end
132
+
133
+ it "matches when empty" do
134
+ expect("http://foo.bar.com").to match(subject.regexp)
135
+ end
136
+
137
+ it "doesn't match when out of order" do
138
+ expect("http://bazfoo.bar.com").not_to match(subject.regexp)
139
+ end
140
+ end
141
+
142
+ context "with a partial prefix wildcard subdomain" do
143
+ let(:url) { "http://*baz.bar.com" }
144
+
145
+ it "matches when present" do
146
+ expect("http://foobaz.bar.com").to match(subject.regexp)
147
+ end
148
+
149
+ it "matches when empty" do
150
+ expect("http://baz.bar.com").to match(subject.regexp)
151
+ end
152
+ end
153
+
154
+ context "with a wildcarded basic auth url" do
155
+ let(:url) { "http://*:*@bar.com" }
156
+
157
+ it "matches when present" do
158
+ expect("http://bilbo:baggins@bar.com").to match(subject.regexp)
159
+ end
160
+
161
+ it "doesn't match when malformed" do
162
+ expect("http://bilbobaggins@bar.com").not_to match(subject.regexp)
163
+ end
164
+
165
+ it "doesn't match when missing password part" do
166
+ expect("http://bilbo@bar.com").not_to match(subject.regexp)
167
+ end
168
+ end
169
+
170
+ context "with a wildcarded path" do
171
+ let(:url) { "http://bar.com/*/whatever" }
172
+
173
+ it "matches with arbitrarily spicy but legal, non-URL-significant characters" do
174
+ expect("http://bar.com/a0-_~[]!$'(),;%+/whatever").to match(subject.regexp)
175
+ end
176
+
177
+ it "doesn't match when you throw a URL-significant char in there" do
178
+ expect("http://bar.com/life=love/whatever").not_to match(subject.regexp)
179
+ end
180
+ end
181
+
182
+ context "with a wildcarded query param" do
183
+ let(:url) { "http://bar.com/whatever?foo=*&bar=bump" }
184
+
185
+ it "matches when present" do
186
+ expect("http://bar.com/whatever?foo=baz&bar=bump").to match(subject.regexp)
187
+ end
188
+
189
+ it "doesn't match when you throw a URL-significant char in there" do
190
+ expect("http://bar.com/whatever?foo=baz#&bar=bump").not_to match(subject.regexp)
191
+ end
192
+ end
193
+ end
194
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webvalve
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Moore
@@ -173,6 +173,7 @@ files:
173
173
  - lib/webvalve/monkey_patches.rb
174
174
  - lib/webvalve/railtie.rb
175
175
  - lib/webvalve/rspec.rb
176
+ - lib/webvalve/service_url_converter.rb
176
177
  - lib/webvalve/version.rb
177
178
  - spec/dummy/config/application.rb
178
179
  - spec/examples.txt
@@ -182,13 +183,28 @@ files:
182
183
  - spec/webvalve/fake_service_config_spec.rb
183
184
  - spec/webvalve/fake_service_spec.rb
184
185
  - spec/webvalve/manager_spec.rb
186
+ - spec/webvalve/service_url_converter_spec.rb
185
187
  - spec/webvalve_spec.rb
186
188
  homepage: https://github.com/Betterment/webvalve
187
189
  licenses:
188
190
  - MIT
189
191
  metadata:
190
192
  rubygems_mfa_required: 'true'
191
- post_install_message:
193
+ post_install_message: |
194
+ Thanks for installing WebValve!
195
+
196
+ Note for upgraders: If you're upgrading from a version less than 2.0, service
197
+ URL behavior has changed. Please verify that your app isn't relying on the
198
+ previous behavior:
199
+
200
+ 1. `*` characters are now interpreted as wildcards, enabling dynamic URL
201
+ segments. In the unlikely event that your URLs use `*` literals, you'll need
202
+ to URL encode them (`%2A`) both in your URL spec and at runtime.
203
+
204
+ 2. URL suffix matching is now strict. For example, `BAR_URL=http://bar.co` will
205
+ no longer match `https://bar.com`, but it will match `http://bar.co/foo`. If
206
+ you need to preserve the previous behavior, you can add a trailing `*` to
207
+ your URL spec, e.g. `BAR_URL=http://bar.co*`.
192
208
  rdoc_options: []
193
209
  require_paths:
194
210
  - lib
@@ -216,4 +232,5 @@ test_files:
216
232
  - spec/webvalve/fake_service_config_spec.rb
217
233
  - spec/webvalve/fake_service_spec.rb
218
234
  - spec/webvalve/manager_spec.rb
235
+ - spec/webvalve/service_url_converter_spec.rb
219
236
  - spec/webvalve_spec.rb