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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/features/about_these_examples.md +18 -0
  3. data/features/cassettes/allow_unused_http_interactions.feature +100 -0
  4. data/features/cassettes/automatic_re_recording.feature +72 -0
  5. data/features/cassettes/decompress.feature +74 -0
  6. data/features/cassettes/dynamic_erb.feature +100 -0
  7. data/features/cassettes/exclusive.feature +126 -0
  8. data/features/cassettes/format.feature +323 -0
  9. data/features/cassettes/freezing_time.feature +68 -0
  10. data/features/cassettes/naming.feature +28 -0
  11. data/features/cassettes/no_cassette.feature +152 -0
  12. data/features/cassettes/update_content_length_header.feature +112 -0
  13. data/features/configuration/allow_http_connections_when_no_cassette.feature +55 -0
  14. data/features/configuration/cassette_library_dir.feature +31 -0
  15. data/features/configuration/debug_logging.feature +59 -0
  16. data/features/configuration/default_cassette_options.feature +100 -0
  17. data/features/configuration/filter_sensitive_data.feature +153 -0
  18. data/features/configuration/hook_into.feature +172 -0
  19. data/features/configuration/ignore_request.feature +192 -0
  20. data/features/configuration/preserve_exact_body_bytes.feature +108 -0
  21. data/features/configuration/query_parser.feature +84 -0
  22. data/features/configuration/uri_parser.feature +89 -0
  23. data/features/getting_started.md +82 -0
  24. data/features/hooks/after_http_request.feature +58 -0
  25. data/features/hooks/around_http_request.feature +57 -0
  26. data/features/hooks/before_http_request.feature +63 -0
  27. data/features/hooks/before_playback.feature +184 -0
  28. data/features/hooks/before_record.feature +172 -0
  29. data/features/http_libraries/em_http_request.feature +250 -0
  30. data/features/http_libraries/net_http.feature +179 -0
  31. data/features/middleware/faraday.feature +56 -0
  32. data/features/middleware/rack.feature +92 -0
  33. data/features/record_modes/all.feature +82 -0
  34. data/features/record_modes/new_episodes.feature +79 -0
  35. data/features/record_modes/none.feature +72 -0
  36. data/features/record_modes/once.feature +95 -0
  37. data/features/request_matching/README.md +30 -0
  38. data/features/request_matching/body.feature +91 -0
  39. data/features/request_matching/body_as_json.feature +90 -0
  40. data/features/request_matching/custom_matcher.feature +135 -0
  41. data/features/request_matching/headers.feature +85 -0
  42. data/features/request_matching/host.feature +95 -0
  43. data/features/request_matching/identical_request_sequence.feature +89 -0
  44. data/features/request_matching/method.feature +96 -0
  45. data/features/request_matching/path.feature +96 -0
  46. data/features/request_matching/playback_repeats.feature +98 -0
  47. data/features/request_matching/query.feature +97 -0
  48. data/features/request_matching/uri.feature +94 -0
  49. data/features/request_matching/uri_without_param.feature +101 -0
  50. data/features/step_definitions/cli_steps.rb +193 -0
  51. data/features/support/env.rb +44 -0
  52. data/features/support/http_lib_filters.rb +53 -0
  53. data/features/test_frameworks/cucumber.feature +211 -0
  54. data/features/test_frameworks/rspec_macro.feature +81 -0
  55. data/features/test_frameworks/rspec_metadata.feature +150 -0
  56. data/features/test_frameworks/test_unit.feature +49 -0
  57. data/lib/vcr.rb +347 -0
  58. data/lib/vcr/cassette.rb +291 -0
  59. data/lib/vcr/cassette/erb_renderer.rb +55 -0
  60. data/lib/vcr/cassette/http_interaction_list.rb +108 -0
  61. data/lib/vcr/cassette/migrator.rb +118 -0
  62. data/lib/vcr/cassette/persisters.rb +42 -0
  63. data/lib/vcr/cassette/persisters/file_system.rb +64 -0
  64. data/lib/vcr/cassette/serializers.rb +57 -0
  65. data/lib/vcr/cassette/serializers/json.rb +48 -0
  66. data/lib/vcr/cassette/serializers/psych.rb +48 -0
  67. data/lib/vcr/cassette/serializers/syck.rb +61 -0
  68. data/lib/vcr/cassette/serializers/yaml.rb +50 -0
  69. data/lib/vcr/configuration.rb +555 -0
  70. data/lib/vcr/deprecations.rb +109 -0
  71. data/lib/vcr/errors.rb +266 -0
  72. data/lib/vcr/extensions/net_http_response.rb +36 -0
  73. data/lib/vcr/library_hooks.rb +18 -0
  74. data/lib/vcr/library_hooks/excon.rb +27 -0
  75. data/lib/vcr/library_hooks/fakeweb.rb +196 -0
  76. data/lib/vcr/library_hooks/faraday.rb +51 -0
  77. data/lib/vcr/library_hooks/typhoeus.rb +120 -0
  78. data/lib/vcr/library_hooks/typhoeus_0.4.rb +103 -0
  79. data/lib/vcr/library_hooks/webmock.rb +164 -0
  80. data/lib/vcr/middleware/excon.rb +221 -0
  81. data/lib/vcr/middleware/excon/legacy_methods.rb +33 -0
  82. data/lib/vcr/middleware/faraday.rb +118 -0
  83. data/lib/vcr/middleware/rack.rb +79 -0
  84. data/lib/vcr/request_handler.rb +114 -0
  85. data/lib/vcr/request_ignorer.rb +43 -0
  86. data/lib/vcr/request_matcher_registry.rb +149 -0
  87. data/lib/vcr/structs.rb +578 -0
  88. data/lib/vcr/tasks/vcr.rake +9 -0
  89. data/lib/vcr/test_frameworks/cucumber.rb +64 -0
  90. data/lib/vcr/test_frameworks/rspec.rb +47 -0
  91. data/lib/vcr/util/hooks.rb +61 -0
  92. data/lib/vcr/util/internet_connection.rb +43 -0
  93. data/lib/vcr/util/logger.rb +59 -0
  94. data/lib/vcr/util/variable_args_block_caller.rb +13 -0
  95. data/lib/vcr/util/version_checker.rb +48 -0
  96. data/lib/vcr/version.rb +34 -0
  97. data/spec/acceptance/threading_spec.rb +34 -0
  98. data/spec/fixtures/cassette_spec/1_x_cassette.yml +110 -0
  99. data/spec/fixtures/cassette_spec/empty.yml +0 -0
  100. data/spec/fixtures/cassette_spec/example.yml +111 -0
  101. data/spec/fixtures/cassette_spec/with_localhost_requests.yml +111 -0
  102. data/spec/fixtures/fake_example_responses.yml +110 -0
  103. data/spec/fixtures/match_requests_on.yml +187 -0
  104. data/spec/lib/vcr/cassette/erb_renderer_spec.rb +53 -0
  105. data/spec/lib/vcr/cassette/http_interaction_list_spec.rb +295 -0
  106. data/spec/lib/vcr/cassette/migrator_spec.rb +195 -0
  107. data/spec/lib/vcr/cassette/persisters/file_system_spec.rb +69 -0
  108. data/spec/lib/vcr/cassette/persisters_spec.rb +39 -0
  109. data/spec/lib/vcr/cassette/serializers_spec.rb +176 -0
  110. data/spec/lib/vcr/cassette_spec.rb +618 -0
  111. data/spec/lib/vcr/configuration_spec.rb +326 -0
  112. data/spec/lib/vcr/deprecations_spec.rb +85 -0
  113. data/spec/lib/vcr/errors_spec.rb +162 -0
  114. data/spec/lib/vcr/extensions/net_http_response_spec.rb +86 -0
  115. data/spec/lib/vcr/library_hooks/excon_spec.rb +104 -0
  116. data/spec/lib/vcr/library_hooks/fakeweb_spec.rb +169 -0
  117. data/spec/lib/vcr/library_hooks/faraday_spec.rb +68 -0
  118. data/spec/lib/vcr/library_hooks/typhoeus_0.4_spec.rb +36 -0
  119. data/spec/lib/vcr/library_hooks/typhoeus_spec.rb +162 -0
  120. data/spec/lib/vcr/library_hooks/webmock_spec.rb +118 -0
  121. data/spec/lib/vcr/library_hooks_spec.rb +51 -0
  122. data/spec/lib/vcr/middleware/faraday_spec.rb +182 -0
  123. data/spec/lib/vcr/middleware/rack_spec.rb +115 -0
  124. data/spec/lib/vcr/request_ignorer_spec.rb +70 -0
  125. data/spec/lib/vcr/request_matcher_registry_spec.rb +345 -0
  126. data/spec/lib/vcr/structs_spec.rb +732 -0
  127. data/spec/lib/vcr/test_frameworks/cucumber_spec.rb +107 -0
  128. data/spec/lib/vcr/test_frameworks/rspec_spec.rb +83 -0
  129. data/spec/lib/vcr/util/hooks_spec.rb +158 -0
  130. data/spec/lib/vcr/util/internet_connection_spec.rb +37 -0
  131. data/spec/lib/vcr/util/version_checker_spec.rb +31 -0
  132. data/spec/lib/vcr/version_spec.rb +27 -0
  133. data/spec/lib/vcr_spec.rb +349 -0
  134. data/spec/monkey_patches.rb +182 -0
  135. data/spec/spec_helper.rb +62 -0
  136. data/spec/support/configuration_stubbing.rb +8 -0
  137. data/spec/support/cucumber_helpers.rb +35 -0
  138. data/spec/support/fixnum_extension.rb +10 -0
  139. data/spec/support/http_library_adapters.rb +289 -0
  140. data/spec/support/limited_uri.rb +21 -0
  141. data/spec/support/ruby_interpreter.rb +7 -0
  142. data/spec/support/shared_example_groups/excon.rb +63 -0
  143. data/spec/support/shared_example_groups/hook_into_http_library.rb +594 -0
  144. data/spec/support/shared_example_groups/request_hooks.rb +59 -0
  145. data/spec/support/sinatra_app.rb +86 -0
  146. data/spec/support/vcr_localhost_server.rb +76 -0
  147. data/spec/support/vcr_stub_helpers.rb +17 -0
  148. 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
+
@@ -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