webvalve 1.3.1 → 2.0.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 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