1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
|
require 'vcr/util/hooks'
require 'uri'
require 'cgi'
module VCR
# Stores the VCR configuration.
class Configuration
include Hooks
include VariableArgsBlockCaller
include Logger::Mixin
# Gets the directory to read cassettes from and write cassettes to.
#
# @return [String] the directory to read cassettes from and write cassettes to
def cassette_library_dir
VCR.cassette_persisters[:file_system].storage_location
end
# Sets the directory to read cassettes from and writes cassettes to.
#
# @example
# VCR.configure do |c|
# c.cassette_library_dir = 'spec/cassettes'
# end
#
# @param dir [String] the directory to read cassettes from and write cassettes to
# @return [void]
# @note This is only necessary if you use the `:file_system`
# cassette persister (the default).
def cassette_library_dir=(dir)
VCR.cassette_persisters[:file_system].storage_location = dir
end
# Default options to apply to every cassette.
#
# @overload default_cassette_options
# @return [Hash] default options to apply to every cassette
# @overload default_cassette_options=(options)
# @param options [Hash] default options to apply to every cassette
# @return [void]
# @example
# VCR.configure do |c|
# c.default_cassette_options = { :record => :new_episodes }
# end
# @note {VCR#insert_cassette} for the list of valid options.
attr_reader :default_cassette_options
# Sets the default options that apply to every cassette.
def default_cassette_options=(overrides)
@default_cassette_options.merge!(overrides)
end
# Configures which libraries VCR will hook into to intercept HTTP requests.
#
# @example
# VCR.configure do |c|
# c.hook_into :webmock, :typhoeus
# end
#
# @param hooks [Array<Symbol>] List of libraries. Valid values are
# `:webmock`, `:typhoeus`, `:excon` and `:faraday`.
# @raise [ArgumentError] when given an unsupported library name.
# @raise [VCR::Errors::LibraryVersionTooLowError] when the version
# of a library you are using is too low for VCR to support.
def hook_into(*hooks)
hooks.each { |a| load_library_hook(a) }
invoke_hook(:after_library_hooks_loaded)
end
# Specifies host(s) that VCR should ignore.
#
# @param hosts [Array<String>] List of hosts to ignore
# @see #ignore_localhost=
# @see #ignore_request
def ignore_hosts(*hosts)
VCR.request_ignorer.ignore_hosts(*hosts)
end
alias ignore_host ignore_hosts
# Specifies host(s) that VCR should stop ignoring.
#
# @param hosts [Array<String>] List of hosts to unignore
# @see #ignore_hosts
def unignore_hosts(*hosts)
VCR.request_ignorer.unignore_hosts(*hosts)
end
alias unignore_host unignore_hosts
# Sets whether or not VCR should ignore localhost requests.
#
# @param value [Boolean] the value to set
# @see #ignore_hosts
# @see #ignore_request
def ignore_localhost=(value)
VCR.request_ignorer.ignore_localhost = value
end
# Defines what requests to ignore using a block.
#
# @example
# VCR.configure do |c|
# c.ignore_request do |request|
# uri = URI(request.uri)
# # ignore only localhost requests to port 7500
# uri.host == 'localhost' && uri.port == 7500
# end
# end
#
# @yield the callback
# @yieldparam request [VCR::Request] the HTTP request
# @yieldreturn [Boolean] whether or not to ignore the request
def ignore_request(&block)
VCR.request_ignorer.ignore_request(&block)
end
# Determines how VCR treats HTTP requests that are made when
# no VCR cassette is in use. When set to `true`, requests made
# when there is no VCR cassette in use will be allowed. When set
# to `false` (the default), an {VCR::Errors::UnhandledHTTPRequestError}
# will be raised for any HTTP request made when there is no
# cassette in use.
#
# @overload allow_http_connections_when_no_cassette?
# @return [Boolean] whether or not HTTP connections are allowed
# when there is no cassette.
# @overload allow_http_connections_when_no_cassette=
# @param value [Boolean] sets whether or not to allow HTTP
# connections when there is no cassette.
attr_writer :allow_http_connections_when_no_cassette
# @private (documented above)
def allow_http_connections_when_no_cassette?
!!@allow_http_connections_when_no_cassette
end
# Sets a parser for VCR to use when parsing query strings for request
# comparisons. The new parser must implement a method `call` that returns
# an object which is both equalivant and consistent when given an HTTP
# query string of possibly differing value ordering.
#
# * `#== # => Boolean`
#
# The `#==` method must return true if both objects represent the
# same query string.
#
# This defaults to `CGI.parse` from the ruby standard library.
#
# @overload query_parser
# @return [#call] the current query string parser object
# @overload query_parser=
# @param value [#call] sets the query_parser
attr_accessor :query_parser
# Sets a parser for VCR to use when parsing URIs. The new parser
# must implement a method `parse` that returns an instance of the
# URI object. This URI object must implement the following
# interface:
#
# * `scheme # => String`
# * `host # => String`
# * `port # => Fixnum`
# * `path # => String`
# * `query # => String`
# * `#port=`
# * `#query=`
# * `#to_s # => String`
# * `#== # => Boolean`
#
# The `#==` method must return true if both URI objects represent the
# same URI.
#
# This defaults to `URI` from the ruby standard library.
#
# @overload uri_parser
# @return [#parse] the current URI parser object
# @overload uri_parser=
# @param value [#parse] sets the uri_parser
attr_accessor :uri_parser
# Registers a request matcher for later use.
#
# @example
# VCR.configure do |c|
# c.register_request_matcher :port do |request_1, request_2|
# URI(request_1.uri).port == URI(request_2.uri).port
# end
# end
#
# VCR.use_cassette("my_cassette", :match_requests_on => [:method, :host, :port]) do
# # ...
# end
#
# @param name [Symbol] the name of the request matcher
# @yield the request matcher
# @yieldparam request_1 [VCR::Request] One request
# @yieldparam request_2 [VCR::Request] The other request
# @yieldreturn [Boolean] whether or not these two requests should be considered
# equivalent
def register_request_matcher(name, &block)
VCR.request_matchers.register(name, &block)
end
# Sets up a {#before_record} and a {#before_playback} hook that will
# insert a placeholder string in the cassette in place of another string.
# You can use this as a generic way to interpolate a variable into the
# cassette for a unique string. It's particularly useful for unique
# sensitive strings like API keys and passwords.
#
# @example
# VCR.configure do |c|
# # Put "<GITHUB_API_KEY>" in place of the actual API key in
# # our cassettes so we don't have to commit to source control.
# c.filter_sensitive_data('<GITHUB_API_KEY>') { GithubClient.api_key }
#
# # Put a "<USER_ID>" placeholder variable in our cassettes tagged with
# # :user_cassette since it can be different for different test runs.
# c.define_cassette_placeholder('<USER_ID>', :user_cassette) { User.last.id }
# end
#
# @param placeholder [String] The placeholder string.
# @param tag [Symbol] Set this to apply this only to cassettes
# with a matching tag; otherwise it will apply to every cassette.
# @yield block that determines what string to replace
# @yieldparam interaction [(optional) VCR::HTTPInteraction::HookAware] the HTTP interaction
# @yieldreturn the string to replace
def define_cassette_placeholder(placeholder, tag = nil, &block)
before_record(tag) do |interaction|
orig_text = call_block(block, interaction)
log "before_record: replacing #{orig_text.inspect} with #{placeholder.inspect}"
interaction.filter!(orig_text, placeholder)
end
before_playback(tag) do |interaction|
orig_text = call_block(block, interaction)
log "before_playback: replacing #{orig_text.inspect} with #{placeholder.inspect}"
interaction.filter!(placeholder, orig_text)
end
end
alias filter_sensitive_data define_cassette_placeholder
# Gets the registry of cassette serializers. Use it to register a custom serializer.
#
# @example
# VCR.configure do |c|
# c.cassette_serializers[:my_custom_serializer] = my_custom_serializer
# end
#
# @return [VCR::Cassette::Serializers] the cassette serializer registry object.
# @note Custom serializers must implement the following interface:
#
# * `file_extension # => String`
# * `serialize(Hash) # => String`
# * `deserialize(String) # => Hash`
def cassette_serializers
VCR.cassette_serializers
end
# Gets the registry of cassette persisters. Use it to register a custom persister.
#
# @example
# VCR.configure do |c|
# c.cassette_persisters[:my_custom_persister] = my_custom_persister
# end
#
# @return [VCR::Cassette::Persisters] the cassette persister registry object.
# @note Custom persisters must implement the following interface:
#
# * `persister[storage_key]` # returns previously persisted content
# * `persister[storage_key] = content` # persists given content
def cassette_persisters
VCR.cassette_persisters
end
define_hook :before_record
# Adds a callback that will be called before the recorded HTTP interactions
# are serialized and written to disk.
#
# @example
# VCR.configure do |c|
# # Don't record transient 5xx errors
# c.before_record do |interaction|
# interaction.ignore! if interaction.response.status.code >= 500
# end
#
# # Modify the response body for cassettes tagged with :twilio
# c.before_record(:twilio) do |interaction|
# interaction.response.body.downcase!
# end
# end
#
# @param tag [(optional) Symbol] Used to apply this hook to only cassettes that match
# the given tag.
# @yield the callback
# @yieldparam interaction [VCR::HTTPInteraction::HookAware] The interaction that will be
# serialized and written to disk.
# @yieldparam cassette [(optional) VCR::Cassette] The current cassette.
# @see #before_playback
def before_record(tag = nil, &block)
super(tag_filter_from(tag), &block)
end
define_hook :before_playback
# Adds a callback that will be called before a previously recorded
# HTTP interaction is loaded for playback.
#
# @example
# VCR.configure do |c|
# # Don't playback transient 5xx errors
# c.before_playback do |interaction|
# interaction.ignore! if interaction.response.status.code >= 500
# end
#
# # Change a response header for playback
# c.before_playback(:twilio) do |interaction|
# interaction.response.headers['X-Foo-Bar'] = 'Bazz'
# end
# end
#
# @param tag [(optional) Symbol] Used to apply this hook to only cassettes that match
# the given tag.
# @yield the callback
# @yieldparam interaction [VCR::HTTPInteraction::HookAware] The interaction that is being
# loaded.
# @yieldparam cassette [(optional) VCR::Cassette] The current cassette.
# @see #before_record
def before_playback(tag = nil, &block)
super(tag_filter_from(tag), &block)
end
# Adds a callback that will be called with each HTTP request before it is made.
#
# @example
# VCR.configure do |c|
# c.before_http_request(:real?) do |request|
# puts "Request: #{request.method} #{request.uri}"
# end
# end
#
# @param filters [optional splat of #to_proc] one or more filters to apply.
# The objects provided will be converted to procs using `#to_proc`. If provided,
# the callback will only be invoked if these procs all return `true`.
# @yield the callback
# @yieldparam request [VCR::Request::Typed] the request that is being made
# @see #after_http_request
# @see #around_http_request
define_hook :before_http_request
define_hook :after_http_request, :prepend
# Adds a callback that will be called with each HTTP request after it is complete.
#
# @example
# VCR.configure do |c|
# c.after_http_request(:ignored?) do |request, response|
# puts "Request: #{request.method} #{request.uri}"
# puts "Response: #{response.status.code}"
# end
# end
#
# @param filters [optional splat of #to_proc] one or more filters to apply.
# The objects provided will be converted to procs using `#to_proc`. If provided,
# the callback will only be invoked if these procs all return `true`.
# @yield the callback
# @yieldparam request [VCR::Request::Typed] the request that is being made
# @yieldparam response [VCR::Response] the response from the request
# @see #before_http_request
# @see #around_http_request
def after_http_request(*filters)
super(*filters.map { |f| request_filter_from(f) })
end
# Adds a callback that will be executed around each HTTP request.
#
# @example
# VCR.configure do |c|
# c.around_http_request(lambda {|r| r.uri =~ /api.geocoder.com/}) do |request|
# # extract an address like "1700 E Pine St, Seattle, WA"
# # from a query like "address=1700+E+Pine+St%2C+Seattle%2C+WA"
# address = CGI.unescape(URI(request.uri).query.split('=').last)
# VCR.use_cassette("geocoding/#{address}", &request)
# end
# end
#
# @yield the callback
# @yieldparam request [VCR::Request::FiberAware] the request that is being made
# @raise [VCR::Errors::NotSupportedError] if the fiber library cannot be loaded.
# @param filters [optional splat of #to_proc] one or more filters to apply.
# The objects provided will be converted to procs using `#to_proc`. If provided,
# the callback will only be invoked if these procs all return `true`.
# @note This method can only be used on ruby interpreters that support
# fibers (i.e. 1.9+). On 1.8 you can use separate `before_http_request` and
# `after_http_request` hooks.
# @note You _must_ call `request.proceed` or pass the request as a proc on to a
# method that yields to a block (i.e. `some_method(&request)`).
# @see #before_http_request
# @see #after_http_request
def around_http_request(*filters, &block)
unless VCR.fibers_available?
raise Errors::NotSupportedError.new \
"VCR::Configuration#around_http_request requires fibers, " +
"which are not available on your ruby intepreter."
end
fibers = {}
fiber_errors = {}
hook_allowed, hook_declaration = false, caller.first
before_http_request(*filters) do |request|
hook_allowed = true
start_new_fiber_for(request, fibers, fiber_errors, hook_declaration, block)
end
after_http_request(lambda { hook_allowed }) do |request, response|
fiber = fibers.delete(Thread.current)
resume_fiber(fiber, fiber_errors, response, hook_declaration)
end
end
# Configures RSpec to use a VCR cassette for any example
# tagged with `:vcr`.
def configure_rspec_metadata!
unless @rspec_metadata_configured
VCR::RSpec::Metadata.configure!
@rspec_metadata_configured = true
end
end
# An object to log debug output to.
#
# @overload debug_logger
# @return [#puts] the logger
# @overload debug_logger=(logger)
# @param logger [#puts] the logger
# @return [void]
# @example
# VCR.configure do |c|
# c.debug_logger = $stderr
# end
# @example
# VCR.configure do |c|
# c.debug_logger = File.open('vcr.log', 'w')
# end
attr_reader :debug_logger
# @private (documented above)
def debug_logger=(value)
@debug_logger = value
if value
@logger = Logger.new(value)
else
@logger = Logger::Null
end
end
# @private
# Logger object that provides logging APIs and helper methods.
attr_reader :logger
# Sets a callback that determines whether or not to base64 encode
# the bytes of a request or response body during serialization in
# order to preserve them exactly.
#
# @example
# VCR.configure do |c|
# c.preserve_exact_body_bytes do |http_message|
# http_message.body.encoding.name == 'ASCII-8BIT' ||
# !http_message.body.valid_encoding?
# end
# end
#
# @yield the callback
# @yieldparam http_message [#body, #headers] the `VCR::Request` or `VCR::Response` object being serialized
# @yieldparam cassette [VCR::Cassette] the cassette the http message belongs to
# @yieldreturn [Boolean] whether or not to preserve the exact bytes for the body of the given HTTP message
# @return [void]
# @see #preserve_exact_body_bytes_for?
# @note This is usually only necessary when the HTTP server returns a response
# with a non-standard encoding or with a body containing invalid bytes for the given
# encoding. Note that when you set this, and the block returns true, you sacrifice
# the human readability of the data in the cassette.
define_hook :preserve_exact_body_bytes
# @return [Boolean] whether or not the body of the given HTTP message should
# be base64 encoded during serialization in order to preserve the bytes exactly.
# @param http_message [#body, #headers] the `VCR::Request` or `VCR::Response` object being serialized
# @see #preserve_exact_body_bytes
def preserve_exact_body_bytes_for?(http_message)
invoke_hook(:preserve_exact_body_bytes, http_message, VCR.current_cassette).any?
end
private
def initialize
@allow_http_connections_when_no_cassette = nil
@rspec_metadata_configured = false
@default_cassette_options = {
:record => :once,
:match_requests_on => RequestMatcherRegistry::DEFAULT_MATCHERS,
:allow_unused_http_interactions => true,
:serialize_with => :yaml,
:persist_with => :file_system
}
self.uri_parser = URI
self.query_parser = CGI.method(:parse)
self.debug_logger = nil
register_built_in_hooks
end
def load_library_hook(hook)
file = "vcr/library_hooks/#{hook}"
require file
rescue LoadError => e
raise e unless e.message.include?(file) # in case WebMock itself is not available
raise ArgumentError.new("#{hook.inspect} is not a supported VCR HTTP library hook.")
end
def resume_fiber(fiber, fiber_errors, response, hook_declaration)
raise fiber_errors[Thread.current] if fiber_errors[Thread.current]
fiber.resume(response)
rescue FiberError => ex
raise Errors::AroundHTTPRequestHookError.new \
"Your around_http_request hook declared at #{hook_declaration}" \
" must call #proceed on the yielded request but did not. " \
"(actual error: #{ex.class}: #{ex.message})"
end
def create_fiber_for(fiber_errors, hook_declaration, proc)
current_thread = Thread.current
Fiber.new do |*args, &block|
begin
# JRuby Fiber runs in a separate thread, so we need to make this Fiber
# use the context of the calling thread
VCR.link_context(current_thread, Fiber.current) if RUBY_PLATFORM == 'java'
proc.call(*args, &block)
rescue StandardError => ex
# Fiber errors get swallowed, so we re-raise the error in the parent
# thread (see resume_fiber)
fiber_errors[current_thread] = ex
raise
ensure
VCR.unlink_context(Fiber.current) if RUBY_PLATFORM == 'java'
end
end
end
def start_new_fiber_for(request, fibers, fiber_errors, hook_declaration, proc)
fiber = create_fiber_for(fiber_errors, hook_declaration, proc)
fibers[Thread.current] = fiber
fiber.resume(Request::FiberAware.new(request))
end
def tag_filter_from(tag)
return lambda { true } unless tag
lambda { |_, cassette| cassette.tags.include?(tag) }
end
def request_filter_from(object)
return object unless object.is_a?(Symbol)
lambda { |arg| arg.send(object) }
end
def register_built_in_hooks
before_playback(:recompress_response) do |interaction|
interaction.response.recompress if interaction.response.vcr_decompressed?
end
before_playback(:update_content_length_header) do |interaction|
interaction.response.update_content_length_header
end
before_record(:decode_compressed_response) do |interaction|
interaction.response.decompress if interaction.response.compressed?
end
preserve_exact_body_bytes do |http_message, cassette|
cassette && cassette.tags.include?(:preserve_exact_body_bytes)
end
end
def log_prefix
"[VCR::Configuration] "
end
# @private
define_hook :after_library_hooks_loaded
end
end
|