require 'vcr/util/logger' require 'vcr/util/variable_args_block_caller' require 'vcr/cassette' require 'vcr/cassette/serializers' require 'vcr/cassette/persisters' require 'vcr/linked_cassette' require 'vcr/configuration' require 'vcr/deprecations' require 'vcr/errors' require 'vcr/library_hooks' require 'vcr/request_ignorer' require 'vcr/request_matcher_registry' require 'vcr/structs' require 'vcr/version' # The main entry point for VCR. # @note This module is extended onto itself; thus, the methods listed # here as instance methods are available directly off of VCR. module VCR include VariableArgsBlockCaller include Errors extend self # Mutex to synchronize access to cassettes in a threaded environment CassetteMutex = Mutex.new # The main thread in which VCR was loaded MainThread = Thread.current autoload :CucumberTags, 'vcr/test_frameworks/cucumber' autoload :InternetConnection, 'vcr/util/internet_connection' module RSpec autoload :Metadata, 'vcr/test_frameworks/rspec' end module Middleware autoload :Faraday, 'vcr/middleware/faraday' autoload :Rack, 'vcr/middleware/rack' end # The currently active cassette. # # @return [nil, VCR::Cassette] The current cassette or nil if there is # no current cassette. def current_cassette cassettes.last end # Inserts the named cassette using the given cassette options. # New HTTP interactions, if allowed by the cassette's `:record` option, will # be recorded to the cassette. The cassette's existing HTTP interactions # will be used to stub requests, unless prevented by the cassette's # `:record` option. # # @example # VCR.insert_cassette('twitter', :record => :new_episodes) # # # ...later, after making an HTTP request: # # VCR.eject_cassette # # @param name [#to_s] The name of the cassette. VCR will sanitize # this to ensure it is a valid file name. # @param options [Hash] The cassette options. The given options will # be merged with the configured default_cassette_options. # @option options :record [:all, :none, :new_episodes, :once] The record mode. # @option options :erb [Boolean, Hash] Whether or not to evaluate the # cassette as an ERB template. Defaults to false. A hash can be used # to provide the ERB template with local variables. # @option options :match_requests_on [Array] List of request matchers # to use to determine what recorded HTTP interaction to replay. Defaults to # [:method, :uri]. The built-in matchers are :method, :uri, :host, :path, :headers # and :body. You can also pass the name of a registered custom request matcher or # any object that responds to #call. # @option options :re_record_interval [Integer] When given, the # cassette will be re-recorded at the given interval, in seconds. # @option options :tag [Symbol] Used to apply tagged `before_record` # and `before_playback` hooks to the cassette. # @option options :tags [Array] Used to apply multiple tags to # a cassette so that tagged `before_record` and `before_playback` hooks # will apply to the cassette. # @option options :update_content_length_header [Boolean] Whether or # not to overwrite the Content-Length header of the responses to # match the length of the response body. Defaults to false. # @option options :decode_compressed_response [Boolean] Whether or # not to decode compressed responses before recording the cassette. # This makes the cassette more human readable. Defaults to false. # @option options :allow_playback_repeats [Boolean] Whether or not to # allow a single HTTP interaction to be played back multiple times. # Defaults to false. # @option options :allow_unused_http_interactions [Boolean] If set to # false, an error will be raised if a cassette is ejected before all # previously recorded HTTP interactions have been used. # Defaults to true. Note that when an error has already occurred # (as indicated by the `$!` variable) unused interactions will be # allowed so that we don't silence the original error (which is almost # certainly more interesting/important). # @option options :exclusive [Boolean] Whether or not to use only this # cassette and to completely ignore any cassettes in the cassettes stack. # Defaults to false. # @option options :serialize_with [Symbol] Which serializer to use. # Valid values are :yaml, :syck, :psych, :json or any registered # custom serializer. Defaults to :yaml. # @option options :persist_with [Symbol] Which cassette persister to # use. Defaults to :file_system. You can also register and use a # custom persister. # @option options :persister_options [Hash] Pass options to the # persister specified in `persist_with`. Currently available options for the file_system persister: # - `:downcase_cassette_names`: when `true`, names of cassettes will be # normalized in lowercase before reading and writing, which can avoid # confusion when using both case-sensitive and case-insensitive file # systems. # @option options :preserve_exact_body_bytes [Boolean] Whether or not # to base64 encode the bytes of the requests and responses for this cassette # when serializing it. See also `VCR::Configuration#preserve_exact_body_bytes`. # # @return [VCR::Cassette] the inserted cassette # # @raise [ArgumentError] when the given cassette is already being used. # @raise [VCR::Errors::TurnedOffError] when VCR has been turned off # without using the :ignore_cassettes option. # @raise [VCR::Errors::MissingERBVariableError] when the `:erb` option # is used and the ERB template requires variables that you did not provide. # # @note If you use this method you _must_ call `eject_cassette` when you # are done. It is generally recommended that you use {#use_cassette} # unless your code-under-test cannot be run as a block. # def insert_cassette(name, options = {}) if turned_on? if cassettes.any? { |c| c.name == name } raise ArgumentError.new("There is already a cassette with the same name (#{name}). You cannot nest multiple cassettes with the same name.") end cassette = Cassette.new(name, options) context_cassettes.push(cassette) cassette elsif !ignore_cassettes? message = "VCR is turned off. You must turn it on before you can insert a cassette. " + "Or you can use the `:ignore_cassettes => true` option to completely ignore cassette insertions." raise TurnedOffError.new(message) end end # Ejects the current cassette. The cassette will no longer be used. # In addition, any newly recorded HTTP interactions will be written to # disk. # # @param options [Hash] Eject options. # @option options :skip_no_unused_interactions_assertion [Boolean] # If `true` is given, this will skip the "no unused HTTP interactions" # assertion enabled by the `:allow_unused_http_interactions => false` # cassette option. This is intended for use when your test has had # an error, but your test framework has already handled it. # @return [VCR::Cassette, nil] the ejected cassette if there was one def eject_cassette(options = {}) cassette = cassettes.last cassette.eject(options) if cassette cassette ensure context_cassettes.delete(cassette) end # Inserts a cassette using the given name and options, runs the given # block, and ejects the cassette. # # @example # VCR.use_cassette('twitter', :record => :new_episodes) do # # make an HTTP request # end # # @param (see #insert_cassette) # @option (see #insert_cassette) # @yield Block to run while this cassette is in use. # @yieldparam cassette [(optional) VCR::Cassette] the cassette that has # been inserted. # @raise (see #insert_cassette) # @return [void] # @see #insert_cassette # @see #eject_cassette def use_cassette(name, options = {}, &block) unless block raise ArgumentError, "`VCR.use_cassette` requires a block. " + "If you cannot wrap your code in a block, use " + "`VCR.insert_cassette` / `VCR.eject_cassette` instead." end cassette = insert_cassette(name, options) begin call_block(block, cassette) rescue StandardError cassette.run_failed! raise ensure eject_cassette end end # Inserts multiple cassettes the given names # # @example # cassettes = [ # { name: 'github' }, # { name: 'apple', options: { erb: true } } # ] # VCR.use_cassettes(cassettes) do # # make multiple HTTP requests # end def use_cassettes(cassettes, &block) cassette = cassettes.pop use_cassette(cassette[:name], cassette[:options] || {}) do if cassettes.empty? block.call else use_cassettes(cassettes, &block) end end end # Used to configure VCR. # # @example # VCR.configure do |c| # c.some_config_option = true # end # # @yield the configuration block # @yieldparam config [VCR::Configuration] the configuration object # @return [void] def configure yield configuration end # @return [VCR::Configuration] the VCR configuration. def configuration @configuration end # Sets up `Before` and `After` cucumber hooks in order to # use VCR with particular cucumber tags. # # @example # VCR.cucumber_tags do |t| # t.tags "tag1", "tag2" # t.tag "@some_other_tag", :record => :new_episodes # end # # @yield the cucumber tags configuration block # @yieldparam t [VCR::CucumberTags] Cucumber tags config object # @return [void] # @see VCR::CucumberTags#tags def cucumber_tags(&block) main_object = eval('self', block.binding) yield VCR::CucumberTags.new(main_object) end # Turns VCR off for the duration of a block. # # @param (see #turn_off!) # @return [void] # @raise (see #turn_off!) # @see #turn_off! # @see #turn_on! # @see #turned_on? # @see #turned_on def turned_off(options = {}) turn_off!(options) begin yield ensure turn_on! end end # Turns VCR off, so that it no longer handles every HTTP request. # # @param options [Hash] hash of options # @option options :ignore_cassettes [Boolean] controls what happens when a cassette is # inserted while VCR is turned off. If `true` is passed, the cassette insertion # will be ignored; otherwise a {VCR::Errors::TurnedOffError} will be raised. # # @return [void] # @raise [VCR::Errors::CassetteInUseError] if there is currently a cassette in use # @raise [ArgumentError] if you pass an invalid option def turn_off!(options = {}) if VCR.current_cassette raise CassetteInUseError, "A VCR cassette is currently in use (#{VCR.current_cassette.name}). " + "You must eject it before you can turn VCR off." end set_context_value(:ignore_cassettes, options.fetch(:ignore_cassettes, false)) invalid_options = options.keys - [:ignore_cassettes] if invalid_options.any? raise ArgumentError.new("You passed some invalid options: #{invalid_options.inspect}") end set_context_value(:turned_off, true) end # Turns on VCR, for the duration of a block. # @param (see #turn_off!) # @return [void] # @see #turn_off! # @see #turned_off # @see #turned_on? def turned_on(options = {}) turn_on! begin yield ensure turn_off!(options) end end # Turns on VCR, if it has previously been turned off. # @return [void] # @see #turn_off! # @see #turned_off # @see #turned_on? # @see #turned_on def turn_on! set_context_value(:turned_off, false) end # @return whether or not VCR is turned on # @note Normally VCR is _always_ turned on; it will only be off if you have # explicitly turned it off. # @see #turn_on! # @see #turn_off! # @see #turned_off def turned_on? linked_context = current_context[:linked_context] return !linked_context[:turned_off] if linked_context !context_value(:turned_off) end # @private def http_interactions return current_cassette.http_interactions if current_cassette VCR::Cassette::HTTPInteractionList::NullList end # @private def real_http_connections_allowed? return current_cassette.recording? if current_cassette !!(configuration.allow_http_connections_when_no_cassette? || !turned_on?) end # @return [RequestMatcherRegistry] the request matcher registry def request_matchers @request_matchers end # @return [Enumerable] list of all cassettes currently being used def cassettes(context = current_context) linked_context = context[:linked_context] linked_cassettes = cassettes(linked_context) if linked_context LinkedCassette.list(context[:cassettes], Array(linked_cassettes)) end # @private def request_ignorer @request_ignorer end # @private def library_hooks @library_hooks end # @private def cassette_serializers @cassette_serializers end # @private def cassette_persisters @cassette_persisters end # @private def record_http_interaction(interaction) return unless cassette = current_cassette return if VCR.request_ignorer.ignore?(interaction.request) cassette.record_http_interaction(interaction) end # @private def link_context(from_thread, to_key) @context[to_key] = get_context(from_thread) end # @private def unlink_context(key) @context.delete(key) end # @private def fibers_available? @fibers_available end private def current_context get_context(Thread.current, Fiber.current) end def get_context(thread_key, fiber_key = nil) context = @context[fiber_key] if fiber_key context ||= @context[thread_key] if context context else @context[thread_key] = dup_context(@context[MainThread]) end end def context_value(name) current_context[name] end def set_context_value(name, value) current_context[name] = value end def dup_context(context) { :turned_off => context[:turned_off], :ignore_cassettes => context[:ignore_cassettes], :cassettes => [], :linked_context => context } end def ignore_cassettes? context_value(:ignore_cassettes) end def context_cassettes context_value(:cassettes) end def initialize_fibers begin require 'fiber' @fibers_available = true rescue LoadError @fibers_available = false end end def initialize_ivars initialize_fibers @context = { MainThread => { :turned_off => false, :ignore_cassettes => false, :cassettes => [], :linked_context => nil } } @configuration = Configuration.new @request_matchers = RequestMatcherRegistry.new @request_ignorer = RequestIgnorer.new @library_hooks = LibraryHooks.new @cassette_serializers = Cassette::Serializers.new @cassette_persisters = Cassette::Persisters.new end initialize_ivars # to avoid warnings end