mustwin-vcr 2.9.3
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.
- checksums.yaml +7 -0
- data/features/about_these_examples.md +18 -0
- data/features/cassettes/allow_unused_http_interactions.feature +100 -0
- data/features/cassettes/automatic_re_recording.feature +72 -0
- data/features/cassettes/decompress.feature +74 -0
- data/features/cassettes/dynamic_erb.feature +100 -0
- data/features/cassettes/exclusive.feature +126 -0
- data/features/cassettes/format.feature +323 -0
- data/features/cassettes/freezing_time.feature +68 -0
- data/features/cassettes/naming.feature +28 -0
- data/features/cassettes/no_cassette.feature +152 -0
- data/features/cassettes/update_content_length_header.feature +112 -0
- data/features/configuration/allow_http_connections_when_no_cassette.feature +55 -0
- data/features/configuration/cassette_library_dir.feature +31 -0
- data/features/configuration/debug_logging.feature +59 -0
- data/features/configuration/default_cassette_options.feature +100 -0
- data/features/configuration/filter_sensitive_data.feature +153 -0
- data/features/configuration/hook_into.feature +172 -0
- data/features/configuration/ignore_request.feature +192 -0
- data/features/configuration/preserve_exact_body_bytes.feature +108 -0
- data/features/configuration/query_parser.feature +84 -0
- data/features/configuration/uri_parser.feature +89 -0
- data/features/getting_started.md +82 -0
- data/features/hooks/after_http_request.feature +58 -0
- data/features/hooks/around_http_request.feature +57 -0
- data/features/hooks/before_http_request.feature +63 -0
- data/features/hooks/before_playback.feature +184 -0
- data/features/hooks/before_record.feature +172 -0
- data/features/http_libraries/em_http_request.feature +250 -0
- data/features/http_libraries/net_http.feature +179 -0
- data/features/middleware/faraday.feature +56 -0
- data/features/middleware/rack.feature +92 -0
- data/features/record_modes/all.feature +82 -0
- data/features/record_modes/new_episodes.feature +79 -0
- data/features/record_modes/none.feature +72 -0
- data/features/record_modes/once.feature +95 -0
- data/features/request_matching/README.md +30 -0
- data/features/request_matching/body.feature +91 -0
- data/features/request_matching/body_as_json.feature +90 -0
- data/features/request_matching/custom_matcher.feature +135 -0
- data/features/request_matching/headers.feature +85 -0
- data/features/request_matching/host.feature +95 -0
- data/features/request_matching/identical_request_sequence.feature +89 -0
- data/features/request_matching/method.feature +96 -0
- data/features/request_matching/path.feature +96 -0
- data/features/request_matching/playback_repeats.feature +98 -0
- data/features/request_matching/query.feature +97 -0
- data/features/request_matching/uri.feature +94 -0
- data/features/request_matching/uri_without_param.feature +101 -0
- data/features/step_definitions/cli_steps.rb +193 -0
- data/features/support/env.rb +44 -0
- data/features/support/http_lib_filters.rb +53 -0
- data/features/test_frameworks/cucumber.feature +211 -0
- data/features/test_frameworks/rspec_macro.feature +81 -0
- data/features/test_frameworks/rspec_metadata.feature +150 -0
- data/features/test_frameworks/test_unit.feature +49 -0
- data/lib/vcr.rb +347 -0
- data/lib/vcr/cassette.rb +291 -0
- data/lib/vcr/cassette/erb_renderer.rb +55 -0
- data/lib/vcr/cassette/http_interaction_list.rb +108 -0
- data/lib/vcr/cassette/migrator.rb +118 -0
- data/lib/vcr/cassette/persisters.rb +42 -0
- data/lib/vcr/cassette/persisters/file_system.rb +64 -0
- data/lib/vcr/cassette/serializers.rb +57 -0
- data/lib/vcr/cassette/serializers/json.rb +48 -0
- data/lib/vcr/cassette/serializers/psych.rb +48 -0
- data/lib/vcr/cassette/serializers/syck.rb +61 -0
- data/lib/vcr/cassette/serializers/yaml.rb +50 -0
- data/lib/vcr/configuration.rb +555 -0
- data/lib/vcr/deprecations.rb +109 -0
- data/lib/vcr/errors.rb +266 -0
- data/lib/vcr/extensions/net_http_response.rb +36 -0
- data/lib/vcr/library_hooks.rb +18 -0
- data/lib/vcr/library_hooks/excon.rb +27 -0
- data/lib/vcr/library_hooks/fakeweb.rb +196 -0
- data/lib/vcr/library_hooks/faraday.rb +51 -0
- data/lib/vcr/library_hooks/typhoeus.rb +120 -0
- data/lib/vcr/library_hooks/typhoeus_0.4.rb +103 -0
- data/lib/vcr/library_hooks/webmock.rb +164 -0
- data/lib/vcr/middleware/excon.rb +221 -0
- data/lib/vcr/middleware/excon/legacy_methods.rb +33 -0
- data/lib/vcr/middleware/faraday.rb +118 -0
- data/lib/vcr/middleware/rack.rb +79 -0
- data/lib/vcr/request_handler.rb +114 -0
- data/lib/vcr/request_ignorer.rb +43 -0
- data/lib/vcr/request_matcher_registry.rb +149 -0
- data/lib/vcr/structs.rb +578 -0
- data/lib/vcr/tasks/vcr.rake +9 -0
- data/lib/vcr/test_frameworks/cucumber.rb +64 -0
- data/lib/vcr/test_frameworks/rspec.rb +47 -0
- data/lib/vcr/util/hooks.rb +61 -0
- data/lib/vcr/util/internet_connection.rb +43 -0
- data/lib/vcr/util/logger.rb +59 -0
- data/lib/vcr/util/variable_args_block_caller.rb +13 -0
- data/lib/vcr/util/version_checker.rb +48 -0
- data/lib/vcr/version.rb +34 -0
- data/spec/acceptance/threading_spec.rb +34 -0
- data/spec/fixtures/cassette_spec/1_x_cassette.yml +110 -0
- data/spec/fixtures/cassette_spec/empty.yml +0 -0
- data/spec/fixtures/cassette_spec/example.yml +111 -0
- data/spec/fixtures/cassette_spec/with_localhost_requests.yml +111 -0
- data/spec/fixtures/fake_example_responses.yml +110 -0
- data/spec/fixtures/match_requests_on.yml +187 -0
- data/spec/lib/vcr/cassette/erb_renderer_spec.rb +53 -0
- data/spec/lib/vcr/cassette/http_interaction_list_spec.rb +295 -0
- data/spec/lib/vcr/cassette/migrator_spec.rb +195 -0
- data/spec/lib/vcr/cassette/persisters/file_system_spec.rb +69 -0
- data/spec/lib/vcr/cassette/persisters_spec.rb +39 -0
- data/spec/lib/vcr/cassette/serializers_spec.rb +176 -0
- data/spec/lib/vcr/cassette_spec.rb +618 -0
- data/spec/lib/vcr/configuration_spec.rb +326 -0
- data/spec/lib/vcr/deprecations_spec.rb +85 -0
- data/spec/lib/vcr/errors_spec.rb +162 -0
- data/spec/lib/vcr/extensions/net_http_response_spec.rb +86 -0
- data/spec/lib/vcr/library_hooks/excon_spec.rb +104 -0
- data/spec/lib/vcr/library_hooks/fakeweb_spec.rb +169 -0
- data/spec/lib/vcr/library_hooks/faraday_spec.rb +68 -0
- data/spec/lib/vcr/library_hooks/typhoeus_0.4_spec.rb +36 -0
- data/spec/lib/vcr/library_hooks/typhoeus_spec.rb +162 -0
- data/spec/lib/vcr/library_hooks/webmock_spec.rb +118 -0
- data/spec/lib/vcr/library_hooks_spec.rb +51 -0
- data/spec/lib/vcr/middleware/faraday_spec.rb +182 -0
- data/spec/lib/vcr/middleware/rack_spec.rb +115 -0
- data/spec/lib/vcr/request_ignorer_spec.rb +70 -0
- data/spec/lib/vcr/request_matcher_registry_spec.rb +345 -0
- data/spec/lib/vcr/structs_spec.rb +732 -0
- data/spec/lib/vcr/test_frameworks/cucumber_spec.rb +107 -0
- data/spec/lib/vcr/test_frameworks/rspec_spec.rb +83 -0
- data/spec/lib/vcr/util/hooks_spec.rb +158 -0
- data/spec/lib/vcr/util/internet_connection_spec.rb +37 -0
- data/spec/lib/vcr/util/version_checker_spec.rb +31 -0
- data/spec/lib/vcr/version_spec.rb +27 -0
- data/spec/lib/vcr_spec.rb +349 -0
- data/spec/monkey_patches.rb +182 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/support/configuration_stubbing.rb +8 -0
- data/spec/support/cucumber_helpers.rb +35 -0
- data/spec/support/fixnum_extension.rb +10 -0
- data/spec/support/http_library_adapters.rb +289 -0
- data/spec/support/limited_uri.rb +21 -0
- data/spec/support/ruby_interpreter.rb +7 -0
- data/spec/support/shared_example_groups/excon.rb +63 -0
- data/spec/support/shared_example_groups/hook_into_http_library.rb +594 -0
- data/spec/support/shared_example_groups/request_hooks.rb +59 -0
- data/spec/support/sinatra_app.rb +86 -0
- data/spec/support/vcr_localhost_server.rb +76 -0
- data/spec/support/vcr_stub_helpers.rb +17 -0
- metadata +677 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module VCR
|
|
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.
|
|
5
|
+
class CassetteArguments
|
|
6
|
+
# @private
|
|
7
|
+
def initialize
|
|
8
|
+
@name = nil
|
|
9
|
+
@options = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Sets (and gets) the cassette name.
|
|
13
|
+
#
|
|
14
|
+
# @param [#to_s] name the cassette name
|
|
15
|
+
# @return [#to_s] the cassette name
|
|
16
|
+
def name(name = nil)
|
|
17
|
+
@name = name if name
|
|
18
|
+
@name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Sets (and gets) the cassette options.
|
|
22
|
+
#
|
|
23
|
+
# @param [Hash] options the cassette options
|
|
24
|
+
# @return [Hash] the cassette options
|
|
25
|
+
def options(options = {})
|
|
26
|
+
@options.merge!(options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
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.
|
|
43
|
+
class Rack
|
|
44
|
+
include VCR::VariableArgsBlockCaller
|
|
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
|
|
53
|
+
def initialize(app, &block)
|
|
54
|
+
raise ArgumentError.new("You must provide a block to set the cassette options") unless block
|
|
55
|
+
@app, @cassette_arguments_block, @mutex = app, block, Mutex.new
|
|
56
|
+
end
|
|
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
|
|
62
|
+
def call(env)
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
VCR.use_cassette(*cassette_arguments(env)) do
|
|
65
|
+
@app.call(env)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def cassette_arguments(env)
|
|
73
|
+
arguments = CassetteArguments.new
|
|
74
|
+
call_block(@cassette_arguments_block, arguments, env)
|
|
75
|
+
[arguments.name, arguments.options]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module VCR
|
|
2
|
+
# @private
|
|
3
|
+
class RequestHandler
|
|
4
|
+
include Logger::Mixin
|
|
5
|
+
|
|
6
|
+
def handle
|
|
7
|
+
log "Handling request: #{request_summary} (disabled: #{disabled?})"
|
|
8
|
+
invoke_before_request_hook
|
|
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 can 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
|
+
# after_request hook.
|
|
22
|
+
set_typed_request_for_after_hook(req_type)
|
|
23
|
+
|
|
24
|
+
send "on_#{req_type}_request"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
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 externally_stubbed? then :externally_stubbed
|
|
36
|
+
when should_ignore? then :ignored
|
|
37
|
+
when has_response_stub?(consume_stub) then :stubbed_by_vcr
|
|
38
|
+
when VCR.real_http_connections_allowed? then :recordable
|
|
39
|
+
else :unhandled
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def invoke_before_request_hook
|
|
44
|
+
return if disabled? || !VCR.configuration.has_hooks_for?(:before_http_request)
|
|
45
|
+
typed_request = Request::Typed.new(vcr_request, request_type)
|
|
46
|
+
VCR.configuration.invoke_hook(:before_http_request, typed_request)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def invoke_after_request_hook(vcr_response)
|
|
50
|
+
return if disabled?
|
|
51
|
+
VCR.configuration.invoke_hook(:after_http_request, @after_hook_typed_request, vcr_response)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def externally_stubbed?
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def should_ignore?
|
|
59
|
+
disabled? || VCR.request_ignorer.ignore?(vcr_request)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def disabled?
|
|
63
|
+
VCR.library_hooks.disabled?(library_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def has_response_stub?(consume_stub)
|
|
67
|
+
if consume_stub
|
|
68
|
+
stubbed_response
|
|
69
|
+
else
|
|
70
|
+
VCR.http_interactions.has_interaction_matching?(vcr_request)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stubbed_response
|
|
75
|
+
@stubbed_response ||= VCR.http_interactions.response_for(vcr_request)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def library_name
|
|
79
|
+
# extracts `:typhoeus` from `VCR::LibraryHooks::Typhoeus::RequestHandler`
|
|
80
|
+
@library_name ||= self.class.name.split('::')[-2].downcase.to_sym
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Subclasses can implement these
|
|
84
|
+
def on_externally_stubbed_request
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def on_ignored_request
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def on_stubbed_by_vcr_request
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def on_recordable_request
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def on_unhandled_request
|
|
97
|
+
raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def request_summary
|
|
101
|
+
request_matchers = if cass = VCR.current_cassette
|
|
102
|
+
cass.match_requests_on
|
|
103
|
+
else
|
|
104
|
+
VCR.configuration.default_cassette_options[:match_requests_on]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
super(vcr_request, request_matchers)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def log_prefix
|
|
111
|
+
"[#{library_name}] "
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'vcr/util/hooks'
|
|
3
|
+
|
|
4
|
+
module VCR
|
|
5
|
+
# @private
|
|
6
|
+
class RequestIgnorer
|
|
7
|
+
include VCR::Hooks
|
|
8
|
+
|
|
9
|
+
define_hook :ignore_request
|
|
10
|
+
|
|
11
|
+
LOCALHOST_ALIASES = %w( localhost 127.0.0.1 0.0.0.0 )
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
ignore_request do |request|
|
|
15
|
+
host = request.parsed_uri.host
|
|
16
|
+
ignored_hosts.include?(host)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ignore_localhost=(value)
|
|
21
|
+
if value
|
|
22
|
+
ignore_hosts(*LOCALHOST_ALIASES)
|
|
23
|
+
else
|
|
24
|
+
ignored_hosts.reject! { |h| LOCALHOST_ALIASES.include?(h) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ignore_hosts(*hosts)
|
|
29
|
+
ignored_hosts.merge(hosts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ignore?(request)
|
|
33
|
+
invoke_hook(:ignore_request, request).any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def ignored_hosts
|
|
39
|
+
@ignored_hosts ||= Set.new
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require 'vcr/errors'
|
|
2
|
+
|
|
3
|
+
module VCR
|
|
4
|
+
# Keeps track of the different request matchers.
|
|
5
|
+
class RequestMatcherRegistry
|
|
6
|
+
|
|
7
|
+
# The default request matchers used for any cassette that does not
|
|
8
|
+
# specify request matchers.
|
|
9
|
+
DEFAULT_MATCHERS = [:method, :uri]
|
|
10
|
+
|
|
11
|
+
# @private
|
|
12
|
+
class Matcher < Struct.new(:callable)
|
|
13
|
+
def matches?(request_1, request_2)
|
|
14
|
+
callable.call(request_1, request_2)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @private
|
|
19
|
+
class URIWithoutParamsMatcher < Struct.new(:params_to_ignore)
|
|
20
|
+
def partial_uri_from(request)
|
|
21
|
+
request.parsed_uri.tap do |uri|
|
|
22
|
+
return uri unless uri.query # ignore uris without params, e.g. "http://example.com/"
|
|
23
|
+
|
|
24
|
+
uri.query = uri.query.split('&').tap { |params|
|
|
25
|
+
params.map! do |p|
|
|
26
|
+
key, value = p.split('=')
|
|
27
|
+
key.gsub!(/\[\]\z/, '') # handle params like tag[]=
|
|
28
|
+
[key, value]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
params.reject! { |p| params_to_ignore.include?(p.first) }
|
|
32
|
+
params.map! { |p| p.join('=') }
|
|
33
|
+
}.join('&')
|
|
34
|
+
|
|
35
|
+
uri.query = nil if uri.query.empty?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(request_1, request_2)
|
|
40
|
+
partial_uri_from(request_1) == partial_uri_from(request_2)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_proc
|
|
44
|
+
lambda { |r1, r2| call(r1, r2) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @private
|
|
49
|
+
def initialize
|
|
50
|
+
@registry = {}
|
|
51
|
+
register_built_ins
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @private
|
|
55
|
+
def register(name, &block)
|
|
56
|
+
if @registry.has_key?(name)
|
|
57
|
+
warn "WARNING: There is already a VCR request matcher registered for #{name.inspect}. Overriding it."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@registry[name] = Matcher.new(block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @private
|
|
64
|
+
def [](matcher)
|
|
65
|
+
@registry.fetch(matcher) do
|
|
66
|
+
matcher.respond_to?(:call) ?
|
|
67
|
+
Matcher.new(matcher) :
|
|
68
|
+
raise_unregistered_matcher_error(matcher)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Builds a dynamic request matcher that matches on a URI while ignoring the
|
|
73
|
+
# named query parameters. This is useful for dealing with non-deterministic
|
|
74
|
+
# URIs (i.e. that have a timestamp or request signature parameter).
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# without_timestamp = VCR.request_matchers.uri_without_param(:timestamp)
|
|
78
|
+
#
|
|
79
|
+
# # use it directly...
|
|
80
|
+
# VCR.use_cassette('example', :match_requests_on => [:method, without_timestamp]) { }
|
|
81
|
+
#
|
|
82
|
+
# # ...or register it as a named matcher
|
|
83
|
+
# VCR.configure do |c|
|
|
84
|
+
# c.register_request_matcher(:uri_without_timestamp, &without_timestamp)
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# VCR.use_cassette('example', :match_requests_on => [:method, :uri_without_timestamp]) { }
|
|
88
|
+
#
|
|
89
|
+
# @param ignores [Array<#to_s>] The names of the query parameters to ignore
|
|
90
|
+
# @return [#call] the request matcher
|
|
91
|
+
def uri_without_params(*ignores)
|
|
92
|
+
uri_without_param_matchers[ignores]
|
|
93
|
+
end
|
|
94
|
+
alias uri_without_param uri_without_params
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def uri_without_param_matchers
|
|
99
|
+
@uri_without_param_matchers ||= Hash.new do |hash, params|
|
|
100
|
+
params = params.map(&:to_s)
|
|
101
|
+
hash[params] = URIWithoutParamsMatcher.new(params)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def raise_unregistered_matcher_error(name)
|
|
106
|
+
raise Errors::UnregisteredMatcherError.new \
|
|
107
|
+
"There is no matcher registered for #{name.inspect}. " +
|
|
108
|
+
"Did you mean one of #{@registry.keys.map(&:inspect).join(', ')}?"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def register_built_ins
|
|
112
|
+
register(:method) { |r1, r2| r2.method.is_a?(Regexp) ? r1.method.match(r2.method) : r1.method == r2.method }
|
|
113
|
+
register(:uri) { |r1, r2| r2.uri.is_a?(Regexp) ? r1.uri.match(r2.uri) : r1.uri == r2.uri }
|
|
114
|
+
register(:body) { |r1, r2| r2.body.is_a?(Regexp) ? r1.body.match(r2.body) : r1.body == r2.body }
|
|
115
|
+
register(:headers) { |r1, r2| r2.headers.is_a?(Regexp) ? r1.headers.match(r2.headers) : r1.headers == r2.headers }
|
|
116
|
+
|
|
117
|
+
register(:host) do |r1, r2|
|
|
118
|
+
r1.parsed_uri.host == r2.parsed_uri.host
|
|
119
|
+
end
|
|
120
|
+
register(:path) do |r1, r2|
|
|
121
|
+
r1.parsed_uri.path == r2.parsed_uri.path
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
register(:query) do |r1, r2|
|
|
125
|
+
VCR.configuration.query_parser.call(r1.parsed_uri.query.to_s) ==
|
|
126
|
+
VCR.configuration.query_parser.call(r2.parsed_uri.query.to_s)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
try_to_register_body_as_json
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def try_to_register_body_as_json
|
|
133
|
+
begin
|
|
134
|
+
require 'json'
|
|
135
|
+
rescue LoadError
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
register(:body_as_json) do |r1, r2|
|
|
140
|
+
begin
|
|
141
|
+
JSON.parse(r1.body) == JSON.parse(r2.body)
|
|
142
|
+
rescue JSON::ParserError
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
data/lib/vcr/structs.rb
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'delegate'
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module VCR
|
|
6
|
+
# @private
|
|
7
|
+
module Normalizers
|
|
8
|
+
# @private
|
|
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 encoding.nil? || string.encoding.name == encoding
|
|
36
|
+
|
|
37
|
+
# ASCII-8BIT just means binary, so encoding to it is nonsensical
|
|
38
|
+
# and yet "\u00f6".encode("ASCII-8BIT") raises an error.
|
|
39
|
+
# Instead, we'll force encode it (essentially just tagging it as binary)
|
|
40
|
+
return string.force_encoding(encoding) if encoding == "ASCII-8BIT"
|
|
41
|
+
|
|
42
|
+
string.encode(encoding)
|
|
43
|
+
rescue EncodingError => e
|
|
44
|
+
struct_type = name.split('::').last.downcase
|
|
45
|
+
warn "VCR: got `#{e.class.name}: #{e.message}` while trying to encode the #{string.encoding.name} " +
|
|
46
|
+
"#{struct_type} body to the original body encoding (#{encoding}). Consider using the " +
|
|
47
|
+
"`:preserve_exact_body_bytes` option to work around this."
|
|
48
|
+
return string
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
def force_encode_string(string, encoding)
|
|
52
|
+
string
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def try_encode_string(string, encoding)
|
|
56
|
+
string
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def initialize(*args)
|
|
62
|
+
super
|
|
63
|
+
|
|
64
|
+
if body && !body.is_a?(String)
|
|
65
|
+
raise ArgumentError, "#{self.class} initialized with an invalid body: #{body.inspect}."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Ensure that the body is a raw string, in case the string instance
|
|
69
|
+
# has been subclassed or extended with additional instance variables
|
|
70
|
+
# or attributes, so that it is serialized to YAML as a raw string.
|
|
71
|
+
# This is needed for rest-client. See this ticket for more info:
|
|
72
|
+
# http://github.com/myronmarston/vcr/issues/4
|
|
73
|
+
self.body = String.new(body.to_s)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def serializable_body
|
|
79
|
+
# Ensure it's just a string, and not a string with some
|
|
80
|
+
# extra state, as such strings serialize to YAML with
|
|
81
|
+
# all the extra state.
|
|
82
|
+
body = String.new(self.body.to_s)
|
|
83
|
+
|
|
84
|
+
if VCR.configuration.preserve_exact_body_bytes_for?(self)
|
|
85
|
+
base_body_hash(body).merge('base64_string' => Base64.encode64(body))
|
|
86
|
+
else
|
|
87
|
+
base_body_hash(body).merge('string' => body)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if ''.respond_to?(:encoding)
|
|
92
|
+
def base_body_hash(body)
|
|
93
|
+
{ 'encoding' => body.encoding.name }
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
def base_body_hash(body)
|
|
97
|
+
{ }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @private
|
|
103
|
+
module Header
|
|
104
|
+
def initialize(*args)
|
|
105
|
+
super
|
|
106
|
+
normalize_headers
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def normalize_headers
|
|
112
|
+
new_headers = {}
|
|
113
|
+
@normalized_header_keys = Hash.new {|h,k| k }
|
|
114
|
+
|
|
115
|
+
headers.each do |k, v|
|
|
116
|
+
val_array = case v
|
|
117
|
+
when Array then v
|
|
118
|
+
when nil then []
|
|
119
|
+
else [v]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
new_headers[String.new(k)] = convert_to_raw_strings(val_array)
|
|
123
|
+
@normalized_header_keys[k.downcase] = k
|
|
124
|
+
end if headers
|
|
125
|
+
|
|
126
|
+
self.headers = new_headers
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def header_key(key)
|
|
130
|
+
key = @normalized_header_keys[key.downcase]
|
|
131
|
+
key if headers.has_key? key
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def get_header(key)
|
|
135
|
+
key = header_key(key)
|
|
136
|
+
headers[key] if key
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def edit_header(key, value = nil)
|
|
140
|
+
if key = header_key(key)
|
|
141
|
+
value ||= yield headers[key]
|
|
142
|
+
headers[key] = Array(value)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def delete_header(key)
|
|
147
|
+
if key = header_key(key)
|
|
148
|
+
@normalized_header_keys.delete key.downcase
|
|
149
|
+
headers.delete key
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def convert_to_raw_strings(array)
|
|
154
|
+
# Ensure the values are raw strings.
|
|
155
|
+
# Apparently for Paperclip uploads to S3, headers
|
|
156
|
+
# get serialized with some extra stuff which leads
|
|
157
|
+
# to a seg fault. See this issue for more info:
|
|
158
|
+
# https://github.com/myronmarston/vcr/issues#issue/39
|
|
159
|
+
array.map do |v|
|
|
160
|
+
case v
|
|
161
|
+
when String; String.new(v)
|
|
162
|
+
when Array; convert_to_raw_strings(v)
|
|
163
|
+
else v
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @private
|
|
171
|
+
module OrderedHashSerializer
|
|
172
|
+
def each
|
|
173
|
+
@ordered_keys.each do |key|
|
|
174
|
+
yield key, self[key] if has_key?(key)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if RUBY_VERSION.to_f > 1.8
|
|
179
|
+
# 1.9+ hashes are already ordered.
|
|
180
|
+
def self.apply_to(*args); end
|
|
181
|
+
else
|
|
182
|
+
def self.apply_to(hash, keys)
|
|
183
|
+
hash.instance_variable_set(:@ordered_keys, keys)
|
|
184
|
+
hash.extend self
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The request of an {HTTPInteraction}.
|
|
190
|
+
#
|
|
191
|
+
# @attr [Symbol] method the HTTP method (i.e. :head, :options, :get, :post, :put, :patch or :delete)
|
|
192
|
+
# @attr [String] uri the request URI
|
|
193
|
+
# @attr [String, nil] body the request body
|
|
194
|
+
# @attr [Hash{String => Array<String>}] headers the request headers
|
|
195
|
+
class Request < Struct.new(:method, :uri, :body, :headers)
|
|
196
|
+
include Normalizers::Header
|
|
197
|
+
include Normalizers::Body
|
|
198
|
+
|
|
199
|
+
def initialize(*args)
|
|
200
|
+
skip_port_stripping = false
|
|
201
|
+
if args.last == :skip_port_stripping
|
|
202
|
+
skip_port_stripping = true
|
|
203
|
+
args.pop
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
super(*args)
|
|
207
|
+
self.method = self.method.to_s.downcase.to_sym if self.method
|
|
208
|
+
self.uri = without_standard_port(self.uri) unless skip_port_stripping
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Builds a serializable hash from the request data.
|
|
212
|
+
#
|
|
213
|
+
# @return [Hash] hash that represents this request and can be easily
|
|
214
|
+
# serialized.
|
|
215
|
+
# @see Request.from_hash
|
|
216
|
+
def to_hash
|
|
217
|
+
{
|
|
218
|
+
'method' => method.to_s,
|
|
219
|
+
'uri' => uri,
|
|
220
|
+
'body' => serializable_body,
|
|
221
|
+
'headers' => headers
|
|
222
|
+
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Constructs a new instance from a hash.
|
|
226
|
+
#
|
|
227
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
228
|
+
# @return [Request] the request
|
|
229
|
+
def self.from_hash(hash)
|
|
230
|
+
method = hash['method']
|
|
231
|
+
method &&= method.to_sym
|
|
232
|
+
new method,
|
|
233
|
+
hash['uri'],
|
|
234
|
+
body_from(hash['body']),
|
|
235
|
+
hash['headers'],
|
|
236
|
+
:skip_port_stripping
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Parses the URI using the configured `uri_parser`.
|
|
240
|
+
#
|
|
241
|
+
# @return [#schema, #host, #port, #path, #query] A parsed URI object.
|
|
242
|
+
def parsed_uri
|
|
243
|
+
VCR.configuration.uri_parser.parse(uri)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
@@object_method = Object.instance_method(:method)
|
|
247
|
+
def method(*args)
|
|
248
|
+
return super if args.empty?
|
|
249
|
+
@@object_method.bind(self).call(*args)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Decorates a {Request} with its current type.
|
|
253
|
+
class Typed < DelegateClass(self)
|
|
254
|
+
# @return [Symbol] One of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
|
|
255
|
+
attr_reader :type
|
|
256
|
+
|
|
257
|
+
# @param [Request] request the request
|
|
258
|
+
# @param [Symbol] type the type. Should be one of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
|
|
259
|
+
def initialize(request, type)
|
|
260
|
+
@type = type
|
|
261
|
+
super(request)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @return [Boolean] whether or not this request is being ignored
|
|
265
|
+
def ignored?
|
|
266
|
+
type == :ignored
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# @return [Boolean] whether or not this request is being stubbed by VCR
|
|
270
|
+
# @see #externally_stubbed?
|
|
271
|
+
# @see #stubbed?
|
|
272
|
+
def stubbed_by_vcr?
|
|
273
|
+
type == :stubbed_by_vcr
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @return [Boolean] whether or not this request is being stubbed by an
|
|
277
|
+
# external library (such as WebMock or FakeWeb).
|
|
278
|
+
# @see #stubbed_by_vcr?
|
|
279
|
+
# @see #stubbed?
|
|
280
|
+
def externally_stubbed?
|
|
281
|
+
type == :externally_stubbed
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# @return [Boolean] whether or not this request will be recorded.
|
|
285
|
+
def recordable?
|
|
286
|
+
type == :recordable
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# @return [Boolean] whether or not VCR knows how to handle this request.
|
|
290
|
+
def unhandled?
|
|
291
|
+
type == :unhandled
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @return [Boolean] whether or not this request will be made for real.
|
|
295
|
+
# @note VCR allows `:ignored` and `:recordable` requests to be made for real.
|
|
296
|
+
def real?
|
|
297
|
+
ignored? || recordable?
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @return [Boolean] whether or not this request will be stubbed.
|
|
301
|
+
# It may be stubbed by an external library or by VCR.
|
|
302
|
+
# @see #stubbed_by_vcr?
|
|
303
|
+
# @see #externally_stubbed?
|
|
304
|
+
def stubbed?
|
|
305
|
+
stubbed_by_vcr? || externally_stubbed?
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
undef method
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Provides fiber-awareness for the {VCR::Configuration#around_http_request} hook.
|
|
312
|
+
class FiberAware < DelegateClass(Typed)
|
|
313
|
+
# Yields the fiber so the request can proceed.
|
|
314
|
+
#
|
|
315
|
+
# @return [VCR::Response] the response from the request
|
|
316
|
+
def proceed
|
|
317
|
+
Fiber.yield
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Builds a proc that allows the request to proceed when called.
|
|
321
|
+
# This allows you to treat the request as a proc and pass it on
|
|
322
|
+
# to a method that yields (at which point the request will proceed).
|
|
323
|
+
#
|
|
324
|
+
# @return [Proc] the proc
|
|
325
|
+
def to_proc
|
|
326
|
+
lambda { proceed }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
undef method
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
private
|
|
333
|
+
|
|
334
|
+
def without_standard_port(uri)
|
|
335
|
+
return uri if uri.nil?
|
|
336
|
+
u = parsed_uri
|
|
337
|
+
return uri unless [['http', 80], ['https', 443]].include?([u.scheme, u.port])
|
|
338
|
+
u.port = nil
|
|
339
|
+
u.to_s
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# The response of an {HTTPInteraction}.
|
|
344
|
+
#
|
|
345
|
+
# @attr [ResponseStatus] status the status of the response
|
|
346
|
+
# @attr [Hash{String => Array<String>}] headers the response headers
|
|
347
|
+
# @attr [String] body the response body
|
|
348
|
+
# @attr [nil, String] http_version the HTTP version
|
|
349
|
+
# @attr [Hash] adapter_metadata Additional metadata used by a specific VCR adapter.
|
|
350
|
+
class Response < Struct.new(:status, :headers, :body, :http_version, :adapter_metadata)
|
|
351
|
+
include Normalizers::Header
|
|
352
|
+
include Normalizers::Body
|
|
353
|
+
|
|
354
|
+
def initialize(*args)
|
|
355
|
+
super(*args)
|
|
356
|
+
self.adapter_metadata ||= {}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Builds a serializable hash from the response data.
|
|
360
|
+
#
|
|
361
|
+
# @return [Hash] hash that represents this response
|
|
362
|
+
# and can be easily serialized.
|
|
363
|
+
# @see Response.from_hash
|
|
364
|
+
def to_hash
|
|
365
|
+
{
|
|
366
|
+
'status' => status.to_hash,
|
|
367
|
+
'headers' => headers,
|
|
368
|
+
'body' => serializable_body,
|
|
369
|
+
'http_version' => http_version
|
|
370
|
+
}.tap do |hash|
|
|
371
|
+
hash['adapter_metadata'] = adapter_metadata unless adapter_metadata.empty?
|
|
372
|
+
OrderedHashSerializer.apply_to(hash, members)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Constructs a new instance from a hash.
|
|
377
|
+
#
|
|
378
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
379
|
+
# @return [Response] the response
|
|
380
|
+
def self.from_hash(hash)
|
|
381
|
+
new ResponseStatus.from_hash(hash.fetch('status', {})),
|
|
382
|
+
hash['headers'],
|
|
383
|
+
body_from(hash['body']),
|
|
384
|
+
hash['http_version'],
|
|
385
|
+
hash['adapter_metadata']
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Updates the Content-Length response header so that it is
|
|
389
|
+
# accurate for the response body.
|
|
390
|
+
def update_content_length_header
|
|
391
|
+
edit_header('Content-Length') { body ? body.bytesize.to_s : '0' }
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# The type of encoding.
|
|
395
|
+
#
|
|
396
|
+
# @return [String] encoding type
|
|
397
|
+
def content_encoding
|
|
398
|
+
enc = get_header('Content-Encoding') and enc.first
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Checks if the type of encoding is one of "gzip" or "deflate".
|
|
402
|
+
def compressed?
|
|
403
|
+
%w[ gzip deflate ].include? content_encoding
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Decodes the compressed body and deletes evidence that it was ever compressed.
|
|
407
|
+
#
|
|
408
|
+
# @return self
|
|
409
|
+
# @raise [VCR::Errors::UnknownContentEncodingError] if the content encoding
|
|
410
|
+
# is not a known encoding.
|
|
411
|
+
def decompress
|
|
412
|
+
self.class.decompress(body, content_encoding) { |new_body|
|
|
413
|
+
self.body = new_body
|
|
414
|
+
update_content_length_header
|
|
415
|
+
delete_header('Content-Encoding')
|
|
416
|
+
}
|
|
417
|
+
return self
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
begin
|
|
421
|
+
require 'zlib'
|
|
422
|
+
require 'stringio'
|
|
423
|
+
HAVE_ZLIB = true
|
|
424
|
+
rescue LoadError
|
|
425
|
+
HAVE_ZLIB = false
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Decode string compressed with gzip or deflate
|
|
429
|
+
#
|
|
430
|
+
# @raise [VCR::Errors::UnknownContentEncodingError] if the content encoding
|
|
431
|
+
# is not a known encoding.
|
|
432
|
+
def self.decompress(body, type)
|
|
433
|
+
unless HAVE_ZLIB
|
|
434
|
+
warn "VCR: cannot decompress response; Zlib not available"
|
|
435
|
+
return
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
case type
|
|
439
|
+
when 'gzip'
|
|
440
|
+
args = [StringIO.new(body)]
|
|
441
|
+
args << { :encoding => 'ASCII-8BIT' } if ''.respond_to?(:encoding)
|
|
442
|
+
yield Zlib::GzipReader.new(*args).read
|
|
443
|
+
when 'deflate'
|
|
444
|
+
yield Zlib::Inflate.inflate(body)
|
|
445
|
+
when 'identity', NilClass
|
|
446
|
+
return
|
|
447
|
+
else
|
|
448
|
+
raise Errors::UnknownContentEncodingError, "unknown content encoding: #{type}"
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# The response status of an {HTTPInteraction}.
|
|
454
|
+
#
|
|
455
|
+
# @attr [Integer] code the HTTP status code
|
|
456
|
+
# @attr [String] message the HTTP status message (e.g. "OK" for a status of 200)
|
|
457
|
+
class ResponseStatus < Struct.new(:code, :message)
|
|
458
|
+
# Builds a serializable hash from the response status data.
|
|
459
|
+
#
|
|
460
|
+
# @return [Hash] hash that represents this response status
|
|
461
|
+
# and can be easily serialized.
|
|
462
|
+
# @see ResponseStatus.from_hash
|
|
463
|
+
def to_hash
|
|
464
|
+
{
|
|
465
|
+
'code' => code, 'message' => message
|
|
466
|
+
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Constructs a new instance from a hash.
|
|
470
|
+
#
|
|
471
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
472
|
+
# @return [ResponseStatus] the response status
|
|
473
|
+
def self.from_hash(hash)
|
|
474
|
+
new hash['code'], hash['message']
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Represents a single interaction over HTTP, containing a request and a response.
|
|
479
|
+
#
|
|
480
|
+
# @attr [Request] request the request
|
|
481
|
+
# @attr [Response] response the response
|
|
482
|
+
# @attr [Time] recorded_at when this HTTP interaction was recorded
|
|
483
|
+
class HTTPInteraction < Struct.new(:request, :response, :recorded_at)
|
|
484
|
+
def initialize(*args)
|
|
485
|
+
super
|
|
486
|
+
self.recorded_at ||= Time.now
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Builds a serializable hash from the HTTP interaction data.
|
|
490
|
+
#
|
|
491
|
+
# @return [Hash] hash that represents this HTTP interaction
|
|
492
|
+
# and can be easily serialized.
|
|
493
|
+
# @see HTTPInteraction.from_hash
|
|
494
|
+
def to_hash
|
|
495
|
+
{
|
|
496
|
+
'request' => request.to_hash,
|
|
497
|
+
'response' => response.to_hash,
|
|
498
|
+
'recorded_at' => recorded_at.httpdate
|
|
499
|
+
}.tap do |hash|
|
|
500
|
+
OrderedHashSerializer.apply_to(hash, members)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Constructs a new instance from a hash.
|
|
505
|
+
#
|
|
506
|
+
# @param [Hash] hash the hash to use to construct the instance.
|
|
507
|
+
# @return [HTTPInteraction] the HTTP interaction
|
|
508
|
+
def self.from_hash(hash)
|
|
509
|
+
new Request.from_hash(hash.fetch('request', {})),
|
|
510
|
+
Response.from_hash(hash.fetch('response', {})),
|
|
511
|
+
Time.httpdate(hash.fetch('recorded_at'))
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# @return [HookAware] an instance with additional capabilities
|
|
515
|
+
# suitable for use in `before_record` and `before_playback` hooks.
|
|
516
|
+
def hook_aware
|
|
517
|
+
HookAware.new(self)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Decorates an {HTTPInteraction} with additional methods useful
|
|
521
|
+
# for a `before_record` or `before_playback` hook.
|
|
522
|
+
class HookAware < DelegateClass(HTTPInteraction)
|
|
523
|
+
def initialize(http_interaction)
|
|
524
|
+
@ignored = false
|
|
525
|
+
super
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Flags the HTTP interaction so that VCR ignores it. This is useful in
|
|
529
|
+
# a {VCR::Configuration#before_record} or {VCR::Configuration#before_playback}
|
|
530
|
+
# hook so that VCR does not record or play it back.
|
|
531
|
+
# @see #ignored?
|
|
532
|
+
def ignore!
|
|
533
|
+
@ignored = true
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# @return [Boolean] whether or not this HTTP interaction should be ignored.
|
|
537
|
+
# @see #ignore!
|
|
538
|
+
def ignored?
|
|
539
|
+
!!@ignored
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Replaces a string in any part of the HTTP interaction (headers, request body,
|
|
543
|
+
# response body, etc) with the given replacement text.
|
|
544
|
+
#
|
|
545
|
+
# @param [#to_s] text the text to replace
|
|
546
|
+
# @param [#to_s] replacement_text the text to put in its place
|
|
547
|
+
def filter!(text, replacement_text)
|
|
548
|
+
text, replacement_text = text.to_s, replacement_text.to_s
|
|
549
|
+
return self if [text, replacement_text].any? { |t| t.empty? }
|
|
550
|
+
filter_object!(self, text, replacement_text)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
private
|
|
554
|
+
|
|
555
|
+
def filter_object!(object, text, replacement_text)
|
|
556
|
+
if object.respond_to?(:gsub)
|
|
557
|
+
object.gsub!(text, replacement_text) if object.include?(text)
|
|
558
|
+
elsif Hash === object
|
|
559
|
+
filter_hash!(object, text, replacement_text)
|
|
560
|
+
elsif object.respond_to?(:each)
|
|
561
|
+
# This handles nested arrays and structs
|
|
562
|
+
object.each { |o| filter_object!(o, text, replacement_text) }
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
object
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def filter_hash!(hash, text, replacement_text)
|
|
569
|
+
filter_object!(hash.values, text, replacement_text)
|
|
570
|
+
|
|
571
|
+
hash.keys.each do |k|
|
|
572
|
+
new_key = filter_object!(k.dup, text, replacement_text)
|
|
573
|
+
hash[new_key] = hash.delete(k) unless k == new_key
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|