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.
Files changed (113) hide show
  1. data/.gitignore +2 -0
  2. data/.limited_red +1 -0
  3. data/.travis.yml +10 -1
  4. data/.yardopts +9 -0
  5. data/CHANGELOG.md +51 -1
  6. data/Gemfile +5 -1
  7. data/LICENSE +1 -1
  8. data/README.md +23 -28
  9. data/Rakefile +63 -18
  10. data/Upgrade.md +200 -0
  11. data/features/.nav +2 -0
  12. data/features/cassettes/automatic_re_recording.feature +19 -15
  13. data/features/cassettes/dynamic_erb.feature +12 -4
  14. data/features/cassettes/exclusive.feature +31 -23
  15. data/features/cassettes/format.feature +54 -30
  16. data/features/cassettes/naming.feature +1 -1
  17. data/features/cassettes/update_content_length_header.feature +16 -12
  18. data/features/configuration/allow_http_connections_when_no_cassette.feature +1 -1
  19. data/features/configuration/debug_logging.feature +52 -0
  20. data/features/configuration/filter_sensitive_data.feature +4 -4
  21. data/features/configuration/hook_into.feature +5 -2
  22. data/features/configuration/ignore_request.feature +5 -3
  23. data/features/configuration/preserve_exact_body_bytes.feature +103 -0
  24. data/features/hooks/after_http_request.feature +17 -4
  25. data/features/hooks/around_http_request.feature +2 -1
  26. data/features/hooks/before_http_request.feature +25 -8
  27. data/features/hooks/before_playback.feature +16 -12
  28. data/features/hooks/before_record.feature +2 -2
  29. data/features/http_libraries/em_http_request.feature +82 -58
  30. data/features/http_libraries/net_http.feature +6 -6
  31. data/features/middleware/faraday.feature +2 -1
  32. data/features/middleware/rack.feature +2 -2
  33. data/features/record_modes/all.feature +19 -15
  34. data/features/record_modes/new_episodes.feature +17 -13
  35. data/features/record_modes/none.feature +15 -11
  36. data/features/record_modes/once.feature +16 -12
  37. data/features/request_matching/body.feature +28 -20
  38. data/features/request_matching/custom_matcher.feature +28 -20
  39. data/features/request_matching/headers.feature +34 -26
  40. data/features/request_matching/host.feature +28 -20
  41. data/features/request_matching/identical_request_sequence.feature +28 -20
  42. data/features/request_matching/method.feature +28 -20
  43. data/features/request_matching/path.feature +28 -20
  44. data/features/request_matching/playback_repeats.feature +28 -20
  45. data/features/request_matching/uri.feature +28 -20
  46. data/features/request_matching/uri_without_param.feature +28 -20
  47. data/features/support/env.rb +7 -6
  48. data/features/support/vcr_cucumber_helpers.rb +1 -0
  49. data/features/test_frameworks/cucumber.feature +8 -8
  50. data/features/test_frameworks/rspec_macro.feature +4 -4
  51. data/features/test_frameworks/rspec_metadata.feature +6 -6
  52. data/features/test_frameworks/shoulda.feature +1 -1
  53. data/features/test_frameworks/test_unit.feature +1 -1
  54. data/lib/vcr.rb +156 -5
  55. data/lib/vcr/cassette.rb +80 -30
  56. data/lib/vcr/cassette/http_interaction_list.rb +33 -4
  57. data/lib/vcr/cassette/migrator.rb +2 -3
  58. data/lib/vcr/cassette/reader.rb +1 -0
  59. data/lib/vcr/cassette/serializers.rb +22 -0
  60. data/lib/vcr/cassette/serializers/json.rb +27 -2
  61. data/lib/vcr/cassette/serializers/psych.rb +26 -2
  62. data/lib/vcr/cassette/serializers/syck.rb +28 -2
  63. data/lib/vcr/cassette/serializers/yaml.rb +28 -2
  64. data/lib/vcr/configuration.rb +348 -10
  65. data/lib/vcr/deprecations.rb +8 -0
  66. data/lib/vcr/errors.rb +40 -0
  67. data/lib/vcr/extensions/net_http_response.rb +12 -11
  68. data/lib/vcr/library_hooks.rb +1 -0
  69. data/lib/vcr/library_hooks/excon.rb +24 -3
  70. data/lib/vcr/library_hooks/fakeweb.rb +32 -16
  71. data/lib/vcr/library_hooks/faraday.rb +3 -0
  72. data/lib/vcr/library_hooks/typhoeus.rb +40 -37
  73. data/lib/vcr/library_hooks/webmock.rb +54 -34
  74. data/lib/vcr/middleware/faraday.rb +13 -0
  75. data/lib/vcr/middleware/rack.rb +35 -0
  76. data/lib/vcr/request_handler.rb +60 -8
  77. data/lib/vcr/request_ignorer.rb +1 -0
  78. data/lib/vcr/request_matcher_registry.rb +28 -0
  79. data/lib/vcr/structs.rb +245 -38
  80. data/lib/vcr/test_frameworks/cucumber.rb +10 -0
  81. data/lib/vcr/test_frameworks/rspec.rb +26 -1
  82. data/lib/vcr/util/hooks.rb +29 -27
  83. data/lib/vcr/util/internet_connection.rb +2 -0
  84. data/lib/vcr/util/logger.rb +25 -0
  85. data/lib/vcr/util/variable_args_block_caller.rb +1 -0
  86. data/lib/vcr/util/version_checker.rb +1 -0
  87. data/lib/vcr/version.rb +8 -1
  88. data/spec/capture_warnings.rb +3 -3
  89. data/spec/monkey_patches.rb +28 -13
  90. data/spec/spec_helper.rb +17 -0
  91. data/spec/support/http_library_adapters.rb +7 -4
  92. data/spec/support/shared_example_groups/hook_into_http_library.rb +96 -32
  93. data/spec/support/shared_example_groups/request_hooks.rb +9 -8
  94. data/spec/support/sinatra_app.rb +3 -1
  95. data/spec/support/vcr_localhost_server.rb +1 -0
  96. data/spec/vcr/cassette/http_interaction_list_spec.rb +119 -54
  97. data/spec/vcr/cassette/migrator_spec.rb +19 -6
  98. data/spec/vcr/cassette/serializers_spec.rb +51 -6
  99. data/spec/vcr/cassette_spec.rb +44 -19
  100. data/spec/vcr/configuration_spec.rb +91 -6
  101. data/spec/vcr/library_hooks/excon_spec.rb +54 -16
  102. data/spec/vcr/library_hooks/fakeweb_spec.rb +12 -21
  103. data/spec/vcr/library_hooks/typhoeus_spec.rb +2 -29
  104. data/spec/vcr/library_hooks/webmock_spec.rb +4 -18
  105. data/spec/vcr/middleware/faraday_spec.rb +1 -16
  106. data/spec/vcr/structs_spec.rb +194 -61
  107. data/spec/vcr/test_frameworks/rspec_spec.rb +10 -0
  108. data/spec/vcr/util/hooks_spec.rb +104 -56
  109. data/spec/vcr/util/version_checker_spec.rb +45 -0
  110. data/spec/vcr_spec.rb +11 -0
  111. data/vcr.gemspec +30 -34
  112. metadata +149 -95
  113. 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)
@@ -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
@@ -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
- return on_ignored_request if should_ignore?
6
- return on_stubbed_request if stubbed_response
7
- return on_recordable_request if VCR.real_http_connections_allowed?
8
- on_connection_not_allowed
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
- VCR.configuration.invoke_hook(:before_http_request, vcr_request)
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, vcr_request, vcr_response)
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 on_connection_not_allowed
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
@@ -3,6 +3,7 @@ require 'set'
3
3
  require 'vcr/util/hooks'
4
4
 
5
5
  module VCR
6
+ # @private
6
7
  class RequestIgnorer
7
8
  include VCR::Hooks
8
9
 
@@ -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' => 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
- # transforms the request into a fiber aware one
112
- def fiber_aware
113
- extend FiberAware
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
- module FiberAware
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
- def ignore!
164
- @ignored = true
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
- def ignored?
168
- !!@ignored
169
- end
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
- def filter!(text, replacement_text)
172
- return self if [text, replacement_text].any? { |t| t.to_s.empty? }
173
- filter_object!(self, text, replacement_text)
174
- end
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
- private
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
- def filter_object!(object, text, replacement_text)
179
- if object.respond_to?(:gsub)
180
- object.gsub!(text, replacement_text) if object.include?(text)
181
- elsif Hash === object
182
- filter_hash!(object, text, replacement_text)
183
- elsif object.respond_to?(:each)
184
- # This handles nested arrays and structs
185
- object.each { |o| filter_object!(o, text, replacement_text) }
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
- object
189
- end
352
+ private
190
353
 
191
- def filter_hash!(hash, text, replacement_text)
192
- filter_object!(hash.values, text, replacement_text)
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
- hash.keys.each do |k|
195
- new_key = filter_object!(k.dup, text, replacement_text)
196
- hash[new_key] = hash.delete(k) unless k == new_key
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' => 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