Currently, setting up Knapsack Pro with WebMock/VCR requires additional steps because WebMock replaces Net::HTTP with its own client that (also) blocks requests to the Knapsack Pro API.
Some codebases configure WM in a way that doesn't play well with Knapsack Pro, even when the manual configuration steps are followed.
For example, each call to WebMock.disable_net_connect! without passing allow: ['api.knapsackpro.com'] overrides the previous WebMock.disable_net_connect!(allow: ['api.knapsackpro.com']) call preventing requests to api.knapsackpro.com.
Users could have multiple calls to WebMock that "reset" the configuration including in before/after hooks. So it's difficult to solve the issue by using hooks to undo the reset.
(Aside: WebMock.reset! does not touch WebMock::Config.instance.allow so that does not pose a problem, only disable_net_connection! does.)
This PR, introduces a mechanism to always call the original Net::HTTP for requests to the Knapsack Pro API preventing WM from interfering.
What follows are the internal details on why this PR works. It may be overwhelming to understand it, so feel free to ignore. But I'm leaving notes for reference.
Test
I created a webmock branch (same name as the branch for this PR) in rails-app-with-knapsack-pro with:
which means if VCR is on it does not allow WM to block anything, otherwise it delegates to WM because net_connect_allowed_without_vcr? is the original WM’s net_connect_allowed?.
If VCR is off, we are in the WM-only situation explained above.
Otherwise, VCR interacts with WM’s @webMockNetHTTP as follows.
VCR hooks into WM's global stubs:
::WebMock.globally_stub_request do |req|
global_hook_disabled?(req) ? nil : RequestHandler.new(req).handle
end
def request(request, body = nil, &block)
# ...
if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
# use stub
elsif WebMock.net_connect_allowed?(request_signature.uri)
# perform request
else
raise WebMock::NetConnectNotAllowedError.new(request_signature)
end
end
def request_type(consume_stub = false)
case
when externally_stubbed? then :externally_stubbed # if stubbed by WM, let WM return the stubbed response
when should_ignore? then :ignored # if ignored by VCR, let WM perform the request and record the cassette
when has_response_stub?(consume_stub) then :stubbed_by_vcr # let WM use the VCR cassette
when VCR.real_http_connections_allowed? then :recordable # let WM perform the request and record the cassette
else :unhandled # raise
end
end
def externally_stubbed? # WM stubbed the request?
# prevent infinite recursion...
VCR::LibraryHooks::WebMock.with_global_hook_disabled(request) do
::WebMock.registered_request?(request)
end
end
def should_ignore?
disabled? || VCR.request_ignorer.ignore?(vcr_request)
end
def disabled?
VCR.library_hooks.disabled?(library_name) # always false when using WM
end
[x] You follow the architecture outlined below for RSpec in Queue Mode, which is a work in progress (feel free to propose changes):
Pure: lib/knapsack_pro/pure/queue/rspec_pure.rb contains pure functions that are unit tested.
Extension: lib/knapsack_pro/extensions/rspec_extension.rb encapsulates calls to RSpec internals and is integration and e2e tested.
Runner: lib/knapsack_pro/runners/queue/rspec_runner.rb invokes the pure code and the extension to produce side effects, which are integration and e2e tested.
Story
Ticket https://trello.com/c/nInb3gxY/359-knapsackpro-gem-dx-remove-manual-steps-for-webmock-vcr
Currently, setting up Knapsack Pro with WebMock/VCR requires additional steps because WebMock replaces
Net::HTTP
with its own client that (also) blocks requests to the Knapsack Pro API.Some codebases configure WM in a way that doesn't play well with Knapsack Pro, even when the manual configuration steps are followed.
For example, each call to
WebMock.disable_net_connect!
without passingallow: ['api.knapsackpro.com']
overrides the previousWebMock.disable_net_connect!(allow: ['api.knapsackpro.com'])
call preventing requests toapi.knapsackpro.com
.Users could have multiple calls to
WebMock
that "reset" the configuration including in before/after hooks. So it's difficult to solve the issue by using hooks to undo the reset.(Aside:
WebMock.reset!
does not touchWebMock::Config.instance.allow
so that does not pose a problem, onlydisable_net_connection!
does.)This PR, introduces a mechanism to always call the original
Net::HTTP
for requests to the Knapsack Pro API preventing WM from interfering.What follows are the internal details on why this PR works. It may be overwhelming to understand it, so feel free to ignore. But I'm leaving notes for reference.
Test
I created a
webmock
branch (same name as the branch for this PR) inrails-app-with-knapsack-pro
with:Since the branch names are the same, CI will use that for the E2E tests of this PR.
Here are:
I also built the API with this PR but without removing the custom WM/VCR config:
And built without the custom WM/VCR config:
Details for WebMock
WebMock enables itself at the beginning of a test run. Here's how it's done for RSpec:
https://github.com/bblimke/webmock/blob/fc6a2ab897a069d861adbc1c88e51b2cf8aa88ac/lib/webmock/rspec.rb#L30-L32
enable!
ends up callingWebMock::HttpLibAdapters::NetHttpAdapter.enable!
:https://github.com/bblimke/webmock/blob/fc6a2ab897a069d861adbc1c88e51b2cf8aa88ac/lib/webmock/http_lib_adapters/net_http.rb#L16-L21
which replaces
Net::HTTP
with@webMockNetHTTP
:https://github.com/bblimke/webmock/blob/fc6a2ab897a069d861adbc1c88e51b2cf8aa88ac/lib/webmock/http_lib_adapters/net_http.rb#L40
Notice it also saves the original
Net::HTTP
inWebMock::HttpLibAdapters::NetHttpAdapter::OriginalNetHTTP
:https://github.com/bblimke/webmock/blob/fc6a2ab897a069d861adbc1c88e51b2cf8aa88ac/lib/webmock/http_lib_adapters/net_http.rb#L14
Details for WebMock and VCR
When WM is used in tandem with VCR, the situation gets a bit more complicated.
When VCR starts it enables WebMock and monkey patches WM's
net_connect_allowed?
to:https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/library_hooks/webmock.rb#L160-L171
which means if VCR is on it does not allow WM to block anything, otherwise it delegates to WM because
net_connect_allowed_without_vcr?
is the original WM’snet_connect_allowed?
.If VCR is off, we are in the WM-only situation explained above.
Otherwise, VCR interacts with WM’s
@webMockNetHTTP
as follows.VCR hooks into WM's global stubs:
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/library_hooks/webmock.rb#L134-L136
In turn, WM in
@webMockNetHTTP
:https://github.com/bblimke/webmock/blob/fc6a2ab897a069d861adbc1c88e51b2cf8aa88ac/lib/webmock/http_lib_adapters/net_http.rb#L71-L108
So
request
invokesWebMock::StubRegistry.instance.response_for_request(request_signature)
, which ends up calling VCR'sRequestHandler.new(req).handle
.WebMock::StubRegistry.instance.response_for_request(request_signature)
may return a stubbed response when:or it may return a fals-y value (
nil
) and let WM perform the request. Remember thatnet_connect_allowed?
is always true when using VCR.Here's how
handle
does it:https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/request_handler.rb#L6-L25
Where
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/request_handler.rb#L33-L41
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/library_hooks/webmock.rb#L97-L102
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/request_handler.rb#L58-L64
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/library_hooks/webmock.rb#L113-L129
https://github.com/vcr/vcr/blob/cde89346aebcbe5280299ba10aedb3d97860c557/lib/vcr/request_handler.rb#L87-L94
Checklist reminder
lib/knapsack_pro/pure/queue/rspec_pure.rb
contains pure functions that are unit tested.lib/knapsack_pro/extensions/rspec_extension.rb
encapsulates calls to RSpec internals and is integration and e2e tested.lib/knapsack_pro/runners/queue/rspec_runner.rb
invokes the pure code and the extension to produce side effects, which are integration and e2e tested.