From 25cf07d55697fe3f38c67bb360ebfe10c724a485 Mon Sep 17 00:00:00 2001 From: Jeff Camera Date: Fri, 22 May 2026 16:38:10 -0400 Subject: [PATCH] Fix Regexp and Addressable::Template URL handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FakeServiceConfig#service_url` previously stringified every URL via `strip_basic_auth(url.to_s)` before it reached `ServiceUrlConverter`, which meant the converter's non-String passthrough branch was unreachable from `Manager#webmock_service`. As a result, the documented `Regexp` and `Addressable::Template` registration forms produced WebMock stubs that matched nothing, and `path_prefix` either returned garbage or raised `URI::InvalidURIError`. See #104. - Only apply `strip_basic_auth` to String URLs; pass `Regexp` and `Addressable::Template` through untouched. - Default `path_prefix` to `""` for non-String URLs (no-op against `FakeServiceWrapper`'s leading-prefix gsub) instead of trying to parse a path out of the pattern. - Add an optional `path_prefix:` keyword to `WebValve.register` for callers who want relative fake-service routes when using a `Regexp` or `Addressable::Template` URL. Adds unit coverage for the new URL forms and override, plus the end-to-end integration coverage that was missing — registering a fake with a `Regexp` / `Template` URL, firing a real `Net::HTTP` request, and asserting the fake responds. The previous test suite only checked the arguments handed to `WebMock.stub_request` and never exercised `FakeServiceWrapper#call`, which is why the bug shipped undetected. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 15 +++++ lib/webvalve/fake_service_config.rb | 12 ++-- spec/webvalve/fake_service_config_spec.rb | 64 +++++++++++++++++++++ spec/webvalve/fake_service_spec.rb | 51 ++++++++++++++++ spec/webvalve/service_url_converter_spec.rb | 9 +++ 5 files changed, 147 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 970c0ee..e94f8cd 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,21 @@ or WebValve.register FakeBank, url: %r{\Ahttp://mybank.com(/.*)?\z} ``` +When a `String` URL is registered, WebValve parses any path component out +of it and strips that prefix from incoming requests before they reach +your `FakeService` — so a service registered at `https://mybank.com/api` +can declare its routes as `get '/accounts'` rather than +`get '/api/accounts'`. WebValve cannot infer a static prefix from a +`Regexp` or `Addressable::Template`, so by default it does not strip any +prefix and your fake routes must use the full request path. If you'd +like to keep relative routes, pass an explicit `path_prefix:`: + +```ruby +WebValve.register FakeBank, + url: %r{\Ahttps://api\.mybank\.com/v2/.*\z}, + path_prefix: '/v2' +``` + ## What's in a `FakeService`? The definition of `FakeService` is really simple. It's just a diff --git a/lib/webvalve/fake_service_config.rb b/lib/webvalve/fake_service_config.rb index 9f70b58..c8048e1 100644 --- a/lib/webvalve/fake_service_config.rb +++ b/lib/webvalve/fake_service_config.rb @@ -2,9 +2,10 @@ module WebValve class FakeServiceConfig attr_reader :service_class_name - def initialize(service_class_name:, url: nil) + def initialize(service_class_name:, url: nil, path_prefix: nil) @service_class_name = service_class_name @custom_service_url = url + @custom_path_prefix = path_prefix end def explicitly_enabled? @@ -22,17 +23,20 @@ def full_url def service_url @service_url ||= begin raise missing_url_message if full_url.blank? - strip_basic_auth full_url + full_url.is_a?(String) ? strip_basic_auth(full_url) : full_url end end def path_prefix - @path_prefix ||= URI::parse(service_url).path + @path_prefix ||= begin + raise missing_url_message if full_url.blank? + custom_path_prefix || (full_url.is_a?(String) ? URI.parse(service_url).path : "") + end end private - attr_reader :custom_service_url + attr_reader :custom_service_url, :custom_path_prefix def value_from_env ENV["#{service_name.to_s.upcase}_ENABLED"] diff --git a/spec/webvalve/fake_service_config_spec.rb b/spec/webvalve/fake_service_config_spec.rb index d1acb71..9c7042f 100644 --- a/spec/webvalve/fake_service_config_spec.rb +++ b/spec/webvalve/fake_service_config_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'addressable/template' RSpec.describe WebValve::FakeServiceConfig do let(:fake_service) do @@ -87,6 +88,26 @@ def self.name expect(subject.service_url).to eq 'http://thingy.dev' end end + + context 'when registered with a Regexp url' do + let(:url) { %r{\Ahttp://thingy\.dev(/.*)?\z} } + + subject { described_class.new(service_class_name: fake_service.name, url: url) } + + it 'returns the Regexp unchanged' do + expect(subject.service_url).to equal(url) + end + end + + context 'when registered with an Addressable::Template url' do + let(:url) { Addressable::Template.new('http://thingy.dev{/path*}') } + + subject { described_class.new(service_class_name: fake_service.name, url: url) } + + it 'returns the Addressable::Template unchanged' do + expect(subject.service_url).to equal(url) + end + end end describe '.path_prefix' do @@ -111,5 +132,48 @@ def self.name expect(subject.path_prefix).to eq '/welcome' # Ignores trailing '/' end end + + context 'when registered with a Regexp url' do + subject { described_class.new(service_class_name: fake_service.name, url: %r{\Ahttp://thingy\.dev/api(/.*)?\z}) } + + it 'defaults to an empty string so FakeServiceWrapper does not strip from PATH_INFO' do + expect(subject.path_prefix).to eq '' + end + end + + context 'when registered with an Addressable::Template url' do + subject { described_class.new(service_class_name: fake_service.name, url: Addressable::Template.new('http://thingy.dev/api{/path*}')) } + + it 'defaults to an empty string so FakeServiceWrapper does not strip from PATH_INFO' do + expect(subject.path_prefix).to eq '' + end + end + + context 'when an explicit path_prefix is provided' do + it 'uses the override with a String url' do + with_env 'DUMMY_API_URL' => 'http://zombo.com' do + config = described_class.new(service_class_name: fake_service.name, path_prefix: '/api/v2') + expect(config.path_prefix).to eq '/api/v2' + end + end + + it 'uses the override with a Regexp url' do + config = described_class.new( + service_class_name: fake_service.name, + url: %r{\Ahttp://thingy\.dev/api/v2(/.*)?\z}, + path_prefix: '/api/v2', + ) + expect(config.path_prefix).to eq '/api/v2' + end + + it 'uses the override with an Addressable::Template url' do + config = described_class.new( + service_class_name: fake_service.name, + url: Addressable::Template.new('http://thingy.dev/api/v2{/path*}'), + path_prefix: '/api/v2', + ) + expect(config.path_prefix).to eq '/api/v2' + end + end end end diff --git a/spec/webvalve/fake_service_spec.rb b/spec/webvalve/fake_service_spec.rb index 3728fa2..edca9bf 100644 --- a/spec/webvalve/fake_service_spec.rb +++ b/spec/webvalve/fake_service_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'addressable/template' RSpec.describe WebValve::FakeService do subject do @@ -66,5 +67,55 @@ def self.name end end end + + context 'when the service is registered with a Regexp url' do + it 'returns the result from the fake when the request URL matches the pattern' do + WebValve.register subject.name, url: %r{\Ahttp://dummy\.dev/.*\z} + WebValve.setup + + expect(Net::HTTP.get(URI('http://dummy.dev/widgets'))).to eq({ result: 'it works!' }.to_json) + end + + it 'does not strip a path prefix by default' do + nested = Class.new(described_class) do + def self.name + 'FakeNested' + end + + get '/api/v2/widgets' do + json({ result: 'nested!' }) + end + end + stub_const('FakeNested', nested) + + WebValve.register nested.name, url: %r{\Ahttp://nested\.dev/api/v2/.*\z} + WebValve.setup + + expect(Net::HTTP.get(URI('http://nested.dev/api/v2/widgets'))).to eq({ result: 'nested!' }.to_json) + end + + it 'strips the configured path_prefix when one is given' do + WebValve.register subject.name, url: %r{\Ahttp://dummy\.dev/api/v2/.*\z}, path_prefix: '/api/v2' + WebValve.setup + + expect(Net::HTTP.get(URI('http://dummy.dev/api/v2/widgets'))).to eq({ result: 'it works!' }.to_json) + end + end + + context 'when the service is registered with an Addressable::Template url' do + it 'returns the result from the fake when the request URL matches the template' do + WebValve.register subject.name, url: Addressable::Template.new('http://dummy.dev{/path*}') + WebValve.setup + + expect(Net::HTTP.get(URI('http://dummy.dev/widgets'))).to eq({ result: 'it works!' }.to_json) + end + + it 'strips the configured path_prefix when one is given' do + WebValve.register subject.name, url: Addressable::Template.new('http://dummy.dev/api/v2{/path*}'), path_prefix: '/api/v2' + WebValve.setup + + expect(Net::HTTP.get(URI('http://dummy.dev/api/v2/widgets'))).to eq({ result: 'it works!' }.to_json) + end + end end end diff --git a/spec/webvalve/service_url_converter_spec.rb b/spec/webvalve/service_url_converter_spec.rb index d2157ca..8604057 100644 --- a/spec/webvalve/service_url_converter_spec.rb +++ b/spec/webvalve/service_url_converter_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'addressable/template' RSpec.describe WebValve::ServiceUrlConverter do let(:url) { "http://bar.com" } @@ -19,6 +20,14 @@ end end + context "with an Addressable::Template" do + let(:url) { Addressable::Template.new("http://foo.com{/path*}") } + + it "returns the same object so WebMock can match against it directly" do + expect(subject.regexp).to equal(url) + end + end + context "with an empty url" do let(:url) { "" }