vcr 2.0.0.rc1 → 2.0.0.rc2
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.
- data/.gitignore +2 -0
- data/.limited_red +1 -0
- data/.travis.yml +10 -1
- data/.yardopts +9 -0
- data/CHANGELOG.md +51 -1
- data/Gemfile +5 -1
- data/LICENSE +1 -1
- data/README.md +23 -28
- data/Rakefile +63 -18
- data/Upgrade.md +200 -0
- data/features/.nav +2 -0
- data/features/cassettes/automatic_re_recording.feature +19 -15
- data/features/cassettes/dynamic_erb.feature +12 -4
- data/features/cassettes/exclusive.feature +31 -23
- data/features/cassettes/format.feature +54 -30
- data/features/cassettes/naming.feature +1 -1
- data/features/cassettes/update_content_length_header.feature +16 -12
- data/features/configuration/allow_http_connections_when_no_cassette.feature +1 -1
- data/features/configuration/debug_logging.feature +52 -0
- data/features/configuration/filter_sensitive_data.feature +4 -4
- data/features/configuration/hook_into.feature +5 -2
- data/features/configuration/ignore_request.feature +5 -3
- data/features/configuration/preserve_exact_body_bytes.feature +103 -0
- data/features/hooks/after_http_request.feature +17 -4
- data/features/hooks/around_http_request.feature +2 -1
- data/features/hooks/before_http_request.feature +25 -8
- data/features/hooks/before_playback.feature +16 -12
- data/features/hooks/before_record.feature +2 -2
- data/features/http_libraries/em_http_request.feature +82 -58
- data/features/http_libraries/net_http.feature +6 -6
- data/features/middleware/faraday.feature +2 -1
- data/features/middleware/rack.feature +2 -2
- data/features/record_modes/all.feature +19 -15
- data/features/record_modes/new_episodes.feature +17 -13
- data/features/record_modes/none.feature +15 -11
- data/features/record_modes/once.feature +16 -12
- data/features/request_matching/body.feature +28 -20
- data/features/request_matching/custom_matcher.feature +28 -20
- data/features/request_matching/headers.feature +34 -26
- data/features/request_matching/host.feature +28 -20
- data/features/request_matching/identical_request_sequence.feature +28 -20
- data/features/request_matching/method.feature +28 -20
- data/features/request_matching/path.feature +28 -20
- data/features/request_matching/playback_repeats.feature +28 -20
- data/features/request_matching/uri.feature +28 -20
- data/features/request_matching/uri_without_param.feature +28 -20
- data/features/support/env.rb +7 -6
- data/features/support/vcr_cucumber_helpers.rb +1 -0
- data/features/test_frameworks/cucumber.feature +8 -8
- data/features/test_frameworks/rspec_macro.feature +4 -4
- data/features/test_frameworks/rspec_metadata.feature +6 -6
- data/features/test_frameworks/shoulda.feature +1 -1
- data/features/test_frameworks/test_unit.feature +1 -1
- data/lib/vcr.rb +156 -5
- data/lib/vcr/cassette.rb +80 -30
- data/lib/vcr/cassette/http_interaction_list.rb +33 -4
- data/lib/vcr/cassette/migrator.rb +2 -3
- data/lib/vcr/cassette/reader.rb +1 -0
- data/lib/vcr/cassette/serializers.rb +22 -0
- data/lib/vcr/cassette/serializers/json.rb +27 -2
- data/lib/vcr/cassette/serializers/psych.rb +26 -2
- data/lib/vcr/cassette/serializers/syck.rb +28 -2
- data/lib/vcr/cassette/serializers/yaml.rb +28 -2
- data/lib/vcr/configuration.rb +348 -10
- data/lib/vcr/deprecations.rb +8 -0
- data/lib/vcr/errors.rb +40 -0
- data/lib/vcr/extensions/net_http_response.rb +12 -11
- data/lib/vcr/library_hooks.rb +1 -0
- data/lib/vcr/library_hooks/excon.rb +24 -3
- data/lib/vcr/library_hooks/fakeweb.rb +32 -16
- data/lib/vcr/library_hooks/faraday.rb +3 -0
- data/lib/vcr/library_hooks/typhoeus.rb +40 -37
- data/lib/vcr/library_hooks/webmock.rb +54 -34
- data/lib/vcr/middleware/faraday.rb +13 -0
- data/lib/vcr/middleware/rack.rb +35 -0
- data/lib/vcr/request_handler.rb +60 -8
- data/lib/vcr/request_ignorer.rb +1 -0
- data/lib/vcr/request_matcher_registry.rb +28 -0
- data/lib/vcr/structs.rb +245 -38
- data/lib/vcr/test_frameworks/cucumber.rb +10 -0
- data/lib/vcr/test_frameworks/rspec.rb +26 -1
- data/lib/vcr/util/hooks.rb +29 -27
- data/lib/vcr/util/internet_connection.rb +2 -0
- data/lib/vcr/util/logger.rb +25 -0
- data/lib/vcr/util/variable_args_block_caller.rb +1 -0
- data/lib/vcr/util/version_checker.rb +1 -0
- data/lib/vcr/version.rb +8 -1
- data/spec/capture_warnings.rb +3 -3
- data/spec/monkey_patches.rb +28 -13
- data/spec/spec_helper.rb +17 -0
- data/spec/support/http_library_adapters.rb +7 -4
- data/spec/support/shared_example_groups/hook_into_http_library.rb +96 -32
- data/spec/support/shared_example_groups/request_hooks.rb +9 -8
- data/spec/support/sinatra_app.rb +3 -1
- data/spec/support/vcr_localhost_server.rb +1 -0
- data/spec/vcr/cassette/http_interaction_list_spec.rb +119 -54
- data/spec/vcr/cassette/migrator_spec.rb +19 -6
- data/spec/vcr/cassette/serializers_spec.rb +51 -6
- data/spec/vcr/cassette_spec.rb +44 -19
- data/spec/vcr/configuration_spec.rb +91 -6
- data/spec/vcr/library_hooks/excon_spec.rb +54 -16
- data/spec/vcr/library_hooks/fakeweb_spec.rb +12 -21
- data/spec/vcr/library_hooks/typhoeus_spec.rb +2 -29
- data/spec/vcr/library_hooks/webmock_spec.rb +4 -18
- data/spec/vcr/middleware/faraday_spec.rb +1 -16
- data/spec/vcr/structs_spec.rb +194 -61
- data/spec/vcr/test_frameworks/rspec_spec.rb +10 -0
- data/spec/vcr/util/hooks_spec.rb +104 -56
- data/spec/vcr/util/version_checker_spec.rb +45 -0
- data/spec/vcr_spec.rb +11 -0
- data/vcr.gemspec +30 -34
- metadata +149 -95
- data/spec/support/shared_example_groups/version_checking.rb +0 -34
|
@@ -5,19 +5,32 @@ require 'vcr/request_handler'
|
|
|
5
5
|
VCR::VersionChecker.new('Faraday', Faraday::VERSION, '0.7.0', '0.7').check_version!
|
|
6
6
|
|
|
7
7
|
module VCR
|
|
8
|
+
# Contains middlewares for use with different libraries.
|
|
8
9
|
module Middleware
|
|
10
|
+
# Faraday middleware that VCR uses to record and replay HTTP requests made through
|
|
11
|
+
# Faraday.
|
|
12
|
+
#
|
|
13
|
+
# @note You can either insert this middleware into the Faraday middleware stack
|
|
14
|
+
# yourself or configure {VCR::Configuration#hook_into} to hook into +:faraday+.
|
|
9
15
|
class Faraday
|
|
10
16
|
include VCR::Deprecations::Middleware::Faraday
|
|
11
17
|
|
|
18
|
+
# Constructs a new instance of the Faraday middleware.
|
|
19
|
+
#
|
|
20
|
+
# @param [#call] the faraday app
|
|
12
21
|
def initialize(app)
|
|
13
22
|
super
|
|
14
23
|
@app = app
|
|
15
24
|
end
|
|
16
25
|
|
|
26
|
+
# Handles the HTTP request being made through Faraday
|
|
27
|
+
#
|
|
28
|
+
# @param [Hash] env the Faraday request env hash
|
|
17
29
|
def call(env)
|
|
18
30
|
RequestHandler.new(@app, env).handle
|
|
19
31
|
end
|
|
20
32
|
|
|
33
|
+
# @private
|
|
21
34
|
class RequestHandler < ::VCR::RequestHandler
|
|
22
35
|
attr_reader :app, :env
|
|
23
36
|
def initialize(app, env)
|
data/lib/vcr/middleware/rack.rb
CHANGED
|
@@ -1,29 +1,64 @@
|
|
|
1
1
|
module VCR
|
|
2
2
|
module Middleware
|
|
3
|
+
# Object yielded by VCR's {Rack} middleware that allows you to configure
|
|
4
|
+
# the cassette dynamically based on the rack env.
|
|
3
5
|
class CassetteArguments
|
|
6
|
+
# @private
|
|
4
7
|
def initialize
|
|
5
8
|
@name = nil
|
|
6
9
|
@options = {}
|
|
7
10
|
end
|
|
8
11
|
|
|
12
|
+
# Sets (and gets) the cassette name.
|
|
13
|
+
#
|
|
14
|
+
# @param [#to_s] name the cassette name
|
|
15
|
+
# @return [#to_s] the cassette name
|
|
9
16
|
def name(name = nil)
|
|
10
17
|
@name = name if name
|
|
11
18
|
@name
|
|
12
19
|
end
|
|
13
20
|
|
|
21
|
+
# Sets (and gets) the cassette options.
|
|
22
|
+
#
|
|
23
|
+
# @param [Hash] options the cassette options
|
|
24
|
+
# @return [Hash] the cassette options
|
|
14
25
|
def options(options = {})
|
|
15
26
|
@options.merge!(options)
|
|
16
27
|
end
|
|
17
28
|
end
|
|
18
29
|
|
|
30
|
+
# Rack middleware that uses a VCR cassette for each incoming HTTP request.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# app = Rack::Builder.new do
|
|
34
|
+
# use VCR::Middleware::Rack do |cassette, env|
|
|
35
|
+
# cassette.name "rack/#{env['SERVER_NAME']}"
|
|
36
|
+
# cassette.options :record => :new_episodes
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# run MyRackApp
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# @note This will record/replay _outbound_ HTTP requests made by your rack app.
|
|
19
43
|
class Rack
|
|
20
44
|
include VCR::VariableArgsBlockCaller
|
|
21
45
|
|
|
46
|
+
# Constructs a new instance of VCR's rack middleware.
|
|
47
|
+
#
|
|
48
|
+
# @param [#call] app the rack app
|
|
49
|
+
# @yield the cassette configuration block
|
|
50
|
+
# @yieldparam [CassetteArguments] cassette the cassette configuration object
|
|
51
|
+
# @yieldparam [(optional) Hash] env the rack env hash
|
|
52
|
+
# @raise [ArgumentError] if no configuration block is provided
|
|
22
53
|
def initialize(app, &block)
|
|
23
54
|
raise ArgumentError.new("You must provide a block to set the cassette options") unless block
|
|
24
55
|
@app, @cassette_arguments_block, @mutex = app, block, Mutex.new
|
|
25
56
|
end
|
|
26
57
|
|
|
58
|
+
# Implements the rack middleware interface.
|
|
59
|
+
#
|
|
60
|
+
# @param [Hash] env the rack env hash
|
|
61
|
+
# @return [Array(Integer, Hash, #each)] the rack response
|
|
27
62
|
def call(env)
|
|
28
63
|
@mutex.synchronize do
|
|
29
64
|
VCR.use_cassette(*cassette_arguments(env)) do
|
data/lib/vcr/request_handler.rb
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
module VCR
|
|
2
|
+
# @private
|
|
2
3
|
class RequestHandler
|
|
4
|
+
include Logger
|
|
5
|
+
|
|
3
6
|
def handle
|
|
7
|
+
log "Handling request: #{request_summary} (disabled: #{disabled?})"
|
|
4
8
|
invoke_before_request_hook
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
req_type = request_type(:consume_stub)
|
|
11
|
+
|
|
12
|
+
log "Identified request type (#{req_type}) for #{request_summary}"
|
|
13
|
+
|
|
14
|
+
# The before_request hook can change the type of request
|
|
15
|
+
# (i.e. by inserting a cassette), so we need to query the
|
|
16
|
+
# request type again.
|
|
17
|
+
#
|
|
18
|
+
# Likewise, the main handler logic an modify what
|
|
19
|
+
# #request_type would return (i.e. when a response stub is
|
|
20
|
+
# used), so we need to store the request type for the
|
|
21
|
+
# the after_request hook.
|
|
22
|
+
set_typed_request_for_after_hook(req_type)
|
|
23
|
+
|
|
24
|
+
send "on_#{req_type}_request"
|
|
9
25
|
end
|
|
10
26
|
|
|
11
27
|
private
|
|
12
28
|
|
|
29
|
+
def set_typed_request_for_after_hook(request_type)
|
|
30
|
+
@after_hook_typed_request = Request::Typed.new(vcr_request, request_type)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def request_type(consume_stub = false)
|
|
34
|
+
case
|
|
35
|
+
when should_ignore? then :ignored
|
|
36
|
+
when has_response_stub?(consume_stub) then :stubbed
|
|
37
|
+
when VCR.real_http_connections_allowed? then :recordable
|
|
38
|
+
else :unhandled
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
13
42
|
def invoke_before_request_hook
|
|
14
|
-
return if disabled?
|
|
15
|
-
|
|
43
|
+
return if disabled? || !VCR.configuration.has_hooks_for?(:before_http_request)
|
|
44
|
+
typed_request = Request::Typed.new(vcr_request, request_type)
|
|
45
|
+
VCR.configuration.invoke_hook(:before_http_request, typed_request)
|
|
16
46
|
end
|
|
17
47
|
|
|
18
48
|
def invoke_after_request_hook(vcr_response)
|
|
19
49
|
return if disabled?
|
|
20
|
-
VCR.configuration.invoke_hook(:after_http_request,
|
|
50
|
+
VCR.configuration.invoke_hook(:after_http_request, @after_hook_typed_request, vcr_response)
|
|
21
51
|
end
|
|
22
52
|
|
|
23
53
|
def should_ignore?
|
|
@@ -28,6 +58,14 @@ module VCR
|
|
|
28
58
|
VCR.library_hooks.disabled?(library_name)
|
|
29
59
|
end
|
|
30
60
|
|
|
61
|
+
def has_response_stub?(consume_stub)
|
|
62
|
+
if consume_stub
|
|
63
|
+
stubbed_response
|
|
64
|
+
else
|
|
65
|
+
VCR.http_interactions.has_interaction_matching?(vcr_request)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
31
69
|
def stubbed_response
|
|
32
70
|
@stubbed_response ||= VCR.http_interactions.response_for(vcr_request)
|
|
33
71
|
end
|
|
@@ -47,8 +85,22 @@ module VCR
|
|
|
47
85
|
def on_recordable_request
|
|
48
86
|
end
|
|
49
87
|
|
|
50
|
-
def
|
|
88
|
+
def on_unhandled_request
|
|
51
89
|
raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request)
|
|
52
90
|
end
|
|
91
|
+
|
|
92
|
+
def request_summary
|
|
93
|
+
request_matchers = if cass = VCR.current_cassette
|
|
94
|
+
cass.match_requests_on
|
|
95
|
+
else
|
|
96
|
+
VCR.configuration.default_cassette_options[:match_requests_on]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
super(vcr_request, request_matchers)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def log_prefix
|
|
103
|
+
"[#{library_name}] "
|
|
104
|
+
end
|
|
53
105
|
end
|
|
54
106
|
end
|
data/lib/vcr/request_ignorer.rb
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
require 'vcr/errors'
|
|
2
2
|
|
|
3
3
|
module VCR
|
|
4
|
+
# Keeps track of the different request matchers.
|
|
4
5
|
class RequestMatcherRegistry
|
|
6
|
+
|
|
7
|
+
# The default request matchers used for any cassette that does not
|
|
8
|
+
# specify request matchers.
|
|
5
9
|
DEFAULT_MATCHERS = [:method, :uri]
|
|
6
10
|
|
|
11
|
+
# @private
|
|
7
12
|
class Matcher < Struct.new(:callable)
|
|
8
13
|
def matches?(request_1, request_2)
|
|
9
14
|
callable.call(request_1, request_2)
|
|
10
15
|
end
|
|
11
16
|
end
|
|
12
17
|
|
|
18
|
+
# @private
|
|
13
19
|
class URIWithoutParamsMatcher < Struct.new(:params_to_ignore)
|
|
14
20
|
def partial_uri_from(request)
|
|
15
21
|
URI(request.uri).tap do |uri|
|
|
@@ -37,11 +43,13 @@ module VCR
|
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# @private
|
|
40
47
|
def initialize
|
|
41
48
|
@registry = {}
|
|
42
49
|
register_built_ins
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
# @private
|
|
45
53
|
def register(name, &block)
|
|
46
54
|
if @registry.has_key?(name)
|
|
47
55
|
warn "WARNING: There is already a VCR request matcher registered for #{name.inspect}. Overriding it."
|
|
@@ -50,6 +58,7 @@ module VCR
|
|
|
50
58
|
@registry[name] = Matcher.new(block)
|
|
51
59
|
end
|
|
52
60
|
|
|
61
|
+
# @private
|
|
53
62
|
def [](matcher)
|
|
54
63
|
@registry.fetch(matcher) do
|
|
55
64
|
matcher.respond_to?(:call) ?
|
|
@@ -58,6 +67,25 @@ module VCR
|
|
|
58
67
|
end
|
|
59
68
|
end
|
|
60
69
|
|
|
70
|
+
# Builds a dynamic request matcher that matches on a URI while ignoring the
|
|
71
|
+
# named query parameters. This is useful for dealing with non-deterministic
|
|
72
|
+
# URIs (i.e. that have a timestamp or request signature parameter).
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# without_timestamp = VCR.request_matchers.uri_without_param(:timestamp)
|
|
76
|
+
#
|
|
77
|
+
# # use it directly...
|
|
78
|
+
# VCR.use_cassette('example', :match_requests_on => [:method, without_timestamp]) { }
|
|
79
|
+
#
|
|
80
|
+
# # ...or register it as a named matcher
|
|
81
|
+
# VCR.configure do |c|
|
|
82
|
+
# c.register_request_matcher(:uri_without_timestamp, &without_timestamp)
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# VCR.use_cassette('example', :match_requests_on => [:method, :uri_without_timestamp]) { }
|
|
86
|
+
#
|
|
87
|
+
# @param ignores [Array<#to_s>] The names of the query parameters to ignore
|
|
88
|
+
# @return [#call] the request matcher
|
|
61
89
|
def uri_without_params(*ignores)
|
|
62
90
|
uri_without_param_matchers[ignores]
|
|
63
91
|
end
|
data/lib/vcr/structs.rb
CHANGED
|
@@ -1,9 +1,57 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'delegate'
|
|
1
3
|
require 'time'
|
|
2
|
-
require 'forwardable'
|
|
3
4
|
|
|
4
5
|
module VCR
|
|
6
|
+
# @private
|
|
5
7
|
module Normalizers
|
|
8
|
+
# @private
|
|
6
9
|
module Body
|
|
10
|
+
def self.included(klass)
|
|
11
|
+
klass.extend ClassMethods
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @private
|
|
15
|
+
module ClassMethods
|
|
16
|
+
def body_from(hash_or_string)
|
|
17
|
+
return hash_or_string unless hash_or_string.is_a?(Hash)
|
|
18
|
+
hash = hash_or_string
|
|
19
|
+
|
|
20
|
+
if hash.has_key?('base64_string')
|
|
21
|
+
string = Base64.decode64(hash['base64_string'])
|
|
22
|
+
force_encode_string(string, hash['encoding'])
|
|
23
|
+
else
|
|
24
|
+
try_encode_string(hash['string'], hash['encoding'])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if "".respond_to?(:encoding)
|
|
29
|
+
def force_encode_string(string, encoding)
|
|
30
|
+
return string unless encoding
|
|
31
|
+
string.force_encoding(encoding)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def try_encode_string(string, encoding)
|
|
35
|
+
return string if string.encoding.name == encoding
|
|
36
|
+
string.encode(encoding)
|
|
37
|
+
rescue EncodingError => e
|
|
38
|
+
struct_type = name.split('::').last.downcase
|
|
39
|
+
warn "VCR: got `#{e.class.name}: #{e.message}` while trying to encode the #{string.encoding.name} " +
|
|
40
|
+
"#{struct_type} body to the original body encoding (#{encoding}). Consider using the " +
|
|
41
|
+
"`:preserve_exact_body_bytes` option to work around this."
|
|
42
|
+
return string
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
def force_encode_string(string, encoding)
|
|
46
|
+
string
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def try_encode_string(string, encoding)
|
|
50
|
+
string
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
7
55
|
def initialize(*args)
|
|
8
56
|
super
|
|
9
57
|
# Ensure that the body is a raw string, in case the string instance
|
|
@@ -13,8 +61,29 @@ module VCR
|
|
|
13
61
|
# http://github.com/myronmarston/vcr/issues/4
|
|
14
62
|
self.body = String.new(body.to_s)
|
|
15
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def serializable_body
|
|
68
|
+
if VCR.configuration.preserve_exact_body_bytes_for?(self)
|
|
69
|
+
base_body_hash(body).merge('base64_string' => Base64.encode64(body))
|
|
70
|
+
else
|
|
71
|
+
base_body_hash(body).merge('string' => body)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if ''.respond_to?(:encoding)
|
|
76
|
+
def base_body_hash(body)
|
|
77
|
+
{ 'encoding' => body.encoding.name }
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
def base_body_hash(body)
|
|
81
|
+
{ }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
16
84
|
end
|
|
17
85
|
|
|
86
|
+
# @private
|
|
18
87
|
module Header
|
|
19
88
|
def initialize(*args)
|
|
20
89
|
super
|
|
@@ -56,6 +125,7 @@ module VCR
|
|
|
56
125
|
end
|
|
57
126
|
end
|
|
58
127
|
|
|
128
|
+
# @private
|
|
59
129
|
module OrderedHashSerializer
|
|
60
130
|
def each
|
|
61
131
|
@ordered_keys.each do |key|
|
|
@@ -74,6 +144,12 @@ module VCR
|
|
|
74
144
|
end
|
|
75
145
|
end
|
|
76
146
|
|
|
147
|
+
# The request of an {HTTPInteraction}.
|
|
148
|
+
#
|
|
149
|
+
# @attr [Symbol] method the HTTP method (i.e. :head, :options, :get, :post, :put, :patch or :delete)
|
|
150
|
+
# @attr [String] uri the request URI
|
|
151
|
+
# @attr [String, nil] body the request body
|
|
152
|
+
# @attr [Hash{String => Array<String>}] headers the request headers
|
|
77
153
|
class Request < Struct.new(:method, :uri, :body, :headers)
|
|
78
154
|
include Normalizers::Header
|
|
79
155
|
include Normalizers::Body
|
|
@@ -84,21 +160,30 @@ module VCR
|
|
|
84
160
|
self.uri = without_standard_port(self.uri)
|
|
85
161
|
end
|
|
86
162
|
|
|
163
|
+
# Builds a serializable hash from the request data.
|
|
164
|
+
#
|
|
165
|
+
# @return [Hash] hash that represents this request and can be easily
|
|
166
|
+
# serialized.
|
|
167
|
+
# @see Request.from_hash
|
|
87
168
|
def to_hash
|
|
88
169
|
{
|
|
89
170
|
'method' => method.to_s,
|
|
90
171
|
'uri' => uri,
|
|
91
|
-
'body' =>
|
|
172
|
+
'body' => serializable_body,
|
|
92
173
|
'headers' => headers
|
|
93
174
|
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
|
|
94
175
|
end
|
|
95
176
|
|
|
177
|
+
# Constructs a new instance from a hash.
|
|
178
|
+
#
|
|
179
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
180
|
+
# @return [Request] the request
|
|
96
181
|
def self.from_hash(hash)
|
|
97
182
|
method = hash['method']
|
|
98
183
|
method &&= method.to_sym
|
|
99
184
|
new method,
|
|
100
185
|
hash['uri'],
|
|
101
|
-
hash['body'],
|
|
186
|
+
body_from(hash['body']),
|
|
102
187
|
hash['headers']
|
|
103
188
|
end
|
|
104
189
|
|
|
@@ -108,19 +193,75 @@ module VCR
|
|
|
108
193
|
@@object_method.bind(self).call(*args)
|
|
109
194
|
end
|
|
110
195
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
196
|
+
# Decorates a {Request} with its current type.
|
|
197
|
+
class Typed < DelegateClass(self)
|
|
198
|
+
# @return [Symbol] One of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
|
|
199
|
+
attr_reader :type
|
|
200
|
+
|
|
201
|
+
# @param [Request] request the request
|
|
202
|
+
# @param [Symbol] type the type. Should be one of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
|
|
203
|
+
def initialize(request, type)
|
|
204
|
+
@type = type
|
|
205
|
+
super(request)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @return [Boolean] whether or not this request is being ignored
|
|
209
|
+
def ignored?
|
|
210
|
+
type == :ignored
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @return [Boolean] whether or not this request will be stubbed
|
|
214
|
+
def stubbed?
|
|
215
|
+
type == :stubbed
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [Boolean] whether or not this request will be recorded.
|
|
219
|
+
def recordable?
|
|
220
|
+
type == :recordable
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @return [Boolean] whether or not VCR knows how to handle this request.
|
|
224
|
+
def unhandled?
|
|
225
|
+
type == :unhandled
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @return [Boolean] whether or not this request will be made for real.
|
|
229
|
+
# @note VCR allows `:ignored` and `:recordable` requests to be made for real.
|
|
230
|
+
def real?
|
|
231
|
+
ignored? || recordable?
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
undef method
|
|
114
235
|
end
|
|
115
236
|
|
|
116
|
-
|
|
237
|
+
# Provides fiber-awareness for the {VCR::Configuration#around_http_request} hook.
|
|
238
|
+
class FiberAware < DelegateClass(Typed)
|
|
239
|
+
# Yields the fiber so the request can proceed.
|
|
240
|
+
#
|
|
241
|
+
# @return [VCR::Response] the response from the request
|
|
117
242
|
def proceed
|
|
118
243
|
Fiber.yield
|
|
119
244
|
end
|
|
120
245
|
|
|
246
|
+
# Builds a proc that allows the request to proceed when called.
|
|
247
|
+
# This allows you to treat the request as a proc and pass it on
|
|
248
|
+
# to a method that yields (at which point the request will proceed).
|
|
249
|
+
#
|
|
250
|
+
# @return [Proc] the proc
|
|
121
251
|
def to_proc
|
|
122
252
|
lambda { proceed }
|
|
123
253
|
end
|
|
254
|
+
|
|
255
|
+
undef method
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Transforms the request into a fiber aware one by extending
|
|
259
|
+
# the {FiberAware} module onto the instance. Necessary for the
|
|
260
|
+
# {VCR::Configuration#around_http_request} hook.
|
|
261
|
+
#
|
|
262
|
+
# @return [Request] the request instance
|
|
263
|
+
def fiber_aware
|
|
264
|
+
extend FiberAware
|
|
124
265
|
end
|
|
125
266
|
|
|
126
267
|
private
|
|
@@ -134,16 +275,22 @@ module VCR
|
|
|
134
275
|
end
|
|
135
276
|
end
|
|
136
277
|
|
|
278
|
+
# Represents a single interaction over HTTP, containing a request and a response.
|
|
279
|
+
#
|
|
280
|
+
# @attr [Request] request the request
|
|
281
|
+
# @attr [Response] response the response
|
|
282
|
+
# @attr [Time] recorded_at when this HTTP interaction was recorded
|
|
137
283
|
class HTTPInteraction < Struct.new(:request, :response, :recorded_at)
|
|
138
|
-
extend ::Forwardable
|
|
139
|
-
def_delegators :request, :uri, :method
|
|
140
|
-
|
|
141
284
|
def initialize(*args)
|
|
142
|
-
@ignored = false
|
|
143
285
|
super
|
|
144
286
|
self.recorded_at ||= Time.now
|
|
145
287
|
end
|
|
146
288
|
|
|
289
|
+
# Builds a serializable hash from the HTTP interaction data.
|
|
290
|
+
#
|
|
291
|
+
# @return [Hash] hash that represents this HTTP interaction
|
|
292
|
+
# and can be easily serialized.
|
|
293
|
+
# @see HTTPInteraction.from_hash
|
|
147
294
|
def to_hash
|
|
148
295
|
{
|
|
149
296
|
'request' => request.to_hash,
|
|
@@ -154,70 +301,117 @@ module VCR
|
|
|
154
301
|
end
|
|
155
302
|
end
|
|
156
303
|
|
|
304
|
+
# Constructs a new instance from a hash.
|
|
305
|
+
#
|
|
306
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
307
|
+
# @return [HTTPInteraction] the HTTP interaction
|
|
157
308
|
def self.from_hash(hash)
|
|
158
309
|
new Request.from_hash(hash.fetch('request', {})),
|
|
159
310
|
Response.from_hash(hash.fetch('response', {})),
|
|
160
311
|
Time.httpdate(hash.fetch('recorded_at'))
|
|
161
312
|
end
|
|
162
313
|
|
|
163
|
-
|
|
164
|
-
|
|
314
|
+
# @return [HookAware] an instance with additional capabilities
|
|
315
|
+
# suitable for use in `before_record` and `before_playback` hooks.
|
|
316
|
+
def hook_aware
|
|
317
|
+
HookAware.new(self)
|
|
165
318
|
end
|
|
166
319
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
320
|
+
# Decorates an {HTTPInteraction} with additional methods useful
|
|
321
|
+
# for a `before_record` or `before_playback` hook.
|
|
322
|
+
class HookAware < DelegateClass(HTTPInteraction)
|
|
323
|
+
def initialize(http_interaction)
|
|
324
|
+
@ignored = false
|
|
325
|
+
super
|
|
326
|
+
end
|
|
170
327
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
328
|
+
# Flags the HTTP interaction so that VCR ignores it. This is useful in
|
|
329
|
+
# a {VCR::Configuration#before_record} or {VCR::Configuration#before_playback}
|
|
330
|
+
# hook so that VCR does not record or play it back.
|
|
331
|
+
# @see #ignored?
|
|
332
|
+
def ignore!
|
|
333
|
+
@ignored = true
|
|
334
|
+
end
|
|
175
335
|
|
|
176
|
-
|
|
336
|
+
# @return [Boolean] whether or not this HTTP interaction should be ignored.
|
|
337
|
+
# @see #ignore!
|
|
338
|
+
def ignored?
|
|
339
|
+
!!@ignored
|
|
340
|
+
end
|
|
177
341
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
342
|
+
# Replaces a string in any part of the HTTP interaction (headers, request body,
|
|
343
|
+
# response body, etc) with the given replacement text.
|
|
344
|
+
#
|
|
345
|
+
# @param [String] text the text to replace
|
|
346
|
+
# @param [String] replacement_text the text to put in its place
|
|
347
|
+
def filter!(text, replacement_text)
|
|
348
|
+
return self if [text, replacement_text].any? { |t| t.to_s.empty? }
|
|
349
|
+
filter_object!(self, text, replacement_text)
|
|
186
350
|
end
|
|
187
351
|
|
|
188
|
-
|
|
189
|
-
end
|
|
352
|
+
private
|
|
190
353
|
|
|
191
|
-
|
|
192
|
-
|
|
354
|
+
def filter_object!(object, text, replacement_text)
|
|
355
|
+
if object.respond_to?(:gsub)
|
|
356
|
+
object.gsub!(text, replacement_text) if object.include?(text)
|
|
357
|
+
elsif Hash === object
|
|
358
|
+
filter_hash!(object, text, replacement_text)
|
|
359
|
+
elsif object.respond_to?(:each)
|
|
360
|
+
# This handles nested arrays and structs
|
|
361
|
+
object.each { |o| filter_object!(o, text, replacement_text) }
|
|
362
|
+
end
|
|
193
363
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
364
|
+
object
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def filter_hash!(hash, text, replacement_text)
|
|
368
|
+
filter_object!(hash.values, text, replacement_text)
|
|
369
|
+
|
|
370
|
+
hash.keys.each do |k|
|
|
371
|
+
new_key = filter_object!(k.dup, text, replacement_text)
|
|
372
|
+
hash[new_key] = hash.delete(k) unless k == new_key
|
|
373
|
+
end
|
|
197
374
|
end
|
|
198
375
|
end
|
|
199
376
|
end
|
|
200
377
|
|
|
378
|
+
# The response of an {HTTPInteraction}.
|
|
379
|
+
#
|
|
380
|
+
# @attr [ResponseStatus] status the status of the response
|
|
381
|
+
# @attr [Hash{String => Array<String>}] headers the response headers
|
|
382
|
+
# @attr [String] body the response body
|
|
383
|
+
# @attr [nil, String] http_version the HTTP version
|
|
201
384
|
class Response < Struct.new(:status, :headers, :body, :http_version)
|
|
202
385
|
include Normalizers::Header
|
|
203
386
|
include Normalizers::Body
|
|
204
387
|
|
|
388
|
+
# Builds a serializable hash from the response data.
|
|
389
|
+
#
|
|
390
|
+
# @return [Hash] hash that represents this response
|
|
391
|
+
# and can be easily serialized.
|
|
392
|
+
# @see Response.from_hash
|
|
205
393
|
def to_hash
|
|
206
394
|
{
|
|
207
395
|
'status' => status.to_hash,
|
|
208
396
|
'headers' => headers,
|
|
209
|
-
'body' =>
|
|
397
|
+
'body' => serializable_body,
|
|
210
398
|
'http_version' => http_version
|
|
211
399
|
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
|
|
212
400
|
end
|
|
213
401
|
|
|
402
|
+
# Constructs a new instance from a hash.
|
|
403
|
+
#
|
|
404
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
405
|
+
# @return [Response] the response
|
|
214
406
|
def self.from_hash(hash)
|
|
215
407
|
new ResponseStatus.from_hash(hash.fetch('status', {})),
|
|
216
408
|
hash['headers'],
|
|
217
|
-
hash['body'],
|
|
409
|
+
body_from(hash['body']),
|
|
218
410
|
hash['http_version']
|
|
219
411
|
end
|
|
220
412
|
|
|
413
|
+
# Updates the Content-Length response header so that it is
|
|
414
|
+
# accurate for the response body.
|
|
221
415
|
def update_content_length_header
|
|
222
416
|
value = body ? body.bytesize.to_s : '0'
|
|
223
417
|
key = %w[ Content-Length content-length ].find { |k| headers.has_key?(k) }
|
|
@@ -225,13 +419,26 @@ module VCR
|
|
|
225
419
|
end
|
|
226
420
|
end
|
|
227
421
|
|
|
422
|
+
# The response status of an {HTTPInteraction}.
|
|
423
|
+
#
|
|
424
|
+
# @attr [Integer] code the HTTP status code
|
|
425
|
+
# @attr [String] message the HTTP status message (e.g. "OK" for a status of 200)
|
|
228
426
|
class ResponseStatus < Struct.new(:code, :message)
|
|
427
|
+
# Builds a serializable hash from the response status data.
|
|
428
|
+
#
|
|
429
|
+
# @return [Hash] hash that represents this response status
|
|
430
|
+
# and can be easily serialized.
|
|
431
|
+
# @see ResponseStatus.from_hash
|
|
229
432
|
def to_hash
|
|
230
433
|
{
|
|
231
434
|
'code' => code, 'message' => message
|
|
232
435
|
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
|
|
233
436
|
end
|
|
234
437
|
|
|
438
|
+
# Constructs a new instance from a hash.
|
|
439
|
+
#
|
|
440
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
441
|
+
# @return [ResponseStatus] the response status
|
|
235
442
|
def self.from_hash(hash)
|
|
236
443
|
new hash['code'], hash['message']
|
|
237
444
|
end
|