vcr 2.0.0.rc1 → 2.0.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|