mustwin-vcr 2.9.3

Sign up to get free protection for your applications and to get access to all the features.
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,347 @@
1
+ require 'vcr/util/logger'
2
+ require 'vcr/util/variable_args_block_caller'
3
+
4
+ require 'vcr/cassette'
5
+ require 'vcr/cassette/serializers'
6
+ require 'vcr/cassette/persisters'
7
+ require 'vcr/configuration'
8
+ require 'vcr/deprecations'
9
+ require 'vcr/errors'
10
+ require 'vcr/library_hooks'
11
+ require 'vcr/request_ignorer'
12
+ require 'vcr/request_matcher_registry'
13
+ require 'vcr/structs'
14
+ require 'vcr/version'
15
+
16
+ # The main entry point for VCR.
17
+ # @note This module is extended onto itself; thus, the methods listed
18
+ # here as instance methods are available directly off of VCR.
19
+ module VCR
20
+ include VariableArgsBlockCaller
21
+ include Errors
22
+
23
+ extend self
24
+
25
+ autoload :CucumberTags, 'vcr/test_frameworks/cucumber'
26
+ autoload :InternetConnection, 'vcr/util/internet_connection'
27
+
28
+ module RSpec
29
+ autoload :Metadata, 'vcr/test_frameworks/rspec'
30
+ autoload :Macros, 'vcr/deprecations'
31
+ end
32
+
33
+ module Middleware
34
+ autoload :Faraday, 'vcr/middleware/faraday'
35
+ autoload :Rack, 'vcr/middleware/rack'
36
+ end
37
+
38
+ # The currently active cassette.
39
+ #
40
+ # @return [nil, VCR::Cassette] The current cassette or nil if there is
41
+ # no current cassette.
42
+ def current_cassette
43
+ cassettes.last
44
+ end
45
+
46
+ # Inserts the named cassette using the given cassette options.
47
+ # New HTTP interactions, if allowed by the cassette's `:record` option, will
48
+ # be recorded to the cassette. The cassette's existing HTTP interactions
49
+ # will be used to stub requests, unless prevented by the cassete's
50
+ # `:record` option.
51
+ #
52
+ # @example
53
+ # VCR.insert_cassette('twitter', :record => :new_episodes)
54
+ #
55
+ # # ...later, after making an HTTP request:
56
+ #
57
+ # VCR.eject_cassette
58
+ #
59
+ # @param name [#to_s] The name of the cassette. VCR will sanitize
60
+ # this to ensure it is a valid file name.
61
+ # @param options [Hash] The cassette options. The given options will
62
+ # be merged with the configured default_cassette_options.
63
+ # @option options :record [:all, :none, :new_episodes, :once] The record mode.
64
+ # @option options :erb [Boolean, Hash] Whether or not to evaluate the
65
+ # cassette as an ERB template. Defaults to false. A hash can be used
66
+ # to provide the ERB template with local variables.
67
+ # @option options :match_requests_on [Array<Symbol, #call>] List of request matchers
68
+ # to use to determine what recorded HTTP interaction to replay. Defaults to
69
+ # [:method, :uri]. The built-in matchers are :method, :uri, :host, :path, :headers
70
+ # and :body. You can also pass the name of a registered custom request matcher or
71
+ # any object that responds to #call.
72
+ # @option options :re_record_interval [Integer] When given, the
73
+ # cassette will be re-recorded at the given interval, in seconds.
74
+ # @option options :tag [Symbol] Used to apply tagged `before_record`
75
+ # and `before_playback` hooks to the cassette.
76
+ # @option options :tags [Array<Symbol>] Used to apply multiple tags to
77
+ # a cassette so that tagged `before_record` and `before_playback` hooks
78
+ # will apply to the cassette.
79
+ # @option options :update_content_length_header [Boolean] Whether or
80
+ # not to overwrite the Content-Length header of the responses to
81
+ # match the length of the response body. Defaults to false.
82
+ # @option options :decode_compressed_response [Boolean] Whether or
83
+ # not to decode compressed responses before recording the cassette.
84
+ # This makes the cassette more human readable. Defaults to false.
85
+ # @option options :allow_playback_repeats [Boolean] Whether or not to
86
+ # allow a single HTTP interaction to be played back multiple times.
87
+ # Defaults to false.
88
+ # @option options :allow_unused_http_interactions [Boolean] If set to
89
+ # false, an error will be raised if a cassette is ejected before all
90
+ # previously recorded HTTP interactions have been used.
91
+ # Defaults to true. Note that when an error has already occurred
92
+ # (as indicated by the `$!` variable) unused interactions will be
93
+ # allowed so that we don't silence the original error (which is almost
94
+ # certainly more interesting/important).
95
+ # @option options :exclusive [Boolean] Whether or not to use only this
96
+ # cassette and to completely ignore any cassettes in the cassettes stack.
97
+ # Defaults to false.
98
+ # @option options :serialize_with [Symbol] Which serializer to use.
99
+ # Valid values are :yaml, :syck, :psych, :json or any registered
100
+ # custom serializer. Defaults to :yaml.
101
+ # @option options :persist_with [Symbol] Which cassette persister to
102
+ # use. Defaults to :file_system. You can also register and use a
103
+ # custom persister.
104
+ # @option options :preserve_exact_body_bytes [Boolean] Whether or not
105
+ # to base64 encode the bytes of the requests and responses for this cassette
106
+ # when serializing it. See also `VCR::Configuration#preserve_exact_body_bytes`.
107
+ #
108
+ # @return [VCR::Cassette] the inserted cassette
109
+ #
110
+ # @raise [ArgumentError] when the given cassette is already being used.
111
+ # @raise [VCR::Errors::TurnedOffError] when VCR has been turned off
112
+ # without using the :ignore_cassettes option.
113
+ # @raise [VCR::Errors::MissingERBVariableError] when the `:erb` option
114
+ # is used and the ERB template requires variables that you did not provide.
115
+ #
116
+ # @note If you use this method you _must_ call `eject_cassette` when you
117
+ # are done. It is generally recommended that you use {#use_cassette}
118
+ # unless your code-under-test cannot be run as a block.
119
+ #
120
+ def insert_cassette(name, options = {})
121
+ if turned_on?
122
+ if cassettes.any? { |c| c.name == name }
123
+ raise ArgumentError.new("There is already a cassette with the same name (#{name}). You cannot nest multiple cassettes with the same name.")
124
+ end
125
+
126
+ cassette = Cassette.new(name, options)
127
+ cassettes.push(cassette)
128
+ cassette
129
+ elsif !ignore_cassettes?
130
+ message = "VCR is turned off. You must turn it on before you can insert a cassette. " +
131
+ "Or you can use the `:ignore_cassettes => true` option to completely ignore cassette insertions."
132
+ raise TurnedOffError.new(message)
133
+ end
134
+ end
135
+
136
+ # Ejects the current cassette. The cassette will no longer be used.
137
+ # In addition, any newly recorded HTTP interactions will be written to
138
+ # disk.
139
+ #
140
+ # @param options [Hash] Eject options.
141
+ # @option options :skip_no_unused_interactions_assertion [Boolean]
142
+ # If `true` is given, this will skip the "no unused HTTP interactions"
143
+ # assertion enabled by the `:allow_unused_http_interactions => false`
144
+ # cassette option. This is intended for use when your test has had
145
+ # an error, but your test framework has already handled it.
146
+ # @return [VCR::Cassette, nil] the ejected cassette if there was one
147
+ def eject_cassette(options = {})
148
+ cassette = cassettes.last
149
+ cassette.eject(options) if cassette
150
+ cassette
151
+ ensure
152
+ cassettes.pop
153
+ end
154
+
155
+ # Inserts a cassette using the given name and options, runs the given
156
+ # block, and ejects the cassette.
157
+ #
158
+ # @example
159
+ # VCR.use_cassette('twitter', :record => :new_episodes) do
160
+ # # make an HTTP request
161
+ # end
162
+ #
163
+ # @param (see #insert_cassette)
164
+ # @option (see #insert_cassette)
165
+ # @yield Block to run while this cassette is in use.
166
+ # @yieldparam cassette [(optional) VCR::Cassette] the cassette that has
167
+ # been inserted.
168
+ # @raise (see #insert_cassette)
169
+ # @return [void]
170
+ # @see #insert_cassette
171
+ # @see #eject_cassette
172
+ def use_cassette(name, options = {}, &block)
173
+ unless block
174
+ raise ArgumentError, "`VCR.use_cassette` requires a block. " +
175
+ "If you cannot wrap your code in a block, use " +
176
+ "`VCR.insert_cassette` / `VCR.eject_cassette` instead."
177
+ end
178
+
179
+ cassette = insert_cassette(name, options)
180
+
181
+ begin
182
+ call_block(block, cassette)
183
+ ensure
184
+ eject_cassette
185
+ end
186
+ end
187
+
188
+ # Used to configure VCR.
189
+ #
190
+ # @example
191
+ # VCR.configure do |c|
192
+ # c.some_config_option = true
193
+ # end
194
+ #
195
+ # @yield the configuration block
196
+ # @yieldparam config [VCR::Configuration] the configuration object
197
+ # @return [void]
198
+ def configure
199
+ yield configuration
200
+ end
201
+
202
+ # @return [VCR::Configuration] the VCR configuration.
203
+ def configuration
204
+ @configuration ||= Configuration.new
205
+ end
206
+
207
+ # Sets up `Before` and `After` cucumber hooks in order to
208
+ # use VCR with particular cucumber tags.
209
+ #
210
+ # @example
211
+ # VCR.cucumber_tags do |t|
212
+ # t.tags "tag1", "tag2"
213
+ # t.tag "@some_other_tag", :record => :new_episodes
214
+ # end
215
+ #
216
+ # @yield the cucumber tags configuration block
217
+ # @yieldparam t [VCR::CucumberTags] Cucumber tags config object
218
+ # @return [void]
219
+ # @see VCR::CucumberTags#tags
220
+ def cucumber_tags(&block)
221
+ main_object = eval('self', block.binding)
222
+ yield VCR::CucumberTags.new(main_object)
223
+ end
224
+
225
+ # Turns VCR off for the duration of a block.
226
+ #
227
+ # @param (see #turn_off!)
228
+ # @return [void]
229
+ # @raise (see #turn_off!)
230
+ # @see #turn_off!
231
+ # @see #turn_on!
232
+ # @see #turned_on?
233
+ def turned_off(options = {})
234
+ turn_off!(options)
235
+
236
+ begin
237
+ yield
238
+ ensure
239
+ turn_on!
240
+ end
241
+ end
242
+
243
+ # Turns VCR off, so that it no longer handles every HTTP request.
244
+ #
245
+ # @param options [Hash] hash of options
246
+ # @option options :ignore_cassettes [Boolean] controls what happens when a cassette is
247
+ # inserted while VCR is turned off. If `true` is passed, the cassette insertion
248
+ # will be ignored; otherwise a {VCR::Errors::TurnedOffError} will be raised.
249
+ #
250
+ # @return [void]
251
+ # @raise [VCR::Errors::CassetteInUseError] if there is currently a cassette in use
252
+ # @raise [ArgumentError] if you pass an invalid option
253
+ def turn_off!(options = {})
254
+ if VCR.current_cassette
255
+ raise CassetteInUseError, "A VCR cassette is currently in use (#{VCR.current_cassette.name}). " +
256
+ "You must eject it before you can turn VCR off."
257
+ end
258
+
259
+ @ignore_cassettes = options[:ignore_cassettes]
260
+ invalid_options = options.keys - [:ignore_cassettes]
261
+ if invalid_options.any?
262
+ raise ArgumentError.new("You passed some invalid options: #{invalid_options.inspect}")
263
+ end
264
+
265
+ @turned_off = true
266
+ end
267
+
268
+ # Turns on VCR, if it has previously been turned off.
269
+ # @return [void]
270
+ # @see #turn_off!
271
+ # @see #turned_off
272
+ # @see #turned_on?
273
+ def turn_on!
274
+ @turned_off = false
275
+ end
276
+
277
+ # @return whether or not VCR is turned on
278
+ # @note Normally VCR is _always_ turned on; it will only be off if you have
279
+ # explicitly turned it off.
280
+ # @see #turn_on!
281
+ # @see #turn_off!
282
+ # @see #turned_off
283
+ def turned_on?
284
+ !@turned_off
285
+ end
286
+
287
+ # @private
288
+ def http_interactions
289
+ return current_cassette.http_interactions if current_cassette
290
+ VCR::Cassette::HTTPInteractionList::NullList
291
+ end
292
+
293
+ # @private
294
+ def real_http_connections_allowed?
295
+ return current_cassette.recording? if current_cassette
296
+ !!(configuration.allow_http_connections_when_no_cassette? || @turned_off)
297
+ end
298
+
299
+ # @return [RequestMatcherRegistry] the request matcher registry
300
+ def request_matchers
301
+ @request_matchers ||= RequestMatcherRegistry.new
302
+ end
303
+
304
+ # @private
305
+ def request_ignorer
306
+ @request_ignorer ||= RequestIgnorer.new
307
+ end
308
+
309
+ # @private
310
+ def library_hooks
311
+ @library_hooks ||= LibraryHooks.new
312
+ end
313
+
314
+ # @private
315
+ def cassette_serializers
316
+ @cassette_serializers ||= Cassette::Serializers.new
317
+ end
318
+
319
+ # @private
320
+ def cassette_persisters
321
+ @cassette_persisters ||= Cassette::Persisters.new
322
+ end
323
+
324
+ # @private
325
+ def record_http_interaction(interaction)
326
+ return unless cassette = current_cassette
327
+ return if VCR.request_ignorer.ignore?(interaction.request)
328
+
329
+ cassette.record_http_interaction(interaction)
330
+ end
331
+
332
+ private
333
+
334
+ def ignore_cassettes?
335
+ @ignore_cassettes
336
+ end
337
+
338
+ def cassettes
339
+ @cassettes ||= []
340
+ end
341
+
342
+ def initialize_ivars
343
+ @turned_off = false
344
+ end
345
+
346
+ initialize_ivars # to avoid warnings
347
+ end
@@ -0,0 +1,291 @@
1
+ require 'vcr/cassette/http_interaction_list'
2
+ require 'vcr/cassette/erb_renderer'
3
+ require 'vcr/cassette/serializers'
4
+
5
+ module VCR
6
+ # The media VCR uses to store HTTP interactions for later re-use.
7
+ class Cassette
8
+ include Logger::Mixin
9
+
10
+ # The supported record modes.
11
+ #
12
+ # * :all -- Record every HTTP interactions; do not play any back.
13
+ # * :none -- Do not record any HTTP interactions; play them back.
14
+ # * :new_episodes -- Playback previously recorded HTTP interactions and record new ones.
15
+ # * :once -- Record the HTTP interactions if the cassette has not already been recorded;
16
+ # otherwise, playback the HTTP interactions.
17
+ VALID_RECORD_MODES = [:all, :none, :new_episodes, :once]
18
+
19
+ # @return [#to_s] The name of the cassette. Used to determine the cassette's file name.
20
+ # @see #file
21
+ attr_reader :name
22
+
23
+ # @return [Symbol] The record mode. Determines whether the cassette records HTTP interactions,
24
+ # plays them back, or does both.
25
+ attr_reader :record_mode
26
+
27
+ # @return [Array<Symbol, #call>] List of request matchers. Used to find a response from an
28
+ # existing HTTP interaction to play back.
29
+ attr_reader :match_requests_on
30
+
31
+ # @return [Boolean, Hash] The cassette's ERB option. The file will be treated as an
32
+ # ERB template if this has a truthy value. A hash, if provided, will be used as local
33
+ # variables for the ERB template.
34
+ attr_reader :erb
35
+
36
+ # @return [Integer, nil] How frequently (in seconds) the cassette should be re-recorded.
37
+ attr_reader :re_record_interval
38
+
39
+ # @return [Array<Symbol>] If set, {VCR::Configuration#before_record} and
40
+ # {VCR::Configuration#before_playback} hooks with a corresponding tag will apply.
41
+ attr_reader :tags
42
+
43
+ # @param (see VCR#insert_cassette)
44
+ # @see VCR#insert_cassette
45
+ def initialize(name, options = {})
46
+ @name = name
47
+ @options = VCR.configuration.default_cassette_options.merge(options)
48
+
49
+ assert_valid_options!
50
+ extract_options
51
+ raise_error_unless_valid_record_mode
52
+
53
+ log "Initialized with options: #{@options.inspect}"
54
+ end
55
+
56
+ # Ejects the current cassette. The cassette will no longer be used.
57
+ # In addition, any newly recorded HTTP interactions will be written to
58
+ # disk.
59
+ #
60
+ # @note This is not intended to be called directly. Use `VCR.eject_cassette` instead.
61
+ #
62
+ # @param (see VCR#eject_casssette)
63
+ # @see VCR#eject_cassette
64
+ def eject(options = {})
65
+ write_recorded_interactions_to_disk
66
+
67
+ if should_assert_no_unused_interactions? && !options[:skip_no_unused_interactions_assertion]
68
+ http_interactions.assert_no_unused_interactions!
69
+ end
70
+ end
71
+
72
+ # @private
73
+ def http_interactions
74
+ @http_interactions ||= HTTPInteractionList.new \
75
+ should_stub_requests? ? previously_recorded_interactions : [],
76
+ match_requests_on,
77
+ @allow_playback_repeats,
78
+ @parent_list,
79
+ log_prefix
80
+ end
81
+
82
+ # @private
83
+ def record_http_interaction(interaction)
84
+ log "Recorded HTTP interaction #{request_summary(interaction.request)} => #{response_summary(interaction.response)}"
85
+ new_recorded_interactions << interaction
86
+ end
87
+
88
+ # @private
89
+ def new_recorded_interactions
90
+ @new_recorded_interactions ||= []
91
+ end
92
+
93
+ # @return [String] The file for this cassette.
94
+ # @raise [NotImplementedError] if the configured cassette persister
95
+ # does not support resolving file paths.
96
+ # @note VCR will take care of sanitizing the cassette name to make it a valid file name.
97
+ def file
98
+ unless @persister.respond_to?(:absolute_path_to_file)
99
+ raise NotImplementedError, "The configured cassette persister does not support resolving file paths"
100
+ end
101
+ @persister.absolute_path_to_file(storage_key)
102
+ end
103
+
104
+ # @return [Boolean] Whether or not the cassette is recording.
105
+ def recording?
106
+ case record_mode
107
+ when :none; false
108
+ when :once; raw_cassette_bytes.to_s.empty?
109
+ else true
110
+ end
111
+ end
112
+
113
+ # @return [Hash] The hash that will be serialized when the cassette is written to disk.
114
+ def serializable_hash
115
+ {
116
+ "http_interactions" => interactions_to_record.map(&:to_hash),
117
+ "recorded_with" => "VCR #{VCR.version}"
118
+ }
119
+ end
120
+
121
+ # @return [Time, nil] The `recorded_at` time of the first HTTP interaction
122
+ # or nil if the cassette has no prior HTTP interactions.
123
+ #
124
+ # @example
125
+ #
126
+ # VCR.use_cassette("some cassette") do |cassette|
127
+ # Timecop.freeze(cassette.originally_recorded_at || Time.now) do
128
+ # # ...
129
+ # end
130
+ # end
131
+ def originally_recorded_at
132
+ @originally_recorded_at ||= previously_recorded_interactions.map(&:recorded_at).min
133
+ end
134
+
135
+ private
136
+
137
+ def assert_valid_options!
138
+ invalid_options = @options.keys - [
139
+ :record, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
140
+ :update_content_length_header, :allow_playback_repeats, :allow_unused_http_interactions,
141
+ :exclusive, :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response,
142
+ :persist_with
143
+ ]
144
+
145
+ if invalid_options.size > 0
146
+ raise ArgumentError.new("You passed the following invalid options to VCR::Cassette.new: #{invalid_options.inspect}.")
147
+ end
148
+ end
149
+
150
+ def extract_options
151
+ [:erb, :match_requests_on, :re_record_interval,
152
+ :allow_playback_repeats, :allow_unused_http_interactions, :exclusive].each do |name|
153
+ instance_variable_set("@#{name}", @options[name])
154
+ end
155
+
156
+ assign_tags
157
+
158
+ @record_mode = @options[:record]
159
+ @serializer = VCR.cassette_serializers[@options[:serialize_with]]
160
+ @persister = VCR.cassette_persisters[@options[:persist_with]]
161
+ @record_mode = :all if should_re_record?
162
+ @parent_list = @exclusive ? HTTPInteractionList::NullList : VCR.http_interactions
163
+ end
164
+
165
+ def assign_tags
166
+ @tags = Array(@options.fetch(:tags) { @options[:tag] })
167
+
168
+ [:update_content_length_header, :preserve_exact_body_bytes, :decode_compressed_response].each do |tag|
169
+ @tags << tag if @options[tag]
170
+ end
171
+ end
172
+
173
+ def previously_recorded_interactions
174
+ @previously_recorded_interactions ||= if !raw_cassette_bytes.to_s.empty?
175
+ deserialized_hash['http_interactions'].map { |h| HTTPInteraction.from_hash(h) }.tap do |interactions|
176
+ invoke_hook(:before_playback, interactions)
177
+
178
+ interactions.reject! do |i|
179
+ i.request.uri.is_a?(String) && VCR.request_ignorer.ignore?(i.request)
180
+ end
181
+ end
182
+ else
183
+ []
184
+ end
185
+ end
186
+
187
+ def storage_key
188
+ @storage_key ||= [name, @serializer.file_extension].join('.')
189
+ end
190
+
191
+ def raise_error_unless_valid_record_mode
192
+ unless VALID_RECORD_MODES.include?(record_mode)
193
+ raise ArgumentError.new("#{record_mode} is not a valid cassette record mode. Valid modes are: #{VALID_RECORD_MODES.inspect}")
194
+ end
195
+ end
196
+
197
+ def should_re_record?
198
+ return false unless @re_record_interval
199
+ return false unless originally_recorded_at
200
+
201
+ now = Time.now
202
+
203
+ (originally_recorded_at + @re_record_interval < now).tap do |value|
204
+ info = "previously recorded at: '#{originally_recorded_at}'; now: '#{now}'; interval: #{@re_record_interval} seconds"
205
+
206
+ if !value
207
+ log "Not re-recording since the interval has not elapsed (#{info})."
208
+ elsif InternetConnection.available?
209
+ log "re-recording (#{info})."
210
+ else
211
+ log "Not re-recording because no internet connection is available (#{info})."
212
+ return false
213
+ end
214
+ end
215
+ end
216
+
217
+ def should_stub_requests?
218
+ record_mode != :all
219
+ end
220
+
221
+ def should_remove_matching_existing_interactions?
222
+ record_mode == :all
223
+ end
224
+
225
+ def should_assert_no_unused_interactions?
226
+ !(@allow_unused_http_interactions || $!)
227
+ end
228
+
229
+ def raw_cassette_bytes
230
+ @raw_cassette_bytes ||= VCR::Cassette::ERBRenderer.new(@persister[storage_key], erb, name).render
231
+ end
232
+
233
+ def merged_interactions
234
+ old_interactions = previously_recorded_interactions
235
+
236
+ if should_remove_matching_existing_interactions?
237
+ new_interaction_list = HTTPInteractionList.new(new_recorded_interactions, match_requests_on)
238
+ old_interactions = old_interactions.reject do |i|
239
+ new_interaction_list.response_for(i.request)
240
+ end
241
+ end
242
+
243
+ old_interactions + new_recorded_interactions
244
+ end
245
+
246
+ def interactions_to_record
247
+ # We deep-dup the interactions by roundtripping them to/from a hash.
248
+ # This is necessary because `before_record` can mutate the interactions.
249
+ merged_interactions.map { |i| HTTPInteraction.from_hash(i.to_hash) }.tap do |interactions|
250
+ invoke_hook(:before_record, interactions)
251
+ end
252
+ end
253
+
254
+ def write_recorded_interactions_to_disk
255
+ return if new_recorded_interactions.none?
256
+ hash = serializable_hash
257
+ return if hash["http_interactions"].none?
258
+
259
+ @persister[storage_key] = @serializer.serialize(hash)
260
+ end
261
+
262
+ def invoke_hook(type, interactions)
263
+ interactions.delete_if do |i|
264
+ i.hook_aware.tap do |hw|
265
+ VCR.configuration.invoke_hook(type, hw, self)
266
+ end.ignored?
267
+ end
268
+ end
269
+
270
+ def deserialized_hash
271
+ @deserialized_hash ||= @serializer.deserialize(raw_cassette_bytes).tap do |hash|
272
+ unless hash.is_a?(Hash) && hash['http_interactions'].is_a?(Array)
273
+ raise Errors::InvalidCassetteFormatError.new \
274
+ "#{file} does not appear to be a valid VCR 2.0 cassette. " +
275
+ "VCR 1.x cassettes are not valid with VCR 2.0. When upgrading from " +
276
+ "VCR 1.x, it is recommended that you delete all your existing cassettes and " +
277
+ "re-record them, or use the provided vcr:migrate_cassettes rake task to migrate " +
278
+ "them. For more info, see the VCR upgrade guide."
279
+ end
280
+ end
281
+ end
282
+
283
+ def log_prefix
284
+ @log_prefix ||= "[Cassette: '#{name}'] "
285
+ end
286
+
287
+ def request_summary(request)
288
+ super(request, match_requests_on)
289
+ end
290
+ end
291
+ end