This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'timeout'
|
||||
|
||||
module Faraday
|
||||
class Adapter
|
||||
# @example
|
||||
# test = Faraday::Connection.new do
|
||||
# use Faraday::Adapter::Test do |stub|
|
||||
# # Define matcher to match the request
|
||||
# stub.get '/resource.json' do
|
||||
# # return static content
|
||||
# [200, {'Content-Type' => 'application/json'}, 'hi world']
|
||||
# end
|
||||
#
|
||||
# # response with content generated based on request
|
||||
# stub.get '/showget' do |env|
|
||||
# [200, {'Content-Type' => 'text/plain'}, env[:method].to_s]
|
||||
# end
|
||||
#
|
||||
# # A regular expression can be used as matching filter
|
||||
# stub.get /\A\/items\/(\d+)\z/ do |env, meta|
|
||||
# # in case regular expression is used, an instance of MatchData
|
||||
# # can be received
|
||||
# [200,
|
||||
# {'Content-Type' => 'text/plain'},
|
||||
# "showing item: #{meta[:match_data][1]}"
|
||||
# ]
|
||||
# end
|
||||
#
|
||||
# # Test the request body is the same as the stubbed body
|
||||
# stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] }
|
||||
#
|
||||
# # You can pass a proc as a stubbed body and check the request body in your way.
|
||||
# # In this case, the proc should return true or false.
|
||||
# stub.post('/foo', ->(request_body) do
|
||||
# JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, '']
|
||||
# end
|
||||
#
|
||||
# # You can set strict_mode to exactly match the stubbed requests.
|
||||
# stub.strict_mode = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# resp = test.get '/resource.json'
|
||||
# resp.body # => 'hi world'
|
||||
#
|
||||
# resp = test.get '/showget'
|
||||
# resp.body # => 'get'
|
||||
#
|
||||
# resp = test.get '/items/1'
|
||||
# resp.body # => 'showing item: 1'
|
||||
#
|
||||
# resp = test.get '/items/2'
|
||||
# resp.body # => 'showing item: 2'
|
||||
#
|
||||
# resp = test.post '/bar', 'name=YK&word=call'
|
||||
# resp.status # => 200
|
||||
#
|
||||
# resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now)
|
||||
# resp.status # => 200
|
||||
class Test < Faraday::Adapter
|
||||
attr_accessor :stubs
|
||||
|
||||
# A stack of Stubs
|
||||
class Stubs
|
||||
class NotFound < StandardError
|
||||
end
|
||||
|
||||
def initialize(strict_mode: false)
|
||||
# { get: [Stub, Stub] }
|
||||
@stack = {}
|
||||
@consumed = {}
|
||||
@strict_mode = strict_mode
|
||||
@stubs_mutex = Monitor.new
|
||||
yield(self) if block_given?
|
||||
end
|
||||
|
||||
def empty?
|
||||
@stack.empty?
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def match(env)
|
||||
request_method = env[:method]
|
||||
return false unless @stack.key?(request_method)
|
||||
|
||||
stack = @stack[request_method]
|
||||
consumed = (@consumed[request_method] ||= [])
|
||||
|
||||
@stubs_mutex.synchronize do
|
||||
stub, meta = matches?(stack, env)
|
||||
if stub
|
||||
removed = stack.delete(stub)
|
||||
consumed << removed unless removed.nil?
|
||||
return stub, meta
|
||||
end
|
||||
end
|
||||
matches?(consumed, env)
|
||||
end
|
||||
|
||||
def get(path, headers = {}, &block)
|
||||
new_stub(:get, path, headers, &block)
|
||||
end
|
||||
|
||||
def head(path, headers = {}, &block)
|
||||
new_stub(:head, path, headers, &block)
|
||||
end
|
||||
|
||||
def post(path, body = nil, headers = {}, &block)
|
||||
new_stub(:post, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def put(path, body = nil, headers = {}, &block)
|
||||
new_stub(:put, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def patch(path, body = nil, headers = {}, &block)
|
||||
new_stub(:patch, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def delete(path, headers = {}, &block)
|
||||
new_stub(:delete, path, headers, &block)
|
||||
end
|
||||
|
||||
def options(path, headers = {}, &block)
|
||||
new_stub(:options, path, headers, &block)
|
||||
end
|
||||
|
||||
# Raises an error if any of the stubbed calls have not been made.
|
||||
def verify_stubbed_calls
|
||||
failed_stubs = []
|
||||
@stack.each do |method, stubs|
|
||||
next if stubs.empty?
|
||||
|
||||
failed_stubs.concat(
|
||||
stubs.map do |stub|
|
||||
"Expected #{method} #{stub}."
|
||||
end
|
||||
)
|
||||
end
|
||||
raise failed_stubs.join(' ') unless failed_stubs.empty?
|
||||
end
|
||||
|
||||
# Set strict_mode. If the value is true, this adapter tries to find matched requests strictly,
|
||||
# which means that all of a path, parameters, and headers must be the same as an actual request.
|
||||
def strict_mode=(value)
|
||||
@strict_mode = value
|
||||
@stack.each_value do |stubs|
|
||||
stubs.each do |stub|
|
||||
stub.strict_mode = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def new_stub(request_method, path, headers = {}, body = nil, &block)
|
||||
normalized_path, host =
|
||||
if path.is_a?(Regexp)
|
||||
path
|
||||
else
|
||||
[
|
||||
Faraday::Utils.normalize_path(path),
|
||||
Faraday::Utils.URI(path).host
|
||||
]
|
||||
end
|
||||
path, query = normalized_path.respond_to?(:split) ? normalized_path.split('?') : normalized_path
|
||||
headers = Utils::Headers.new(headers)
|
||||
|
||||
stub = Stub.new(host, path, query, headers, body, @strict_mode, block)
|
||||
(@stack[request_method] ||= []) << stub
|
||||
end
|
||||
|
||||
# @param stack [Hash]
|
||||
# @param env [Faraday::Env]
|
||||
def matches?(stack, env)
|
||||
stack.each do |stub|
|
||||
match_result, meta = stub.matches?(env)
|
||||
return stub, meta if match_result
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Stub request
|
||||
Stub = Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) do
|
||||
# @param env [Faraday::Env]
|
||||
def matches?(env)
|
||||
request_host = env[:url].host
|
||||
request_path = Faraday::Utils.normalize_path(env[:url].path)
|
||||
request_headers = env.request_headers
|
||||
request_body = env[:body]
|
||||
|
||||
# meta is a hash used as carrier
|
||||
# that will be yielded to consumer block
|
||||
meta = {}
|
||||
[(host.nil? || host == request_host) &&
|
||||
path_match?(request_path, meta) &&
|
||||
params_match?(env) &&
|
||||
body_match?(request_body) &&
|
||||
headers_match?(request_headers), meta]
|
||||
end
|
||||
|
||||
def path_match?(request_path, meta)
|
||||
if path.is_a?(Regexp)
|
||||
!!(meta[:match_data] = path.match(request_path))
|
||||
else
|
||||
path == request_path
|
||||
end
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def params_match?(env)
|
||||
request_params = env[:params]
|
||||
params = env.params_encoder.decode(query) || {}
|
||||
|
||||
if strict_mode
|
||||
return Set.new(params) == Set.new(request_params)
|
||||
end
|
||||
|
||||
params.keys.all? do |key|
|
||||
request_params[key] == params[key]
|
||||
end
|
||||
end
|
||||
|
||||
def headers_match?(request_headers)
|
||||
if strict_mode
|
||||
headers_with_user_agent = headers.dup.tap do |hs|
|
||||
# NOTE: Set User-Agent in case it's not set when creating Stubs.
|
||||
# Users would not want to set Faraday's User-Agent explicitly.
|
||||
hs[:user_agent] ||= Connection::USER_AGENT
|
||||
end
|
||||
return Set.new(headers_with_user_agent) == Set.new(request_headers)
|
||||
end
|
||||
|
||||
headers.keys.all? do |key|
|
||||
request_headers[key] == headers[key]
|
||||
end
|
||||
end
|
||||
|
||||
def body_match?(request_body)
|
||||
return true if body.to_s.empty?
|
||||
|
||||
case body
|
||||
when Proc
|
||||
body.call(request_body)
|
||||
else
|
||||
request_body == body
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{path} #{body}"
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app, stubs = nil, &block)
|
||||
super(app)
|
||||
@stubs = stubs || Stubs.new
|
||||
configure(&block) if block
|
||||
end
|
||||
|
||||
def configure
|
||||
yield(stubs)
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def call(env)
|
||||
super
|
||||
|
||||
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
|
||||
env[:params] = env.params_encoder.decode(env[:url].query) || {}
|
||||
stub, meta = stubs.match(env)
|
||||
|
||||
unless stub
|
||||
raise Stubs::NotFound, "no stubbed request for #{env[:method]} " \
|
||||
"#{env[:url]} #{env[:body]} #{env[:headers]}"
|
||||
end
|
||||
|
||||
block_arity = stub.block.arity
|
||||
params = if block_arity >= 0
|
||||
[env, meta].take(block_arity)
|
||||
else
|
||||
[env, meta]
|
||||
end
|
||||
|
||||
timeout = request_timeout(:open, env[:request])
|
||||
timeout ||= request_timeout(:read, env[:request])
|
||||
|
||||
status, headers, body =
|
||||
if timeout
|
||||
::Timeout.timeout(timeout, Faraday::TimeoutError) do
|
||||
stub.block.call(*params)
|
||||
end
|
||||
else
|
||||
stub.block.call(*params)
|
||||
end
|
||||
|
||||
# We need to explicitly pass `reason_phrase = nil` here to avoid keyword args conflicts.
|
||||
# See https://github.com/lostisland/faraday/issues/1444
|
||||
# TODO: remove `nil` explicit reason_phrase once Ruby 3.0 becomes minimum req. version
|
||||
save_response(env, status, body, headers, nil)
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Adapter.register_middleware(test: Faraday::Adapter::Test)
|
||||
Reference in New Issue
Block a user